Project: Pinniped (Provisioning / Key Management)
File: code-walkthrough.md

title: Code Walk-through description: A brief overview of the Pinniped source code. cascade: layout: docs menu: docs: name: Code Walk-through weight: 60 parent: reference The purpose of this document is to provide a high-level, brief introduction to the Pinniped source code for new contributors. The target audience is someone who wants to read the source code. Users who only want to install and configure Pinniped should not need to read this document. This document aims to help a reader navigate towards the part of the code which they might be interested in exploring in more detail. We take an "outside-in" approach, describing how to start finding and understanding code based on several flavors of "outside" entry points. This document avoids getting too detailed in its description because the details of the code will change over time. New contributors are also encouraged to read the and . There are three binaries in the Pinniped source: The Pinniped CLI The `main()` function is in . Each is in a file: `pinniped version` in `pinniped whoami` in `pinniped get kubeconfig` in The following subcommands are not typically used directly by and end user. Instead, they are usually embedded as a kubectl credential exec plugin in a kubeconfig file: `pinniped login oidc` in `pinniped login static` in The Pinniped Kube cert agent component The Kube cert agent is a very simple binary that is sometimes deployed by the Pinniped Concierge server component at runtime as a separate Deployment. It exists as a separate binary in the same container image as the other Pinniped server components. When needed, the Concierge will exec into the Deployment's pods to invoke the cert agent binary to query for the cluster's keypair, which is used to sign client certificates used to access the Kubernetes API server. This is to support the Token Credential Request API strategy described in the . The Kube cert agent code is in . The Pinniped server components There are three server components. They are all compiled into a single binary in a single container image by the project's . The `main()` function chooses which component to start based on the path used to invoke the binary, as seen in . The Concierge can be installed on a cluster to authenticate users externally via dynamically registered authenticators, and then allow those users to access the cluster's API server using that identity. The Concierge's entry point is in . The Supervisor can be installed on a central cluster to provide single-sign capabilities on to other clusters, using various types of external identity providers as the source of user identity. The Supervisor's entry point is in . The Local User Authenticator is a component used only for integration testing and demos of the

At this time, it is not intended for production use. It can be registered as a WebhookAuthenticator with the Concierge. It is implemented in . The YAML manifests required to deploy the server-side components to Kubernetes clusters are in the directory. For each release, these ytt templates are rendered by the CI/CD system. The reference to the container image for that release is templated in, but otherwise the default values from the respective `values.yaml` files are used. The resulting manifests are attached to each . Users may use these pre-rendered manifests to install the Supervisor or Concierge. Alternatively, a user may render the templates of any release themselves to customize the values in `values.yaml` using ytt by following the or . CRDs are used to configure both the Supervisor and the Concierge. The source code for these can be found in the `.tmpl` files under the various subdirectories of the directory. Any struct with the special `+kubebuilder:resource:` comment will become a CRD. After adding or changing one of these files, the code generator may be executed by running and the results will be written to the directory, where they may be committed. Other `.tmpl` files that do not use the `+kubebuilder:resource:` comments will also be picked up by the code generator. These will not become CRDs, but are also considered part of Pinniped's public API for golang client code to use. Both the Supervisor and Concierge components use Kubernetes-style controllers to watch resources and to take action to converge towards a desired state. For example, all the Pinniped CRDs are watched by controllers. All controllers are written using a custom controller library which is in the directory. Each individual controller is implemented as a file in one of the subdirectories of . Each server component uses `controllerlib.NewManager()` and then adds all of its controller instances to the manager on subsequent lines, which can be read as a catalog of all controllers. This happens: For the Concierge, in For the Supervisor, in Each controller mostly follows a general pattern: Each has a constructor-like function which internally registers which resources the controller would like to watch. Whenever one of the watched resources changes, or whenever about 3 minutes has elapsed, the `Sync()` method is called. The `Sync()` method reads state from informer caches to understand the actual current state of the world. The `Sync()` method then performs business logic to determine the desired state of the world, and makes updates to the world to converge towards the desired state. It may create/update/delete Kubernetes resources by calling the Kubernetes API, or it may update an in-memory cache of objects that are shared by other parts of the code (often an API endpoint's implementation), or it may perform other

