pepr 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { pickBy } from "ramda";
8
8
  import Log from "./logger";
9
9
  import { isBuildMode, isDevMode, isWatchMode } from "./module";
10
10
  import { PeprStore, Storage } from "./storage";
11
+ import { OnSchedule, Schedule } from "./schedule";
11
12
  import {
12
13
  Binding,
13
14
  BindingFilter,
@@ -34,7 +35,39 @@ export class Capability implements CapabilityExport {
34
35
  #namespaces?: string[] | undefined;
35
36
  #bindings: Binding[] = [];
36
37
  #store = new Storage();
38
+ #scheduleStore = new Storage();
37
39
  #registered = false;
40
+ #scheduleRegistered = false;
41
+ hasSchedule: boolean;
42
+
43
+ /**
44
+ * Run code on a schedule with the capability.
45
+ *
46
+ * @param schedule The schedule to run the code on
47
+ * @returns
48
+ */
49
+ OnSchedule: (schedule: Schedule) => void = (schedule: Schedule) => {
50
+ const { name, every, unit, run, startTime, completions } = schedule;
51
+
52
+ if (process.env.PEPR_WATCH_MODE === "true") {
53
+ // Only create/watch schedule store if necessary
54
+ this.hasSchedule = true;
55
+
56
+ // Create a new schedule
57
+ const newSchedule: Schedule = {
58
+ name,
59
+ every,
60
+ unit,
61
+ run,
62
+ startTime,
63
+ completions,
64
+ };
65
+
66
+ this.#scheduleStore.onReady(() => {
67
+ new OnSchedule(newSchedule).setStore(this.#scheduleStore);
68
+ });
69
+ }
70
+ };
38
71
 
39
72
  /**
40
73
  * Store is a key-value data store that can be used to persist data that should be shared
@@ -50,6 +83,24 @@ export class Capability implements CapabilityExport {
50
83
  setItem: this.#store.setItem,
51
84
  subscribe: this.#store.subscribe,
52
85
  onReady: this.#store.onReady,
86
+ setItemAndWait: this.#store.setItemAndWait,
87
+ };
88
+
89
+ /**
90
+ * ScheduleStore is a key-value data store used to persist schedule data that should be shared
91
+ * between intervals. Each Schedule shares store, and the data is persisted in Kubernetes
92
+ * in the `pepr-system` namespace.
93
+ *
94
+ * Note: There is no direct access to schedule store
95
+ */
96
+ ScheduleStore: PeprStore = {
97
+ clear: this.#scheduleStore.clear,
98
+ getItem: this.#scheduleStore.getItem,
99
+ removeItem: this.#scheduleStore.removeItem,
100
+ setItemAndWait: this.#scheduleStore.setItemAndWait,
101
+ setItem: this.#scheduleStore.setItem,
102
+ subscribe: this.#scheduleStore.subscribe,
103
+ onReady: this.#scheduleStore.onReady,
53
104
  };
54
105
 
55
106
  get bindings() {
@@ -72,11 +123,32 @@ export class Capability implements CapabilityExport {
72
123
  this.#name = cfg.name;
73
124
  this.#description = cfg.description;
74
125
  this.#namespaces = cfg.namespaces;
126
+ this.hasSchedule = false;
75
127
 
76
128
  Log.info(`Capability ${this.#name} registered`);
77
129
  Log.debug(cfg);
78
130
  }
79
131
 
132
+ /**
133
+ * Register the store with the capability. This is called automatically by the Pepr controller.
134
+ *
135
+ * @param store
136
+ */
137
+ registerScheduleStore = () => {
138
+ Log.info(`Registering schedule store for ${this.#name}`);
139
+
140
+ if (this.#scheduleRegistered) {
141
+ throw new Error(`Schedule store already registered for ${this.#name}`);
142
+ }
143
+
144
+ this.#scheduleRegistered = true;
145
+
146
+ // Pass back any ready callback to the controller
147
+ return {
148
+ scheduleStore: this.#scheduleStore,
149
+ };
150
+ };
151
+
80
152
  /**
81
153
  * Register the store with the capability. This is called automatically by the Pepr controller.
82
154
  *
@@ -44,10 +44,14 @@ export class Controller {
44
44
  this.#capabilities = capabilities;
45
45
 
46
46
  // Initialize the Pepr store for each capability
47
- new PeprControllerStore(config, capabilities, () => {
47
+ new PeprControllerStore(config, capabilities, `pepr-${config.uuid}-store`, () => {
48
48
  this.#bindEndpoints();
49
49
  onReady && onReady();
50
50
  Log.info("✅ Controller startup complete");
51
+ // Initialize the schedule store for each capability
52
+ new PeprControllerStore(config, capabilities, `pepr-${config.uuid}-schedule`, () => {
53
+ Log.info("✅ Scheduling processed");
54
+ });
51
55
  });
52
56
 
53
57
  // Middleware for logging requests
@@ -12,7 +12,7 @@ import { ModuleConfig } from "../module";
12
12
  import { DataOp, DataSender, DataStore, Storage } from "../storage";
13
13
 
14
14
  const namespace = "pepr-system";
15
- const debounceBackoff = 5000;
15
+ export const debounceBackoff = 5000;
16
16
 
17
17
  export class PeprControllerStore {
18
18
  #name: string;
@@ -20,22 +20,40 @@ export class PeprControllerStore {
20
20
  #sendDebounce: NodeJS.Timeout | undefined;
21
21
  #onReady?: () => void;
22
22
 
23
- constructor(config: ModuleConfig, capabilities: Capability[], onReady?: () => void) {
23
+ constructor(config: ModuleConfig, capabilities: Capability[], name: string, onReady?: () => void) {
24
24
  this.#onReady = onReady;
25
25
 
26
26
  // Setup Pepr State bindings
27
- this.#name = `pepr-${config.uuid}-store`;
27
+ this.#name = name;
28
+
29
+ if (name.includes("schedule")) {
30
+ // Establish the store for each capability
31
+ for (const { name, registerScheduleStore, hasSchedule } of capabilities) {
32
+ // Guard Clause to exit early
33
+ if (hasSchedule !== true) {
34
+ return;
35
+ }
36
+ // Register the scheduleStore with the capability
37
+ const { scheduleStore } = registerScheduleStore();
38
+
39
+ // Bind the store sender to the capability
40
+ scheduleStore.registerSender(this.#send(name));
28
41
 
29
- // Establish the store for each capability
30
- for (const { name, registerStore } of capabilities) {
31
- // Register the store with the capability
32
- const { store } = registerStore();
42
+ // Store the storage instance
43
+ this.#stores[name] = scheduleStore;
44
+ }
45
+ } else {
46
+ // Establish the store for each capability
47
+ for (const { name, registerStore } of capabilities) {
48
+ // Register the store with the capability
49
+ const { store } = registerStore();
33
50
 
34
- // Bind the store sender to the capability
35
- store.registerSender(this.#send(name));
51
+ // Bind the store sender to the capability
52
+ store.registerSender(this.#send(name));
36
53
 
37
- // Store the storage instance
38
- this.#stores[name] = store;
54
+ // Store the storage instance
55
+ this.#stores[name] = store;
56
+ }
39
57
  }
40
58
 
41
59
  // Add a jitter to the Store creation to avoid collisions
@@ -2,6 +2,7 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
4
  import { CapabilityExport } from "./types";
5
+ import { promises as fs } from "fs";
5
6
 
6
7
  type RBACMap = {
7
8
  [key: string]: {
@@ -37,3 +38,15 @@ export const createRBACMap = (capabilities: CapabilityExport[]): RBACMap => {
37
38
  return acc;
38
39
  }, {});
39
40
  };
41
+
42
+ export async function createDirectoryIfNotExists(path: string) {
43
+ try {
44
+ await fs.access(path);
45
+ } catch (error) {
46
+ if (error.code === "ENOENT") {
47
+ await fs.mkdir(path, { recursive: true });
48
+ } else {
49
+ throw error;
50
+ }
51
+ }
52
+ }
package/src/lib/module.ts CHANGED
@@ -87,6 +87,7 @@ export class PeprModule {
87
87
  description: capability.description,
88
88
  namespaces: capability.namespaces,
89
89
  bindings: capability.bindings,
90
+ hasSchedule: capability.hasSchedule,
90
91
  });
91
92
  }
92
93
 
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { PeprStore } from "./storage";
5
+
6
+ type Unit = "seconds" | "second" | "minute" | "minutes" | "hours" | "hour";
7
+
8
+ export interface Schedule {
9
+ /**
10
+ * * The name of the store
11
+ */
12
+ name: string;
13
+ /**
14
+ * The value associated with a unit of time
15
+ */
16
+ every: number;
17
+ /**
18
+ * The unit of time
19
+ */
20
+ unit: Unit;
21
+ /**
22
+ * The code to run
23
+ */
24
+ run: () => void;
25
+ /**
26
+ * The start time of the schedule
27
+ */
28
+ startTime?: Date | undefined;
29
+
30
+ /**
31
+ * The number of times the schedule has run
32
+ */
33
+ completions?: number | undefined;
34
+ /**
35
+ * Tje intervalID to clear the interval
36
+ */
37
+ intervalID?: NodeJS.Timeout;
38
+ }
39
+
40
+ export class OnSchedule implements Schedule {
41
+ intervalId: NodeJS.Timeout | null = null;
42
+ store: PeprStore | undefined;
43
+ name!: string;
44
+ completions?: number | undefined;
45
+ every: number;
46
+ unit: Unit;
47
+ run!: () => void;
48
+ startTime?: Date | undefined;
49
+ duration: number | undefined;
50
+ lastTimestamp: Date | undefined;
51
+
52
+ constructor(schedule: Schedule) {
53
+ this.name = schedule.name;
54
+ this.run = schedule.run;
55
+ this.every = schedule.every;
56
+ this.unit = schedule.unit;
57
+ this.startTime = schedule?.startTime;
58
+ this.completions = schedule?.completions;
59
+ }
60
+ setStore(store: PeprStore) {
61
+ this.store = store;
62
+ this.startInterval();
63
+ }
64
+ startInterval() {
65
+ this.checkStore();
66
+ this.getDuration();
67
+ this.setupInterval();
68
+ }
69
+ /**
70
+ * Checks the store for this schedule and sets the values if it exists
71
+ * @returns
72
+ */
73
+ checkStore() {
74
+ const result = this.store && this.store.getItem(this.name);
75
+ if (result) {
76
+ const storedSchedule = JSON.parse(result);
77
+ this.completions = storedSchedule?.completions;
78
+ this.startTime = storedSchedule?.startTime;
79
+ this.lastTimestamp = storedSchedule?.lastTimestamp;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Saves the schedule to the store
85
+ * @returns
86
+ */
87
+ saveToStore() {
88
+ const schedule = {
89
+ completions: this.completions,
90
+ startTime: this.startTime,
91
+ lastTimestamp: new Date(),
92
+ name: this.name,
93
+ };
94
+ this.store && this.store.setItem(this.name, JSON.stringify(schedule));
95
+ }
96
+
97
+ /**
98
+ * Gets the durations in milliseconds
99
+ */
100
+ getDuration() {
101
+ switch (this.unit) {
102
+ case "seconds":
103
+ if (this.every < 10) throw new Error("10 Seconds in the smallest interval allowed");
104
+ this.duration = 1000 * this.every;
105
+ break;
106
+ case "minutes":
107
+ case "minute":
108
+ this.duration = 1000 * 60 * this.every;
109
+ break;
110
+ case "hours":
111
+ case "hour":
112
+ this.duration = 1000 * 60 * 60 * this.every;
113
+ break;
114
+ default:
115
+ throw new Error("Invalid time unit");
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Sets up the interval
121
+ */
122
+ setupInterval() {
123
+ const now = new Date();
124
+ let delay: number | undefined;
125
+
126
+ if (this.lastTimestamp && this.startTime) {
127
+ this.startTime = undefined;
128
+ }
129
+
130
+ if (this.startTime) {
131
+ delay = this.startTime.getTime() - now.getTime();
132
+ } else if (this.lastTimestamp && this.duration) {
133
+ const lastTimestamp = new Date(this.lastTimestamp);
134
+ delay = this.duration - (now.getTime() - lastTimestamp.getTime());
135
+ }
136
+
137
+ if (delay === undefined || delay <= 0) {
138
+ this.start();
139
+ } else {
140
+ setTimeout(() => {
141
+ this.start();
142
+ }, delay);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Starts the interval
148
+ */
149
+ start() {
150
+ this.intervalId = setInterval(() => {
151
+ if (this.completions === 0) {
152
+ this.stop();
153
+ return;
154
+ } else {
155
+ this.run();
156
+
157
+ if (this.completions && this.completions !== 0) {
158
+ this.completions -= 1;
159
+ }
160
+ this.saveToStore();
161
+ }
162
+ }, this.duration);
163
+ }
164
+
165
+ /**
166
+ * Stops the interval
167
+ */
168
+ stop() {
169
+ if (this.intervalId) {
170
+ clearInterval(this.intervalId);
171
+ this.intervalId = null;
172
+ }
173
+ this.store && this.store.removeItem(this.name);
174
+ }
175
+ }
@@ -10,6 +10,7 @@ export type DataSender = (op: DataOp, keys: string[], value?: string) => void;
10
10
  export type DataReceiver = (data: DataStore) => void;
11
11
  export type Unsubscribe = () => void;
12
12
 
13
+ const MAX_WAIT_TIME = 15000;
13
14
  export interface PeprStore {
14
15
  /**
15
16
  * Returns the current value associated with the given key, or null if the given key does not exist.
@@ -40,6 +41,12 @@ export interface PeprStore {
40
41
  * Register a function to be called when the store is ready.
41
42
  */
42
43
  onReady(callback: DataReceiver): void;
44
+
45
+ /**
46
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
47
+ * Resolves when the key/value show up in the store.
48
+ */
49
+ setItemAndWait(key: string, value: string): Promise<void>;
43
50
  }
44
51
 
45
52
  /**
@@ -88,6 +95,32 @@ export class Storage implements PeprStore {
88
95
  this.#dispatchUpdate("add", [key], value);
89
96
  };
90
97
 
98
+ /**
99
+ * Creates a promise and subscribes to the store, the promise resolves when
100
+ * the key and value are seen in the store.
101
+ *
102
+ * @param key - The key to add into the store
103
+ * @param value - The value of the key
104
+ * @returns
105
+ */
106
+ setItemAndWait = (key: string, value: string) => {
107
+ this.#dispatchUpdate("add", [key], value);
108
+ return new Promise<void>((resolve, reject) => {
109
+ const unsubscribe = this.subscribe(data => {
110
+ if (data[key] === value) {
111
+ unsubscribe();
112
+ resolve();
113
+ }
114
+ });
115
+
116
+ // If promise has not resolved before MAX_WAIT_TIME reject
117
+ setTimeout(() => {
118
+ unsubscribe();
119
+ return reject();
120
+ }, MAX_WAIT_TIME);
121
+ });
122
+ };
123
+
91
124
  subscribe = (subscriber: DataReceiver) => {
92
125
  const idx = this.#subscriberId++;
93
126
  this.#subscribers[idx] = subscriber;
package/src/lib/types.ts CHANGED
@@ -43,6 +43,7 @@ export interface CapabilityCfg {
43
43
 
44
44
  export interface CapabilityExport extends CapabilityCfg {
45
45
  bindings: Binding[];
46
+ hasSchedule: boolean;
46
47
  }
47
48
 
48
49
  export type WhenSelector<T extends GenericClass> = {
package/src/lib.ts CHANGED
@@ -1,32 +1,25 @@
1
- import { K8s, RegisterKind, fetch, kind, kind as a, fetchStatus } from "kubernetes-fluent-client";
1
+ import { K8s, RegisterKind, kind as a, fetch, fetchStatus, kind } from "kubernetes-fluent-client";
2
2
  import * as R from "ramda";
3
3
 
4
4
  import { Capability } from "./lib/capability";
5
5
  import Log from "./lib/logger";
6
6
  import { PeprModule } from "./lib/module";
7
7
  import { PeprMutateRequest } from "./lib/mutate-request";
8
- import { PeprValidateRequest } from "./lib/validate-request";
9
8
  import * as PeprUtils from "./lib/utils";
10
-
11
- // Import type information for external packages
12
- import type * as RTypes from "ramda";
9
+ import { PeprValidateRequest } from "./lib/validate-request";
13
10
 
14
11
  export {
15
- a,
16
- kind,
17
- /** PeprModule is used to setup a complete Pepr Module: `new PeprModule(cfg, {...capabilities})` */
12
+ Capability,
13
+ K8s,
14
+ Log,
18
15
  PeprModule,
19
16
  PeprMutateRequest,
20
- PeprValidateRequest,
21
17
  PeprUtils,
22
- RegisterKind,
23
- K8s,
24
- Capability,
25
- Log,
18
+ PeprValidateRequest,
26
19
  R,
20
+ RegisterKind,
21
+ a,
27
22
  fetch,
28
23
  fetchStatus,
29
-
30
- // Export the imported type information for external packages
31
- RTypes,
24
+ kind,
32
25
  };
@@ -1 +1 @@
1
- $secondary: #EFCA81;
1
+ $secondary: #EFCA81;
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: OnSchedule
3
+ linkTitle: OnSchedule
4
+ ---
5
+
6
+ # OnSchedule
7
+
8
+ The `OnSchedule` feature allows you to schedule and automate the execution of specific code at predefined intervals or schedules. This feature is designed to simplify recurring tasks and can serve as an alternative to traditional CronJobs. This code is designed to be run at the top level on a Capability, not within a function like `When`.
9
+
10
+ > **Note -** To use this feature in dev mode you MUST set `PEPR_WATCH_MODE="true"`. This is because the scheduler only runs on the watch controller and the watch controller is not started by default in dev mode.
11
+
12
+ For example: `PEPR_WATCH_MODE="true" npx pepr dev`
13
+
14
+ ## Best Practices
15
+
16
+ `OnSchedule` is designed for targeting intervals equal to or larger than 30 seconds due to the storage mechanism used to archive schedule info.
17
+
18
+ ## Usage
19
+
20
+ Create a recurring task execution by calling the OnSchedule function with the following parameters:
21
+
22
+ **name** - The unique name of the schedule.
23
+
24
+ **every** - An integer that represents the frequency of the schedule in number of _units_.
25
+
26
+ **unit** - A string specifying the time unit for the schedule (e.g., `seconds`, `minute`, `minutes`, `hour`, `hours`).
27
+
28
+ **startTime** - (Optional) A UTC timestamp indicating when the schedule should start. All date times must be provided in GMT. If not specified the schedule will start when the schedule store reports ready.
29
+
30
+ **run** - A function that contains the code you want to execute on the defined schedule.
31
+
32
+ **completions** - (Optional) An integer indicating the maximum number of times the schedule should run to completion. If not specified the schedule will run indefinitely.
33
+
34
+
35
+ ## Examples
36
+
37
+ Update the curr ConfigMap every 15 seconds and use the store to track the current count:
38
+
39
+ ```typescript
40
+ OnSchedule({
41
+ name: "hello-interval",
42
+ every: 30,
43
+ unit: "seconds",
44
+ run: async () => {
45
+ Log.info("Wait 30 seconds and create/update a ConfigMap");
46
+
47
+ try {
48
+ await K8s(kind.ConfigMap).Apply({
49
+ metadata: {
50
+ name: "last-updated",
51
+ namespace: "default",
52
+ },
53
+ data: {
54
+ count: `${new Date()}`,
55
+ },
56
+ });
57
+
58
+ } catch (error) {
59
+ Log.error(error, "Failed to apply ConfigMap using server-side apply.");
60
+ }
61
+ },
62
+ });
63
+ ```
64
+
65
+ Refresh an AWSToken every 24 hours, with a delayed start of 30 seconds, running a total of 3 times:
66
+
67
+ ```typescript
68
+
69
+ OnSchedule({
70
+ name: "refresh-aws-token",
71
+ every: 24,
72
+ unit: "hours",
73
+ startTime: new Date(new Date().getTime() + 1000 * 30),
74
+ run: async () => {
75
+ await RefreshAWSToken();
76
+ },
77
+ completions: 3,
78
+ });
79
+ ```
80
+
81
+ ## Advantages
82
+
83
+ - Simplifies scheduling recurring tasks without the need for complex CronJob configurations.
84
+ - Provides flexibility to define schedules in a human-readable format.
85
+ - Allows you to execute code with precision at specified intervals.
86
+ - Supports limiting the number of schedule completions for finite tasks.
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: Store
3
+ linkTitle: Store
4
+ ---
5
+
6
+ # Pepr Store: A Lightweight Key-Value Store for Pepr Modules
7
+
8
+ The nature of admission controllers and general watch operations (the `Mutate`, `Validate` and `Watch` actions in Pepr) make some types of complex and long-running operations difficult. There are also times when you need to share data between different actions. While you could manually create your own K8s resources and manage their cleanup, this can be very hard to track and keep performant at scale.
9
+
10
+ The Pepr Store solves this by exposing a simple, [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Storage)-compatible mechanism for use within capabilities. Additionally, as Pepr runs multiple replicas of the admission controller along with a watch controller, the Pepr Store provides a unique way to share data between these different instances automatically.
11
+
12
+ Each Pepr Capability has a `Store` instance that can be used to get, set and delete data as well as subscribe to any changes to the Store. Behind the scenes, all capability store instances in a single Pepr Module are stored within a single CRD in the cluster. This CRD is automatically created when the Pepr Module is deployed. Care is taken to make the read and write operations as efficient as possible by using K8s watches, batch processing and patch operations for writes.
13
+
14
+ ## Key Features
15
+
16
+ - **Asynchronous Key-Value Store**: Provides an asynchronous interface for storing small amounts of data, making it ideal for sharing information between various actions and capabilities.
17
+ - **Web Storage API Compatibility**: The store's API is aligned with the standard [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Storage), simplifying the learning curve.
18
+ - **Real-time Updates**: The `.subscribe()` and `onReady()` methods enable real-time updates, allowing you to react to changes in the data store instantaneously.
19
+
20
+ - **Automatic CRD Management**: Each Pepr Module has its data stored within a single Custom Resource Definition (CRD) that is automatically created upon deployment.
21
+ - **Efficient Operations**: Pepr Store uses Kubernetes watches, batch processing, and patch operations to make read and write operations as efficient as possible.
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ // Example usage for Pepr Store
27
+ Store.setItem("example-1", "was-here");
28
+ Store.setItem("example-1-data", JSON.stringify(request.Raw.data));
29
+ Store.onReady(data => {
30
+ Log.info(data, "Pepr Store Ready");
31
+ });
32
+ const unsubscribe = Store.subscribe(data => {
33
+ Log.info(data, "Pepr Store Updated");
34
+ unsubscribe();
35
+ });
36
+ ```
37
+
38
+ ## API Reference
39
+
40
+ ### Methods
41
+
42
+ - `getItem(key: string)`: Retrieves a value by its key. Returns `null` if the key doesn't exist.
43
+ - `setItem(key: string, value: string)`: Sets a value for a given key. Creates a new key-value pair if the key doesn't exist.
44
+ - `setItemAndWait(key: string, value: string)`: Sets a value for a given key. Creates a new key-value pair if the key doesn't exist. Returns a promise when the new key and value show up in the store. Should only be used on a `Watch` to avoid [timeouts](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#timeouts).
45
+ - `removeItem(key: string)`: Deletes a key-value pair by its key.
46
+ - `clear()`: Clears all key-value pairs from the store.
47
+ - `subscribe(listener: DataReceiver)`: Subscribes to store updates.
48
+ - `onReady(callback: DataReceiver)`: Executes a callback when the store is ready.