Pepr Kubernetes Application Engine

repository·main·Indexed 42 days ago

https://github.com/defenseunicorns/pepr

A type-safe Kubernetes middleware that allows developers to define transformations, mutations, and validations using TypeScript. Pepr provides a fluent API to implement admission controller logic via Mutate() and Validate() actions, as well as separate controllers using Watch(), Reconcile(), and Finalize() actions for Operator patterns and real-time resource tracking.

Tokens
53.2K
Snippets
164
Records
220
Agent score
53%

What's inside Pepr

  1. What is the Pepr Store and when to use it

    main

    The Pepr Store is a lightweight, asynchronous key-value store designed for use within Pepr Capabilities. It is specifically built to solve two problems in Kubernetes admission controllers:

    1. Complex/Long-running operations: It provides a way to manage state that is difficult to handle within the standard Mutate, Validate, and Watch action lifecycles.
    2. Data Sharing: It allows sharing data between different actions and different replicas of the admission controller and watch controller automatically.

    How it works internally: Behind the scenes, all store instances within a single Pepr Module are stored within a single Custom Resource Definition (CRD) that is automatically created when the module is deployed. To maintain performance at scale, the store uses Kubernetes watches, batch processing, and patch operations for efficient reads and writes.

  2. Use Reconcile for building Kubernetes Controllers and Operators

    main

    The Reconcile function is designed for building Kubernetes Controllers and Operators. Unlike Watch, Reconcile processes callback operations in a Queue, which guarantees ordered and synchronous processing of events even under heavy load.

    By default, ordering is configured as a single queue that maintains the ordering of operations across all resources of a specific kind. However, you can configure it to use separate processing queues per resource instance. For detailed configuration options, see the Configuring Reconcile guide.

    When(WebApp)
      .IsCreatedOrUpdated()
      .Validate(validator) // Validate the shape of the resource
      .Reconcile(async instance => {
        try {
          Store.setItem(instance.metadata.name, JSON.stringify(instance));
          await reconciler(instance); // Reconcile the resource - Deploy Kubernetes manifests, etc.
        } catch (error) {
          Log.info(`Error reconciling instance of WebApp`);
        }
      });
  3. Difference between .Watch() and .Reconcile() in Pepr

    main

    Pepr provides two primary mechanisms for handling Kubernetes events, differing in how they manage event order and processing:

    • .Watch(): Processes events immediately as they arrive. It does not guarantee event ordering. Use this when immediate reaction is acceptable and the sequence of events (e.g., an update followed by a delete) does not impact the correctness of your logic.
    • .Reconcile(): Processes events sequentially through a queue. This ensures that events are handled in the order they were received, which is critical for preventing race conditions during resource updates or deletions.

    Recommendation: For most real-world workloads where consistency and event order are critical, use .Reconcile() with the kindNsName configuration.

    /* Use .Reconcile() when consistency and event order are critical */
  4. Module Resolution Strategy for Pepr

    main

    Pepr uses a hybrid build strategy to support both CommonJS (CJS) and ECMA Script Modules (ESM). While the project was originally focused on CJS, it now supports both module types. For the most up-to-date information on how Pepr handles module resolution, refer to ADR-0021 (TypeScript Module Resolution).

    This strategy ensures:

    • Efficient Debugging: Maintains the pepr dev experience with zero-config breakpoint inspection in VSCode without offset issues between TS source and transpiled code.
    • Reliable Bundling: Produces a single-file, small-footprint output for the runtime controller that avoids ESM import breakage.
    • Unified Workflow: Uses the same code and configurations for both pepr dev and pepr build, reducing the risk of environment-specific bugs.
  5. How compliance reporting works in Pepr

    main

    Pepr handles compliance through reporting rather than enforcement. Compliance enforcement is handled by the bindings themselves, while Pepr uses the external compliance-reporter library to aggregate and report on the status of those controls.

    To implement compliance reporting, you use a pattern of registering controls and then mapping implementations to them. This allows Pepr to identify 'compliance gaps' where controls exist but lack sufficient implementation coverage.

  6. Implement the Validate-After-Mutate Pattern

    main

    When using mutating admission policies, always include a Validate block immediately after your Mutate block. This pattern is critical for:

    1. Detecting Misconfigurations: Catching bugs in mutation logic before they reach the cluster.
    2. Maintaining Integrity: Ensuring the mutated resource adheres to expected formats.
    3. Handling failurePolicy: ignore: If your module is configured to ignore failures, validation is the only way to prevent invalid resources from being accepted.
    4. Mitigating Webhook Timeouts: Catching errors before they cause timeout-related rejections.

    Pattern Workflow:

    1. Apply transformations in .Mutate().
    2. Validate the result in .Validate().
    3. Use .Deny("message") if the validation fails.
    When(a.Pod)
      .IsCreated()
      .InNamespace("my-app")
      .WithName("database")
      .Mutate(po => po.SetLabel("pepr", "true"))
      .Validate(po => {
        if (po.Raw.metadata?.labels["pepr"] !== "true") {
          return po.Approve();
        }
        return po.Deny("Needs pepr label set to true")
      });
  7. Configure Pepr Git Hooks

    main

    Pepr uses husky to manage git hooks:

    • pre-commit: Automatically runs linters to ensure code adheres to style guides.
    • pre-push: Warns you if a branch has a large number of changed lines to encourage smaller, more reviewable PRs.

    The pre-push hook is optional. You can opt-in by setting the PEPR_HOOK_OPT_IN=1 environment variable.

  8. Understand Pepr Store Consistency

    main

    The PeprStore is backed by ETCD in a PeprStore resource.

    Key Characteristics:

    • Update Interval: Updates occur at 5-second intervals when an array of patches is sent to the Kubernetes API Server.
    • Consistency Model: The store is eventually consistent, not transactional. The last operation within an interval will be persisted, potentially overwriting other operations.
    • Caution: Because changes are not guaranteed to occur simultaneously, you must manage errors and ensure consistency carefully in your logic.
  9. Understand Pepr's backward compatibility scope

    main

    Pepr's backward compatibility guarantees apply to four primary areas of the project. When upgrading, you should verify compatibility across these surfaces:

    1. Public API: Function signatures, parameters, and return types for core abstractions like Actions, Capabilities, Store, and OnSchedule.
    2. Configuration: YAML or JSON structures used for Pepr Modules, including Environment Variables and customization settings (e.g., log formats, reconcile configuration, Helm/package.json customizations).
    3. Pepr SDK: Helper functions provided in the SDK for common tasks.
    4. CLI Commands: All commands, flags, and output formats provided by the Pepr CLI.
  10. How Mutate actions work in Pepr

    main

    Mutating admission webhooks in Pepr are invoked first to modify objects sent to the API server, allowing you to enforce custom defaults.

    When a mutation is successful, Pepr annotates the object to indicate its status. The annotation format follows the pattern: <module-uuid>.<pepr-domain>/<capability-name>: succeeded.

    For example, if a module has UUID static-test and a capability named hello-pepr, a successful mutation will result in the annotation: static-test.pepr.dev/hello-pepr: succeeded.

  11. How Pepr Filters work

    main

    Filters are functions that evaluate an AdmissionReview or Watch event and return a boolean. They allow you to restrict Pepr actions (like Mutate or Validate) to only specific resources that meet defined criteria. You chain filter methods onto a resource type selector (e.g., When(a.ConfigMap)) to build a predicate. If the filter returns true, the subsequent action is executed; otherwise, it is skipped.

    When(a.ConfigMap)
      .IsCreated()
      .InNamespace("webapp")
      .WithName("example-1")
      .WithLabel("app", "webapp")
      .Mutate(request => {
        // This only runs if the ConfigMap is new, in 'webapp' namespace, 
        // named 'example-1', and has label 'app=webapp'
        request.SetLabel("pepr", "was-here");
      });
  12. How Pepr Modules, Capabilities, and Actions work together

    main

    Pepr uses a hierarchical model to define Kubernetes transformations:

    Module

    A Module is the top-level TypeScript project. It serves as a collection of capabilities and includes the entry point for loading configurations and actions. During the build process, a module produces a secret containing transpiled code. If the module contains admission actions (Mutate() or Validate()), it also produces the corresponding Kubernetes Webhook configurations.

    Capability

    A Capability is a set of related Actions that work together to achieve a specific transformation or operation on Kubernetes resources. Capabilities are defined within a module and can be scoped (e.g., for mutating or validating) and reused across multiple modules.

    Action

    An Action is the atomic unit of behavior. It is a discrete function that acts on a specific Kubernetes GroupVersionKind (GVK).

    Types of Actions:

    • Mutate(): Modifies Kubernetes resources within the admission controller lifecycle.
    • Validate(): Validates Kubernetes resources within the admission controller lifecycle.
    • Watch(): Watches for changes to existing Kubernetes resources (similar to controller-runtime's Manager.Watch()).
    • Reconcile(): Reconciles existing Kubernetes resources.
    • Finalize(): Performs cleanup operations after a resource is deleted (used after Watch() or Reconcile()).