The `Sync()` method is generally written to be idempotent and reasonably performant because it can be called fairly often. Some controllers are written to collaborate with other controllers. For example, one controller might create a Secret and annotate it with an expiration timestamp, while another controller watches those Secrets to delete any that are beyond their expiration time. A simple example of a controller which employs these patterns is in . The Supervisor and Concierge each usually consist of multiple replica Pods. Each replica runs an identical copy of the server component. For example, if there are two Supervisor replica Pods, then there are two identical copies of the Supervisor software running, each running identical copies of all Supervisor controllers. Leader election is used to help avoid confusing situations where two identical controllers race to come to the same nearly-simultaneous conclusions. Or even worse, they may race to come to different nearly-simultaneous conclusions, caused by one controller's caches lagging slightly behind or by new business logic introduced to an existing controller during a rolling upgrade. Leader election is done transparently in a centralized client middleware component, and will not be immediately obvious when looking at the controller source code. The middleware is in . One of the Pods is always elected as leader, and only the Kubernetes API client calls made by that pod are allowed to perform writes. The non-leader Pods' controllers are always running, but their writes will always fail, so they are unable to compete to make changes to Kubernetes resources. These failed write operations will appear in the logs as write errors due to not being the leader. While this might look like an error, this is normal for the controllers. This still allows the non-leader Pods' controllers to read state, update in-memory caches, etc. The Concierge hosts the following endpoints, which are automatically registered with the Kubernetes API server as aggregated API endpoints, which makes them appear to a client almost as if they were built into Kubernetes itself. `TokenCredentialRequest` can receive a token, pass the token to an external authenticator to authenticate the user, and then return a short-lived mTLS client certificate keypair which can be used to gain access to the Kuberetes API as that user. It is in . `WhoAmI` will return basic details about the currently authenticated user. It is in . The Concierge may also run an impersonation proxy service. This is not an aggregated API endpoint, so it needs to be exposed outside the cluster as a Service. When operating this mode, a client's kubeconfig causes the client to make all Kubernetes API requests to the impersonation proxy endpoint instead of the real API server. The impersonation proxy then authenticates the user and calls the real Kubernetes API on their

Calls made to the real API server are made as a service account using impersonation to impersonate the identity of the end user. The code tries to reuse as much code from Kubernetes itself as possible, so it can behave as closely as possible to the real API server from the client's point of view. It can be found in . Further discussion of this feature can be found in the . The Supervisor's endpoints are: A global `/healthz` which always returns 200 OK And a number of endpoints for each FederationDomain that is configured by the user. Starting in release v0.20.0, the Supervisor has aggregated API endpoints, which makes them appear to a client almost as if they were built into Kubernetes itself. Each FederationDomain's endpoints are mounted under the path of the FederationDomain's `spec.issuer`, if the `spec.issuer` URL has a path component specified. If the issuer has no path, then they are mounted under `/`. These per-FederationDomain endpoint are all mounted by the code in . The per-FederationDomain endpoints are: `<issuer_path>/.well-known/openid-configuration` is the standard OIDC discovery endpoint, which can be used to discover all the other endpoints listed here. See . `<issuer_path>/jwks.json` is the standard OIDC JWKS discovery endpoint. See . `<issuer_path>/oauth2/authorize` is the standard OIDC authorize endpoint. See . `<issuer_path>/oauth2/token` is the standard OIDC token endpoint. See . The token endpoint can handle the standard OIDC `authorizationcode` and `refreshtoken` grant types, and has also been extended in to handle an additional grant type for token exchanges to reduce the applicable scope (technically, the `aud` claim) of ID tokens. `<issuer_path>/callback` is a special endpoint that is used as the redirect URL when performing an OIDC authcode flow against an upstream OIDC identity provider as configured by an OIDCIdentityProvider custom resource. See . `<issuerpath>/v1alpha1/pinnipedidentity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers. See . `<issuer_path>/login` is a login UI page to support the optional browser-based login flow for LDAP and Active Directory identity providers. See . The OIDC specifications implemented by the Supervisor can be found at . The aggregated API endpoints are: `OIDCClientSecretRequest` may be used to create client secrets for OIDCClients. It is in . The Kubernetes API groups used by the Pinniped CRDs and the Concierge's aggregated API endpoints are configurable at install time. By default, everything is placed in the `*.pinniped.dev` group, for example one of the CRDs is `jwtauthenticators.authentication.concierge.pinniped.dev`. Making this group name configurable is not a common pattern in Kubernetes apps, but it yields several advantages. A discussion of this feature, including its implementation details, can be found in the . Similar to leader election, much of this behavior is implemented in client middleware, and will not be obvious when reading the code. The middleware will automatically replace the API group names as needed on each request/response to/from the Kubernetes API server. The middleware logic can be found in .