generated from kubernetes/kubernetes-template-project
-
Notifications
You must be signed in to change notification settings - Fork 14
Add Secret Reader plugin with controller example #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
k8s-ci-robot
merged 25 commits into
kubernetes-sigs:main
from
kahirokunn:add-credentials-plugin
Oct 9, 2025
+663
−52
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
9a1d3d9
Add secretreader-plugin
kahirokunn 736e81b
Update controller example with kubeconfig and script
kahirokunn ee37286
Add GitHub Action to validate README controller example
kahirokunn 1938fb0
Update cmd/secretreader-plugin/README.md
kahirokunn f7d579f
Update cmd/secretreader-plugin/README.md
kahirokunn d579b87
secretreader: Fix indentation in core.go file
kahirokunn 14b72bb
secretreader: Remove unnecessary TrimSpace call on namespace
kahirokunn 18410aa
secretreader: make Secret.data 'token' key a const
kahirokunn 2575954
secretreader: replace dynamic client with typed clients (kubernetes, …
kahirokunn 47074d2
secretreader: handle stdin/stdout in library, switch Provider to GetT…
kahirokunn cd10336
Simplify examples/controller-example/down.sh
kahirokunn 0e63f30
secretreader: Bump k8s.io/client-go clientauthentication to v1
kahirokunn a166677
secretreader: Use client-go authexec.LoadExecCredentialFromEnv instea…
kahirokunn 6417581
secretreader: Refactor secretreader into single file
kahirokunn 82c0ed2
credentials: propagate exec extension to ExecCredential.Cluster.Config
kahirokunn aaf033f
secretreader: use per-cluster exec extension for token selection
kahirokunn 9381b97
Unify shell interpreter to /bin/bash
kahirokunn e38d37c
secretreader: Rename clusterProfile.name to clusterName
kahirokunn 18b707d
secretreader: Remove unused ClusterInventoryAPI client
kahirokunn 787f991
secretreader: Remove redundant namespace determination logic
kahirokunn 4887a7f
secretreader: Use early returns in GetToken for cluster config valida…
kahirokunn 3045ae2
credentials: align exec plugin extension handling with client-go/clie…
kahirokunn 7fabd65
credentials: populate ExecProvider.Config via scheme-based v1→interna…
kahirokunn 2853612
Add clusterExtensionKey with reference link
kahirokunn aace2af
Update README for ClusterProfile extensions usage
kahirokunn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| name: Validate controller example in README | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: readme-controller-example-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| validate-controller-example: | ||
| name: Run README controller example | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Go | ||
| uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod | ||
| cache: false | ||
|
|
||
| - name: Setup kind (install only) | ||
| uses: helm/kind-action@v1 | ||
| with: | ||
| install_only: true | ||
|
|
||
| - name: Tool versions | ||
| run: | | ||
| kind version | ||
| kubectl version --client=true --output=yaml | ||
| go version | ||
|
|
||
| - name: Run setup script (create clusters, secrets, and ClusterProfile) | ||
| run: | | ||
| bash ./examples/controller-example/setup-kind-demo.sh | ||
|
|
||
| - name: Build Secret Reader plugin | ||
| run: | | ||
| go build -o ./bin/secretreader-plugin ./cmd/secretreader-plugin | ||
|
|
||
| - name: Build controller example | ||
| run: | | ||
| go build -o ./examples/controller-example/controller-example.bin ./examples/controller-example | ||
|
|
||
| - name: Execute controller example | ||
| env: | ||
| KUBECONFIG: ./examples/controller-example/hub.kubeconfig | ||
| run: | | ||
| ./examples/controller-example/controller-example.bin \ | ||
| -clusterprofile-provider-file ./examples/controller-example/cp-creds.json \ | ||
| -namespace default \ | ||
| -clusterprofile spoke-1 | ||
|
|
||
| - name: Cleanup kind clusters | ||
| if: always() | ||
| run: | | ||
| bash ./examples/controller-example/down.sh | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,3 +26,5 @@ go.work | |
| *.swp | ||
| *.swo | ||
| *~ | ||
| *.kubeconfig | ||
| *.bin | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # Secret Reader plugin | ||
|
|
||
| When executed by a controller, this plugin reads the `token` from the Kubernetes Secret `<CONSUMER_NAMESPACE>/<CLUSTER_PROFILE_NAME>` and writes an ExecCredential (JSON) to stdout. | ||
|
|
||
| The specification follows the Secret Reader plugin KEP. | ||
|
|
||
| ## Required RBAC | ||
|
|
||
| ```yaml | ||
| apiVersion: rbac.authorization.k8s.io/v1 | ||
| kind: Role | ||
| metadata: | ||
| name: secretreader | ||
| namespace: <CONSUMER_NAMESPACE> | ||
| rules: | ||
| - apiGroups: [""] | ||
| resources: ["secrets"] | ||
| verbs: ["get"] | ||
| --- | ||
| apiVersion: rbac.authorization.k8s.io/v1 | ||
| kind: RoleBinding | ||
| metadata: | ||
| name: secretreader | ||
| namespace: <CONSUMER_NAMESPACE> | ||
| subjects: | ||
| - kind: ServiceAccount | ||
| name: <CONSUMER_SERVICE_ACCOUNT_NAME> | ||
| namespace: <CONSUMER_NAMESPACE> | ||
| roleRef: | ||
| apiGroup: rbac.authorization.k8s.io | ||
| kind: Role | ||
| name: secretreader | ||
| ``` | ||
|
|
||
| ## Build | ||
|
|
||
| ```bash | ||
| go build -o ./bin/secretreader-plugin ./cmd/secretreader-plugin | ||
| ``` | ||
|
|
||
| ## Usage in a controller | ||
|
|
||
| Use the following provider config to exec the secret-reader plugin. | ||
|
|
||
| ```jsonc | ||
| { | ||
| "providers": [ | ||
| { | ||
| "name": "secretreader", | ||
| "execConfig": { | ||
| "apiVersion": "client.authentication.k8s.io/v1", | ||
| "command": "./bin/secretreader-plugin", | ||
| "provideClusterInfo": true | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### Note: `ClusterProfile.status.credentialProviders[].cluster.extensions` | ||
|
|
||
| - Required: set `extensions[].name` to `client.authentication.k8s.io/exec`. | ||
| - The library reads only the `extension` field of that entry and passes it through to `ExecCredential.Spec.Cluster.Config`. | ||
| - The `secretreader` plugin uses `clusterName` inside that Config. | ||
|
|
||
| Example: | ||
|
|
||
| ```yaml | ||
| status: | ||
| credentialProviders: | ||
| - name: secretreader | ||
| cluster: | ||
| server: https://<spoke-server> | ||
| certificate-authority-data: <BASE64_CA> | ||
| extensions: | ||
| - name: client.authentication.k8s.io/exec | ||
| extension: | ||
| clusterName: spoke-1 | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
|
|
||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| kubernetes "k8s.io/client-go/kubernetes" | ||
| clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" | ||
| "k8s.io/client-go/rest" | ||
| "k8s.io/client-go/tools/clientcmd" | ||
| "sigs.k8s.io/cluster-inventory-api/pkg/credentialplugin" | ||
| ) | ||
|
|
||
| type Provider struct { | ||
| // KubeClient is the typed client for core Kubernetes resources (e.g. Secret). | ||
| KubeClient kubernetes.Interface | ||
| // Namespace, if set, overrides namespace inference. | ||
| Namespace string | ||
| } | ||
|
|
||
| // NewDefault constructs a Provider with pre-initialized typed clientsets and an inferred namespace. | ||
| func NewDefault() (*Provider, error) { | ||
| // Build Kubernetes rest.Config via in-cluster first, then fallback to kubeconfig | ||
| cfg, err := rest.InClusterConfig() | ||
| if err != nil { | ||
| kubeconfig := os.Getenv("KUBECONFIG") | ||
| cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to build kube client config: %w", err) | ||
| } | ||
| } | ||
|
|
||
| kubeClient, err := kubernetes.NewForConfig(cfg) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create kubernetes clientset: %w", err) | ||
| } | ||
|
|
||
| return &Provider{KubeClient: kubeClient, Namespace: inferNamespace()}, nil | ||
| } | ||
|
|
||
| // ProviderName is the name of the credential provider. | ||
| const ProviderName = "secretreader" | ||
|
|
||
| // SecretTokenKey is the `Secret.data` key. | ||
| const SecretTokenKey = "token" | ||
|
|
||
| func (Provider) Name() string { return ProviderName } | ||
|
|
||
| func (p Provider) GetToken(ctx context.Context, info clientauthenticationv1.ExecCredential) (clientauthenticationv1.ExecCredentialStatus, error) { | ||
| // Require pre-initialized typed clients | ||
| if p.KubeClient == nil { | ||
| return clientauthenticationv1.ExecCredentialStatus{}, errors.New("provider clients are not initialized; construct with NewDefault or set clients") | ||
| } | ||
|
|
||
| // Require clusterName to be present in extensions config | ||
| type execClusterConfig struct { | ||
| ClusterName string `json:"clusterName"` | ||
| } | ||
| // Validate presence of cluster config | ||
| if info.Spec.Cluster == nil || len(info.Spec.Cluster.Config.Raw) == 0 { | ||
| return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("missing ExecCredential.Spec.Cluster.Config") | ||
| } | ||
| var cfg execClusterConfig | ||
| if err := json.Unmarshal(info.Spec.Cluster.Config.Raw, &cfg); err != nil { | ||
| return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("invalid ExecCredential.Spec.Cluster.Config: %w", err) | ||
| } | ||
| if cfg.ClusterName == "" { | ||
| return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("missing clusterName in ExecCredential.Spec.Cluster.Config") | ||
| } | ||
| clusterName := cfg.ClusterName | ||
|
|
||
| // Read Secret <namespace>/<clusterName> via typed client and return token | ||
| sec, err := p.KubeClient.CoreV1().Secrets(p.Namespace).Get(ctx, clusterName, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("failed to get secret %s/%s: %w", p.Namespace, clusterName, err) | ||
| } | ||
| data, ok := sec.Data[SecretTokenKey] | ||
| if !ok || len(data) == 0 { | ||
| return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("secret %s/%s missing %q key", p.Namespace, clusterName, SecretTokenKey) | ||
| } | ||
|
|
||
| return clientauthenticationv1.ExecCredentialStatus{Token: string(data)}, nil | ||
| } | ||
|
|
||
| // inferNamespace determines the namespace to read Secrets from, preferring kubeconfig current-context | ||
| func inferNamespace() string { | ||
| // kubeconfig current-context namespace | ||
| rules := clientcmd.NewDefaultClientConfigLoadingRules() | ||
| if path := os.Getenv("KUBECONFIG"); strings.TrimSpace(path) != "" { | ||
| rules.ExplicitPath = path | ||
| } | ||
| cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{}) | ||
| if n, _, err := cc.Namespace(); err == nil && strings.TrimSpace(n) != "" { | ||
| return n | ||
| } | ||
| // in-cluster kubeconfig is unavailable; library returns default namespace | ||
| return "default" | ||
| } | ||
|
|
||
| func main() { | ||
| p, err := NewDefault() | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| credentialplugin.Run(*p) | ||
kahirokunn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Controller Example | ||
|
|
||
| This example automatically sets up the following, stores the spoke cluster token in a Secret using the `secretreader` plugin, and lists spoke Pods from the `ClusterProfile`. | ||
|
|
||
| - Create a hub cluster and a spoke cluster with kind | ||
| - On the spoke, create a ServiceAccount and ClusterRole/Binding that can list Pods and issue a token | ||
| - On the hub, create a Secret with the token in `data.token` | ||
| - On the hub, create a `ClusterProfile` with spoke information (set `secretreader` in `status.credentialProviders`) | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - `kind`, `kubectl`, and `go` are available | ||
| - Working directory is the repository root | ||
|
|
||
| ## 1. Run the setup script | ||
|
|
||
| Hub and spoke clusters will be created. | ||
|
|
||
| ```bash | ||
| bash ./examples/controller-example/setup-kind-demo.sh | ||
| ``` | ||
|
|
||
| ## 2. Build the Secret Reader plugin | ||
|
|
||
| ```bash | ||
| go build -o ./bin/secretreader-plugin ./cmd/secretreader-plugin | ||
| ``` | ||
|
|
||
| ## 3. Build the controller | ||
|
|
||
| ```bash | ||
| go build -o ./examples/controller-example/controller-example.bin ./examples/controller-example | ||
| ``` | ||
|
|
||
| ## 4. Run | ||
|
|
||
| ```bash | ||
| KUBECONFIG=./examples/controller-example/hub.kubeconfig ./examples/controller-example/controller-example.bin \ | ||
| -clusterprofile-provider-file ./examples/controller-example/cp-creds.json \ | ||
| -namespace default \ | ||
| -clusterprofile spoke-1 | ||
| ``` | ||
|
|
||
| ## Note: ClusterProfile extensions | ||
|
|
||
| - Required: set `status.credentialProviders[].cluster.extensions[].name` to `client.authentication.k8s.io/exec`. | ||
| - The library reads only the `extension` field of that entry (arbitrary JSON). Other `extensions` entries are ignored. | ||
| - That `extension` is passed through to `ExecCredential.Spec.Cluster.Config`. The `secretreader` plugin uses `clusterName` in that object. | ||
|
|
||
| Example (to be merged into `ClusterProfile.status`): | ||
|
|
||
| ```yaml | ||
| status: | ||
| credentialProviders: | ||
| - name: secretreader | ||
| cluster: | ||
| server: https://<spoke-server> | ||
| certificate-authority-data: <BASE64_CA> | ||
| extensions: | ||
| - name: client.authentication.k8s.io/exec | ||
| extension: | ||
| clusterName: spoke-1 | ||
| ``` | ||
|
|
||
| Note: `client.authentication.k8s.io/exec` is a reserved key in the Kubernetes client authentication API. See the official documentation ("client.authentication.k8s.io"). |
6 changes: 3 additions & 3 deletions
6
pkg/cp-creds.json → examples/controller-example/cp-creds.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/bin/bash | ||
|
|
||
| kind delete cluster --name "hub" || true | ||
| kind delete cluster --name "spoke" || true |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.