Compare commits

..

63 Commits

Author SHA1 Message Date
Matt Moyer
9addb4d6e0 Merge pull request #385 from vmware-tanzu/credential_request_spec_api_group
Use custom suffix in `Spec.Authenticator.APIGroup` of `TokenCredentialRequest`
2021-02-04 16:19:20 -06:00
Ryan Richard
2a921f7090 Merge branch 'main' into credential_request_spec_api_group 2021-02-04 13:44:53 -08:00
Matt Moyer
bb8b65cca6 Merge pull request #387 from vmware-tanzu/blog/multiple-pinnipeds
Add a v0.5.0 "multiple Pinnipeds" blog post.
2021-02-04 15:22:52 -06:00
Matt Moyer
5c331e9002 Fix go.pinniped.dev redirects.
Our meeting notes are now on HackMD, our Zoom link changed, and I added a YouTube link.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-02-04 14:56:50 -06:00
Matt Moyer
1382fc6e5f Add a v0.5.0 "multiple Pinnipeds" blog post. 2021-02-04 14:56:49 -06:00
Andrew Keesler
cc8c917249 Merge pull request #325 from ankeesler/restart-test
Add an integration test helper to assert that no pods restart during the test
2021-02-04 13:07:40 -05:00
Andrew Keesler
ae498f14b4 test/integration: ensure no pods restart during integration tests
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-02-04 10:24:33 -05:00
Ryan Richard
288d9c999e Use custom suffix in Spec.Authenticator.APIGroup of TokenCredentialRequest
When the Pinniped server has been installed with the `api_group_suffix`
option, for example using `mysuffix.com`, then clients who would like to
submit a `TokenCredentialRequest` to the server should set the
`Spec.Authenticator.APIGroup` field as `authentication.concierge.mysuffix.com`.

This makes more sense from the client's point of view than using the
default `authentication.concierge.pinniped.dev` because
`authentication.concierge.mysuffix.com` is the name of the API group
that they can observe their cluster and `authentication.concierge.pinniped.dev`
does not exist as an API group on their cluster.

This commit includes both the client and server-side changes to make
this work, as well as integration test updates.

Co-authored-by: Andrew Keesler <akeesler@vmware.com>
Co-authored-by: Ryan Richard <richardry@vmware.com>
Co-authored-by: Margo Crawford <margaretc@vmware.com>
2021-02-03 15:49:15 -08:00
Andrew Keesler
26922307ad prepare-for-integration-tests.sh: New cmdline option --api_group_suffix
Makes it easy to deploy Pinniped under a different API group for manual
testing and iterating on integration tests on your laptop.

Signed-off-by: Ryan Richard <richardry@vmware.com>
2021-02-03 12:07:38 -08:00
Ryan Richard
5549a262b9 Rename client_test.go to concierge_client_test.go
Because it is a test of the conciergeclient package, and the naming
convention for integration test files is supervisor_*_test.go,
concierge_*_test.go, or cli_*_test.go to identify which component
the test is primarily covering.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-02-03 12:07:38 -08:00
Mo Khan
c5df66fbd5 Merge pull request #383 from enj/enj/i/avoid_scheme_double_register
Avoid double registering types in server scheme
2021-02-03 13:55:33 -05:00
Monis Khan
300d7bd99c Drop duplicate logic for unversioned type registration
Signed-off-by: Monis Khan <mok@vmware.com>
2021-02-03 12:16:57 -05:00
Monis Khan
012bebd66e Avoid double registering types in server scheme
This makes sure that if our clients ever send types with the wrong
group, the server will refuse to decode it.

Signed-off-by: Monis Khan <mok@vmware.com>
2021-02-03 12:16:57 -05:00
Andrew Keesler
e1d06ce4d8 internal/mocks/mockroundtripper: we don't need these anymore
We thought we needed these to test the middleware, but we don't.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-02-03 08:55:38 -05:00
Andrew Keesler
52b98bdb87 Merge pull request #330 from enj/enj/f/better_middleware
Enhance middleware to allow multiple Pinnipeds
2021-02-03 08:53:00 -05:00
Andrew Keesler
62c117421a internal/kubeclient: fix not found test and request body closing bug
- I realized that the hardcoded fakekubeapi 404 not found response was invalid,
  so we were getting a default error message. I fixed it so the tests follow a
  higher fidelity code path.
- I caved and added a test for making sure the request body was always closed,
  and believe it or not, we were double closing a body. I don't *think* this will
  matter in production, since client-go will pass us ioutil.NopReader()'s, but
  at least we know now.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-02-03 08:19:34 -05:00
Monis Khan
efe1fa89fe Allow multiple Pinnipeds to work on same cluster
Yes, this is a huge commit.

The middleware allows you to customize the API groups of all of the
*.pinniped.dev API groups.

Some notes about other small things in this commit:
- We removed the internal/client package in favor of pkg/conciergeclient. The
  two packages do basically the same thing. I don't think we use the former
  anymore.
- We re-enabled cluster-scoped owner assertions in the integration tests.
  This code was added in internal/ownerref. See a0546942 for when this
  assertion was removed.
- Note: the middlware code is in charge of restoring the GV of a request object,
  so we should never need to write mutations that do that.
- We updated the supervisor secret generation to no longer manually set an owner
  reference to the deployment since the middleware code now does this. I think we
  still need some way to make an initial event for the secret generator
  controller, which involves knowing the namespace and the name of the generated
  secret, so I still wired the deployment through. We could use a namespace/name
  tuple here, but I was lazy.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
Co-authored-by: Ryan Richard <richardry@vmware.com>
2021-02-02 15:18:41 -08:00
Andrew Keesler
93d25a349f hack: fix docker most recent tag check
I think this stopped working when we starting using a specific registry in e0b94f47.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-02-02 18:01:07 -05:00
Andrew Keesler
93ebd0f949 internal/plog: add Enabled()
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-02-02 18:01:06 -05:00
Matt Moyer
74a8005f92 Merge pull request #376 from mattmoyer/add-csrftoken-test
Add some trivial unit tests to internal/oidc/csrftoken.
2021-02-02 11:02:39 -06:00
Matt Moyer
5b4e58f0b8 Add some trivial unit tests to internal/oidc/csrftoken.
This change is primarily to test that our test coverage reporting is working as expected.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-02-02 09:38:17 -06:00
Matt Moyer
b871a02ca3 Merge pull request #375 from mattmoyer/test-coverage
Add Codecov configuration file.
2021-02-01 15:19:37 -06:00
Matt Moyer
6a20bbf607 Add Codecov configuration file.
This configures how our coverage reports are processed on https://codecov.io. See https://docs.codecov.io/docs/codecov-yaml for reference.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-02-01 14:28:38 -06:00
Ryan Richard
dfa4d639e6 Merge pull request #374 from microwavables/main
Updated the community meeting info with new zoom link and agenda notes
2021-01-29 14:15:17 -08:00
Nanci Lancaster
8b4024bf82 Updated the community meeting info with new zoom link and agenda notes
Signed-off-by: Nanci Lancaster <nancil@vmware.com>
2021-01-29 16:07:23 -06:00
Ryan Richard
d89c6546e7 Merge pull request #373 from microwavables/main
Updated text on community meetings and added YouTube link
2021-01-28 09:49:12 -08:00
Nanci Lancaster
2710591429 Updated text on community meetings and added YouTube link
Signed-off-by: Nanci Lancaster <nancil@vmware.com>
2021-01-28 11:22:44 -06:00
Matt Moyer
02815cfb26 Revert "Use GitHub's "latest" handling so this doesn't get out of sync."
This reverts commit 46ad41e813.

This turns out not to work, so we have to use a hardcoded version here.
2021-01-28 10:28:46 -06:00
Matt Moyer
3f7cb5d9f8 Merge pull request #372 from mattmoyer/fix-redirects-version
Fix get.pinniped.dev latest version redirects.
2021-01-28 10:26:51 -06:00
Matt Moyer
46ad41e813 Use GitHub's "latest" handling so this doesn't get out of sync.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-28 10:25:33 -06:00
Matt Moyer
d4eca3a82a Fix get.pinniped.dev latest version redirects.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-28 10:23:48 -06:00
Matt Moyer
c03a088399 Merge pull request #370 from mattmoyer/cleanup-docs
Clean up docs using https://get.pinniped.dev redirects.
2021-01-28 10:17:46 -06:00
Matt Moyer
f81dda4eda Add syntax highlighting CSS.
This was generated via `hugo gen chromastyles --style=monokailight > ./site/themes/pinniped/assets/scss/_syntax.css`.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-28 10:15:39 -06:00
Matt Moyer
1ceef5874e Clean up docs using https://get.pinniped.dev redirects.
We have these redirects set up to make the `kubectl apply -f [...]` commands cleaner, but we never went back and fixed up the documentation to use them until now.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-28 10:15:39 -06:00
Matt Moyer
1b224bc4f2 Merge pull request #369 from mattmoyer/cleanup-go-sum
Prune unused versions from go.sum.
2021-01-28 10:09:06 -06:00
Matt Moyer
530d6961c2 Prune unused versions from go.sum.
The broken github.com/oleiade/reflections v1.0.0 package was still causing problems with Dependabot.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-28 09:03:00 -06:00
Matt Moyer
fe500882ef Merge pull request #365 from mattmoyer/upgrade-oleiade-reflections-dep
Upgrade github.com/oleiade/reflections to v1.0.1.
2021-01-27 15:56:49 -06:00
Matt Moyer
8358c26107 Upgrade github.com/oleiade/reflections to v1.0.1.
This project overwrote the v1.0.0 tag with a different commit ID, which has caused issues with the Go module sum DB (which accurately detected the issue).

This has been one of the reasons why Dependabot is not updating our Go dependencies.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-27 13:49:30 -06:00
Matt Moyer
ad9a187522 Merge pull request #335 from mattmoyer/optimize-dockerfile
Optimize image build using .dockerignore and BuildKit features.
2021-01-27 11:35:42 -06:00
Matt Moyer
8a41419b94 Optimize image build using .dockerignore and BuildKit features.
This optimizes our image in a few different ways:

- It adds a bunch of files and directories to the `.dockerignore` file.
  This lets us have a single `COPY . .` but still be very aggressive about pruning what files end up in the build context.

- It adds build-time cache mounts to the `go build` commands using BuildKit's `--mount=type=cache` flag.
  This requires BuildKit-capable Docker, but means that our Go builds can all be incremental builds.
  This replaces the previous flow we had where we needed to split out `go mod download`.

- Instead of letting the full `apt-get install ca-certificates` layer end up in our final image, we copy just the single file we need.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-27 10:42:56 -06:00
Ryan Richard
6ef7ec21cd Merge branch 'release-0.4' into main 2021-01-25 15:13:14 -08:00
Ryan Richard
df1d15ebd1 Merge pull request from GHSA-wp53-6256-whf9
This is a fake PR for testing - please ignore
2021-01-22 12:46:53 -08:00
Ryan Richard
b3732e8b6c Trivial change to a comment 2021-01-22 12:43:35 -08:00
Matt Moyer
7e887666ce Merge pull request #349 from microwavables/main
Add Google Group for meetings
2021-01-21 15:15:01 -06:00
Nanci Lancaster
d6e6f51ced Add Google Group for meetings
Signed-off-by: Nanci Lancaster <nancil@vmware.com>
2021-01-21 14:57:14 -06:00
Matt Moyer
9e21de9c47 Merge pull request #347 from mattmoyer/upgrade-go-oidc-library
Upgrade to github.com/coreos/go-oidc v3.0.0.
2021-01-21 14:39:22 -06:00
Matt Moyer
04c4cd9534 Upgrade to github.com/coreos/go-oidc v3.0.0.
See https://github.com/coreos/go-oidc/releases/tag/v3.0.0 for release notes.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2021-01-21 12:08:14 -06:00
Matt Moyer
5821faec03 Merge pull request #342 from vmware-tanzu/pre-commit-fix
Remove pre-commit hooks file to de-duplicate from pre-commit-config
2021-01-21 12:02:11 -06:00
Matt Moyer
8bca244d59 Merge pull request #345 from vmware-tanzu/dependabot/docker/golang-1.15.7
Bump golang from 1.15.6 to 1.15.7
2021-01-21 11:31:06 -06:00
dependabot[bot]
79fa96cfbc Bump golang from 1.15.6 to 1.15.7
Bumps golang from 1.15.6 to 1.15.7.

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-21 13:56:04 +00:00
Ryan Richard
b5cbe018e3 Allow passing multiple redirect URIs to Dex
We need this in CI when we want to configure Dex with the redirect URI for both
primary and secondary deploys at one time (since we only stand up Dex once).

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-01-20 17:06:50 -05:00
Andrew Keesler
33f4b671d1 Merge pull request #327 from ankeesler/reenable-max-inflight-checks
Restore max in flight check when updating to 0.19.5 #243
2021-01-19 18:29:38 -05:00
Andrew Keesler
50c3e4c00f Merge branch 'main' into reenable-max-inflight-checks 2021-01-19 18:14:27 -05:00
Andrew Keesler
5486427d88 Merge pull request #344 from vmware-tanzu/wire-api-group-suffix
Wire api group suffix through YTT/server components/CLI/integration tests
2021-01-19 18:06:12 -05:00
Andrew Keesler
906bfa023c test: wire API group suffix through to tests
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-01-19 17:23:20 -05:00
Andrew Keesler
1c3518e18a cmd/pinniped: wire API group suffix through to client components
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-01-19 17:23:20 -05:00
Andrew Keesler
88fd9e5c5e internal/config: wire API group suffix through to server components
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-01-19 17:23:20 -05:00
Ryan Richard
616211c1bc deploy: wire API group suffix through YTT templates
I didn't advertise this feature in the deploy README's since (hopefully) not
many people will want to use it?

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-01-19 17:23:06 -05:00
Andrew Keesler
7a9c0e8c69 Merge branch 'main' into reenable-max-inflight-checks 2021-01-19 13:53:00 -05:00
Margo Crawford
c09020102c Remove pre-commit hooks file 2021-01-19 09:43:11 -08:00
Andrew Keesler
af11d8cd58 Run Tilt images as root for faster reload
Previously, when triggering a Tilt reload via a *.go file change, a reload would
take ~13 seconds and we would see this error message in the Tilt logs for each
component.

  Live Update failed with unexpected error:
    command terminated with exit code 2
  Falling back to a full image build + deploy

Now, Tilt should reload images a lot faster (~3 seconds) since we are running
the images as root.

Note! Reloading the Concierge component still takes ~13 seconds because there
are 2 containers running in the Concierge namespace that use the Concierge
image: the main Concierge app and the kube cert agent pod. Tilt can't live
reload both of these at once, so the reload takes longer and we see this error
message.

  Will not perform Live Update because:
    Error retrieving container info: can only get container info for a single pod; image target image:image/concierge has 2 pods
  Falling back to a full image build + deploy

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2021-01-15 11:34:53 -05:00
Matt Moyer
93ba1b54f2 Merge branch 'main' into reenable-max-inflight-checks 2021-01-15 10:19:17 -06:00
Andrew Keesler
792bb98680 Revert "Temporarily disable max inflight checks for mutating requests"
This reverts commit 4a28d1f800.

This commit was originally made to fix a bug that caused TokenCredentialRequest
to become slow when the server was idle for an extended period of time. This was
to address a Kubernetes issue that was fixed in 1.19.5 and onward. We are now
running with Kubernetes 1.20, so we should be able to pick up this fix.
2021-01-13 11:12:09 -05:00
137 changed files with 5495 additions and 1340 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
./.*
./*.md
./*.yaml
./apis
./deploy
./Dockerfile
./generated/1.1*
./hack/lib/tilt/
./internal/mocks
./LICENSE
./site/
./test
**/*_test.go

13
.github/codecov.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
codecov:
strict_yaml_branch: main
require_ci_to_pass: no
notify:
wait_for_ci: no
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true

View File

@@ -1,4 +0,0 @@
- id: validate-copyright-year
name: Validate copyright year
entry: hack/check-copyright-year.sh
language: script

View File

@@ -10,22 +10,15 @@ Please see the [Code of Conduct](./CODE_OF_CONDUCT.md).
Learn about the [scope](https://pinniped.dev/docs/scope/) of the project.
## Meeting with the Maintainers
## Community Meetings
The maintainers aspire to hold a video conference every other week with the Pinniped community.
Any community member may request to add topics to the agenda by contacting a [maintainer](MAINTAINERS.md)
in advance, or by attending and raising the topic during time remaining after the agenda is covered.
Typical agenda items include topics regarding the roadmap, feature requests, bug reports, pull requests, etc.
A [public document](https://docs.google.com/document/d/1qYA35wZV-6bxcH5375vOnIGkNBo7e4OROgsV4Sj8WjQ)
tracks the agendas and notes for these meetings.
Pinniped is better because of our contributors and maintainers. It is because of you that we can bring great software to the community. Please join us during our online community meetings, occuring every first and third Thursday of the month at 9AM PT / 12PM PT. Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09) to attend and add any agenda items you wish to discuss to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view). Join our [Google Group](https://groups.google.com/u/1/g/project-pinniped) to receive invites to this meeting.
These meetings are currently scheduled for the first and third Thursday mornings of each month
at 9 AM Pacific Time, using this [Zoom meeting](https://VMware.zoom.us/j/94638309756?pwd=V3NvRXJIdDg5QVc0TUdFM2dYRzgrUT09).
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
## Discussion
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page.
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page or reach out in Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
## Issues

View File

@@ -1,36 +1,41 @@
# syntax = docker/dockerfile:1.0-experimental
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
FROM golang:1.15.12 as build-env
FROM golang:1.15.7 as build-env
WORKDIR /work
# Get dependencies first so they can be cached as a layer
COPY go.* ./
COPY generated/1.20/apis/go.* ./generated/1.20/apis/
COPY generated/1.20/client/go.* ./generated/1.20/client/
RUN go mod download
# Copy only the production source code to avoid cache misses when editing other files
COPY generated ./generated
COPY cmd ./cmd
COPY pkg ./pkg
COPY internal ./internal
COPY hack ./hack
COPY . .
ARG GOPROXY
# Build the executable binary (CGO_ENABLED=0 means static linking)
RUN mkdir out \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-concierge/... \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-supervisor/... \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out ./cmd/local-user-authenticator/...
# Pass in GOCACHE (build cache) and GOMODCACHE (module cache) so they
# can be re-used between image builds.
RUN \
--mount=type=cache,target=/cache/gocache \
--mount=type=cache,target=/cache/gomodcache \
mkdir out && \
GOCACHE=/cache/gocache \
GOMODCACHE=/cache/gomodcache \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
go build -v -ldflags "$(hack/get-ldflags.sh)" -o out \
./cmd/pinniped-concierge/... \
./cmd/pinniped-supervisor/... \
./cmd/local-user-authenticator/...
# Use a runtime image based on Debian slim
FROM debian:10.9-slim
RUN apt-get update && apt-get install -y ca-certificates procps && rm -rf /var/lib/apt/lists/*
# Use a Debian slim image to grab a reasonable default CA bundle.
FROM debian:10.7-slim AS get-ca-bundle-env
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* /var/cache/debconf/*
# Copy the binaries from the build-env stage
COPY --from=build-env /work/out/pinniped-concierge /usr/local/bin/pinniped-concierge
COPY --from=build-env /work/out/pinniped-supervisor /usr/local/bin/pinniped-supervisor
COPY --from=build-env /work/out/local-user-authenticator /usr/local/bin/local-user-authenticator
# Use a runtime image based on Debian slim.
FROM debian:10.7-slim
COPY --from=get-ca-bundle-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# Copy the binaries from the build-env stage.
COPY --from=build-env /work/out/ /usr/local/bin/
# Document the ports
EXPOSE 8080 8443

View File

@@ -47,9 +47,15 @@ To learn more, see [architecture](https://pinniped.dev/docs/architecture/).
Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/demo/).
## Community Meetings
Pinniped is better because of our contributors and maintainers. It is because of you that we can bring great software to the community. Please join us during our online community meetings, occuring every first and third Thursday of the month at 9AM PT / 12PM PT. Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09) to attend and add any agenda items you wish to discuss to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view). Join our [Google Group](https://groups.google.com/u/1/g/project-pinniped) to receive invites to this meeting.
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
## Discussion
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page.
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page or reach out in Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
## Contributions

View File

@@ -37,6 +37,7 @@ import (
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/deploymentref"
"go.pinniped.dev/internal/downward"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/oidc/provider"
@@ -258,13 +259,15 @@ func run(podInfo *downward.PodInfo, cfg *supervisor.Config) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// TODO remove code that relies on supervisorDeployment directly
dref, supervisorDeployment, err := deploymentref.New(podInfo)
if err != nil {
return fmt.Errorf("cannot create deployment ref: %w", err)
}
client, err := kubeclient.New(dref)
client, err := kubeclient.New(
dref,
kubeclient.WithMiddleware(groupsuffix.New(*cfg.APIGroupSuffix)),
)
if err != nil {
return fmt.Errorf("cannot create k8s client: %w", err)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -51,6 +51,7 @@ func legacyGetKubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
namespace string
authenticatorType string
authenticatorName string
apiGroupSuffix string
)
cmd.Flags().StringVar(&token, "token", "", "Credential to include in the resulting kubeconfig output (Required)")
@@ -59,6 +60,8 @@ func legacyGetKubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
cmd.Flags().StringVar(&namespace, "pinniped-namespace", "pinniped-concierge", "Namespace in which Pinniped was installed")
cmd.Flags().StringVar(&authenticatorType, "authenticator-type", "", "Authenticator type (e.g., 'webhook', 'jwt')")
cmd.Flags().StringVar(&authenticatorName, "authenticator-name", "", "Authenticator name")
cmd.Flags().StringVar(&apiGroupSuffix, "api-group-suffix", "pinniped.dev", "Concierge API group suffix")
mustMarkRequired(cmd, "token")
plog.RemoveKlogGlobalFlags()
cmd.RunE = func(cmd *cobra.Command, args []string) error {
@@ -70,6 +73,7 @@ func legacyGetKubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
namespace: namespace,
authenticatorName: authenticatorName,
authenticatorType: authenticatorType,
apiGroupSuffix: apiGroupSuffix,
},
})
}

View File

@@ -15,7 +15,7 @@ import (
"strings"
"time"
"github.com/coreos/go-oidc"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
@@ -26,23 +26,27 @@ import (
conciergev1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1"
conciergeclientset "go.pinniped.dev/generated/1.20/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/kubeclient"
)
type kubeconfigDeps struct {
getPathToSelf func() (string, error)
getClientset func(clientcmd.ClientConfig) (conciergeclientset.Interface, error)
getClientset func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error)
}
func kubeconfigRealDeps() kubeconfigDeps {
return kubeconfigDeps{
getPathToSelf: os.Executable,
getClientset: func(clientConfig clientcmd.ClientConfig) (conciergeclientset.Interface, error) {
getClientset: func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) {
restConfig, err := clientConfig.ClientConfig()
if err != nil {
return nil, err
}
client, err := kubeclient.New(kubeclient.WithConfig(restConfig))
client, err := kubeclient.New(
kubeclient.WithConfig(restConfig),
kubeclient.WithMiddleware(groupsuffix.New(apiGroupSuffix)),
)
if err != nil {
return nil, err
}
@@ -73,6 +77,7 @@ type getKubeconfigConciergeParams struct {
namespace string
authenticatorName string
authenticatorType string
apiGroupSuffix string
}
type getKubeconfigParams struct {
@@ -103,6 +108,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
f.StringVar(&flags.concierge.namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
f.StringVar(&flags.concierge.authenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)")
f.StringVar(&flags.concierge.authenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name (default: autodiscover)")
f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix")
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)")
@@ -124,6 +130,11 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
//nolint:funlen
func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
// Validate api group suffix and immediately return an error if it is invalid.
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
return fmt.Errorf("invalid api group suffix: %w", err)
}
execConfig := clientcmdapi.ExecConfig{
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
Args: []string{},
@@ -151,7 +162,7 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
if err != nil {
return fmt.Errorf("could not load --kubeconfig/--kubeconfig-context: %w", err)
}
clientset, err := deps.getClientset(clientConfig)
clientset, err := deps.getClientset(clientConfig, flags.concierge.apiGroupSuffix)
if err != nil {
return fmt.Errorf("could not configure Kubernetes client: %w", err)
}
@@ -258,6 +269,7 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams,
// Append the flags to configure the Concierge credential exchange at runtime.
execConfig.Args = append(execConfig.Args,
"--enable-concierge",
"--concierge-api-group-suffix="+flags.concierge.apiGroupSuffix,
"--concierge-namespace="+flags.concierge.namespace,
"--concierge-authenticator-name="+flags.concierge.authenticatorName,
"--concierge-authenticator-type="+flags.concierge.authenticatorType,

View File

@@ -46,6 +46,7 @@ func TestGetKubeconfig(t *testing.T) {
wantStdout string
wantStderr string
wantOptionsCount int
wantAPIGroupSuffix string
}{
{
name: "help flag passed",
@@ -57,6 +58,7 @@ func TestGetKubeconfig(t *testing.T) {
kubeconfig [flags]
Flags:
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
--concierge-authenticator-name string Concierge authenticator name (default: autodiscover)
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)
--concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
@@ -279,6 +281,17 @@ func TestGetKubeconfig(t *testing.T) {
Error: only one of --static-token and --static-token-env can be specified
`),
},
{
name: "invalid api group suffix",
args: []string{
"--concierge-api-group-suffix", ".starts.with.dot",
},
wantError: true,
wantStderr: here.Doc(`
Error: invalid api group suffix: 1 error(s):
- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
`),
},
{
name: "valid static token",
args: []string{
@@ -313,6 +326,7 @@ func TestGetKubeconfig(t *testing.T) {
- login
- static
- --enable-concierge
- --concierge-api-group-suffix=pinniped.dev
- --concierge-namespace=test-namespace
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=webhook
@@ -358,6 +372,7 @@ func TestGetKubeconfig(t *testing.T) {
- login
- static
- --enable-concierge
- --concierge-api-group-suffix=pinniped.dev
- --concierge-namespace=test-namespace
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=webhook
@@ -410,6 +425,7 @@ func TestGetKubeconfig(t *testing.T) {
- login
- oidc
- --enable-concierge
- --concierge-api-group-suffix=pinniped.dev
- --concierge-namespace=pinniped-concierge
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=jwt
@@ -429,6 +445,7 @@ func TestGetKubeconfig(t *testing.T) {
name: "autodetect nothing, set a bunch of options",
args: []string{
"--kubeconfig", "./testdata/kubeconfig.yaml",
"--concierge-api-group-suffix", "tuna.io",
"--concierge-authenticator-type", "webhook",
"--concierge-authenticator-name", "test-authenticator",
"--oidc-issuer", "https://example.com/issuer",
@@ -468,6 +485,7 @@ func TestGetKubeconfig(t *testing.T) {
- login
- oidc
- --enable-concierge
- --concierge-api-group-suffix=tuna.io
- --concierge-namespace=pinniped-concierge
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=webhook
@@ -486,6 +504,7 @@ func TestGetKubeconfig(t *testing.T) {
env: []
provideClusterInfo: true
`, base64.StdEncoding.EncodeToString(testCA.Bundle())),
wantAPIGroupSuffix: "tuna.io",
},
}
for _, tt := range tests {
@@ -498,7 +517,12 @@ func TestGetKubeconfig(t *testing.T) {
}
return ".../path/to/pinniped", nil
},
getClientset: func(clientConfig clientcmd.ClientConfig) (conciergeclientset.Interface, error) {
getClientset: func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) {
if tt.wantAPIGroupSuffix == "" {
require.Equal(t, "pinniped.dev", apiGroupSuffix) // "pinniped.dev" = api group suffix default
} else {
require.Equal(t, tt.wantAPIGroupSuffix, apiGroupSuffix)
}
if tt.getClientsetErr != nil {
return nil, tt.getClientsetErr
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -16,7 +16,7 @@ import (
"path/filepath"
"time"
"github.com/coreos/go-oidc"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
@@ -64,6 +64,7 @@ type oidcLoginFlags struct {
conciergeAuthenticatorName string
conciergeEndpoint string
conciergeCABundle string
conciergeAPIGroupSuffix string
}
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
@@ -92,6 +93,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix")
mustMarkHidden(&cmd, "debug-session-cache")
mustMarkRequired(&cmd, "issuer")
@@ -135,6 +137,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
)
if err != nil {
return fmt.Errorf("invalid concierge parameters: %w", err)

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -60,6 +60,7 @@ func TestLoginOIDCCommand(t *testing.T) {
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
--ca-bundle-data strings Base64 endcoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
--client-id string OpenID Connect client ID (default "pinniped-cli")
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
--concierge-authenticator-name string Concierge authenticator name
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
--concierge-ca-bundle-data string CA bundle to use when connecting to the concierge
@@ -119,6 +120,22 @@ func TestLoginOIDCCommand(t *testing.T) {
Error: could not read --ca-bundle-data: illegal base64 data at input byte 7
`),
},
{
name: "invalid api group suffix",
args: []string{
"--issuer", "test-issuer",
"--enable-concierge",
"--concierge-api-group-suffix", ".starts.with.dot",
"--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", "test-authenticator",
"--concierge-endpoint", "https://127.0.0.1:1234/",
},
wantError: true,
wantStderr: here.Doc(`
Error: invalid concierge parameters: invalid api group suffix: 1 error(s):
- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
`),
},
{
name: "login error",
args: []string{
@@ -175,6 +192,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--concierge-authenticator-name", "test-authenticator",
"--concierge-endpoint", "https://127.0.0.1:1234/",
"--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
"--concierge-api-group-suffix", "some.suffix.com",
},
wantOptionsCount: 7,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n",

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -46,6 +46,7 @@ type staticLoginParams struct {
conciergeAuthenticatorName string
conciergeEndpoint string
conciergeCABundle string
conciergeAPIGroupSuffix string
}
func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
@@ -66,6 +67,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix")
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
return &cmd
}
@@ -83,6 +85,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
)
if err != nil {
return fmt.Errorf("invalid concierge parameters: %w", err)

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -51,6 +51,7 @@ func TestLoginStaticCommand(t *testing.T) {
static [--token TOKEN] [--token-env TOKEN_NAME] [flags]
Flags:
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
--concierge-authenticator-name string Concierge authenticator name
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
--concierge-ca-bundle-data string CA bundle to use when connecting to the concierge
@@ -129,6 +130,22 @@ func TestLoginStaticCommand(t *testing.T) {
Error: could not complete concierge credential exchange: some concierge error
`),
},
{
name: "invalid api group suffix",
args: []string{
"--token", "test-token",
"--enable-concierge",
"--concierge-api-group-suffix", ".starts.with.dot",
"--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", "test-authenticator",
"--concierge-endpoint", "https://127.0.0.1:1234/",
},
wantError: true,
wantStderr: here.Doc(`
Error: invalid concierge parameters: invalid api group suffix: 1 error(s):
- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
`),
},
{
name: "static token success",
args: []string{

View File

@@ -10,17 +10,17 @@ for details.
## Installing the Latest Version with Default Options
```bash
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/$(curl https://api.github.com/repos/vmware-tanzu/pinniped/releases/latest -s | jq .name -r)/install-pinniped-concierge.yaml
kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml
```
## Installing an Older Version with Default Options
## Installing a Specific Version with Default Options
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
and use it to replace the version number in the URL below.
```bash
# Replace v0.2.0 with your preferred version in the URL below
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.2.0/install-pinniped-concierge.yaml
# Replace v0.4.1 with your preferred version in the URL below
kubectl apply -f https://get.pinniped.dev/v0.4.1/install-pinniped-concierge.yaml
```
## Installing with Custom Options

View File

@@ -3,7 +3,7 @@
#@ load("@ytt:data", "data")
#@ load("@ytt:json", "json")
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "getAndValidateLogLevel")
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "getAndValidateLogLevel", "pinnipedDevAPIGroupWithPrefix")
#@ if not data.values.into_namespace:
---
@@ -37,6 +37,7 @@ data:
servingCertificate:
durationSeconds: (@= str(data.values.api_serving_certificate_duration_seconds) @)
renewBeforeSeconds: (@= str(data.values.api_serving_certificate_renew_before_seconds) @)
apiGroupSuffix: (@= data.values.api_group_suffix @)
names:
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)
@@ -90,8 +91,8 @@ spec:
scheduler.alpha.kubernetes.io/critical-pod: ""
spec:
securityContext:
runAsUser: 1001
runAsGroup: 1001
runAsUser: #@ data.values.run_as_user
runAsGroup: #@ data.values.run_as_group
serviceAccountName: #@ defaultResourceName()
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
imagePullSecrets:
@@ -191,11 +192,11 @@ spec:
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.login.concierge.pinniped.dev
name: #@ pinnipedDevAPIGroupWithPrefix("v1alpha1.login.concierge")
labels: #@ labels()
spec:
version: v1alpha1
group: login.concierge.pinniped.dev
group: #@ pinnipedDevAPIGroupWithPrefix("login.concierge")
groupPriorityMinimum: 2500
versionPriority: 10
#! caBundle: Do not include this key here. Starts out null, will be updated/owned by the golang code.

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -12,6 +12,10 @@
#@ return data.values.app_name + "-" + suffix
#@ end
#@ def pinnipedDevAPIGroupWithPrefix(prefix):
#@ return prefix + "." + data.values.api_group_suffix
#@ end
#@ def namespace():
#@ if data.values.into_namespace:
#@ return data.values.into_namespace

View File

@@ -2,7 +2,7 @@
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "pinnipedDevAPIGroupWithPrefix")
#! Give permission to various cluster-scoped objects
---
@@ -66,7 +66,9 @@ rules:
- apiGroups: [ "" ]
resources: [ pods/exec ]
verbs: [ create ]
- apiGroups: [ config.concierge.pinniped.dev, authentication.concierge.pinniped.dev ]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
resources: [ "*" ]
verbs: [ create, get, list, update, watch ]
- apiGroups: [apps]
@@ -124,7 +126,8 @@ metadata:
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
labels: #@ labels()
rules:
- apiGroups: [ login.concierge.pinniped.dev ]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("login.concierge")
resources: [ tokencredentialrequests ]
verbs: [ create ]
---

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@data/values
@@ -54,3 +54,12 @@ api_serving_certificate_renew_before_seconds: 2160000
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! information), trace (timing information), all (kitchen sink).
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
run_as_user: 1001 #! run_as_user specifies the user ID that will own the local-user-authenticator process
run_as_group: 1001 #! run_as_group specifies the group ID that will own the local-user-authenticator process
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
api_group_suffix: pinniped.dev

View File

@@ -1,23 +1,33 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:overlay", "overlay")
#@ load("helpers.lib.yaml", "labels")
#@ load("helpers.lib.yaml", "labels", "pinnipedDevAPIGroupWithPrefix")
#@ load("@ytt:data", "data")
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"credentialissuers.config.concierge.pinniped.dev"}}), expects=1
---
metadata:
#@overlay/match missing_ok=True
labels: #@ labels()
name: #@ pinnipedDevAPIGroupWithPrefix("credentialissuers.config.concierge")
spec:
group: #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"webhookauthenticators.authentication.concierge.pinniped.dev"}}), expects=1
---
metadata:
#@overlay/match missing_ok=True
labels: #@ labels()
name: #@ pinnipedDevAPIGroupWithPrefix("webhookauthenticators.authentication.concierge")
spec:
group: #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"jwtauthenticators.authentication.concierge.pinniped.dev"}}), expects=1
---
metadata:
#@overlay/match missing_ok=True
labels: #@ labels()
name: #@ pinnipedDevAPIGroupWithPrefix("jwtauthenticators.authentication.concierge")
spec:
group: #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")

View File

@@ -15,17 +15,17 @@ User accounts can be created and edited dynamically using `kubectl` commands (se
## Installing the Latest Version with Default Options
```bash
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/latest/download/install-local-user-authenticator.yaml
kubectl apply -f https://get.pinniped.dev/latest/install-local-user-authenticator.yaml
```
## Installing an Older Version with Default Options
## Installing a Specific Version with Default Options
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
and use it to replace the version number in the URL below.
```bash
# Replace v0.2.0 with your preferred version in the URL below
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.2.0/install-local-user-authenticator.yaml
# Replace v0.4.1 with your preferred version in the URL below
kubectl apply -f https://get.pinniped.dev/v0.4.1/install-local-user-authenticator.yaml
```
## Installing with Custom Options

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -48,8 +48,8 @@ spec:
app: local-user-authenticator
spec:
securityContext:
runAsUser: 1001
runAsGroup: 1001
runAsUser: #@ data.values.run_as_user
runAsGroup: #@ data.values.run_as_group
serviceAccountName: local-user-authenticator
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
imagePullSecrets:

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@data/values
@@ -14,3 +14,6 @@ image_tag: latest
#! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]'
#! Optional.
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
run_as_user: 1001 #! run_as_user specifies the user ID that will own the local-user-authenticator process
run_as_group: 1001 #! run_as_group specifies the group ID that will own the local-user-authenticator process

View File

@@ -8,17 +8,17 @@ It can be deployed when those features are needed.
## Installing the Latest Version with Default Options
```bash
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/latest/download/install-pinniped-supervisor.yaml
kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-supervisor.yaml
```
## Installing an Older Version with Default Options
## Installing a Specific Version with Default Options
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
and use it to replace the version number in the URL below.
```bash
# Replace v0.3.0 with your preferred version in the URL below
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.3.0/install-pinniped-supervisor.yaml
# Replace v0.4.1 with your preferred version in the URL below
kubectl apply -f https://get.pinniped.dev/v0.4.1/install-pinniped-supervisor.yaml
```
## Installing with Custom Options

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -30,6 +30,7 @@ metadata:
data:
#@yaml/text-templated-strings
pinniped.yaml: |
apiGroupSuffix: (@= data.values.api_group_suffix @)
names:
defaultTLSCertificateSecret: (@= defaultResourceNameWithSuffix("default-tls-certificate") @)
labels: (@= json.encode(labels()).rstrip() @)
@@ -64,8 +65,8 @@ spec:
labels: #@ defaultLabel()
spec:
securityContext:
runAsUser: 1001
runAsGroup: 1001
runAsUser: #@ data.values.run_as_user
runAsGroup: #@ data.values.run_as_group
serviceAccountName: #@ defaultResourceName()
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
imagePullSecrets:

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -12,6 +12,10 @@
#@ return data.values.app_name + "-" + suffix
#@ end
#@ def pinnipedDevAPIGroupWithPrefix(prefix):
#@ return prefix + "." + data.values.api_group_suffix
#@ end
#@ def namespace():
#@ if data.values.into_namespace:
#@ return data.values.into_namespace

View File

@@ -1,8 +1,8 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "pinnipedDevAPIGroupWithPrefix")
#! Give permission to various objects within the app's own namespace
---
@@ -16,13 +16,16 @@ rules:
- apiGroups: [""]
resources: [secrets]
verbs: [create, get, list, patch, update, watch, delete]
- apiGroups: [config.supervisor.pinniped.dev]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
resources: [federationdomains]
verbs: [update, get, list, watch]
- apiGroups: [idp.supervisor.pinniped.dev]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [oidcidentityproviders]
verbs: [get, list, watch]
- apiGroups: [idp.supervisor.pinniped.dev]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [oidcidentityproviders/status]
verbs: [get, patch, update]
#! We want to be able to read pods/replicasets/deployment so we can learn who our deployment is to set

View File

@@ -1,4 +1,4 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@data/values
@@ -56,3 +56,12 @@ service_loadbalancer_ip: #! e.g. 1.2.3.4
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! information), trace (timing information), all (kitchen sink).
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
run_as_user: 1001 #! run_as_user specifies the user ID that will own the local-user-authenticator process
run_as_group: 1001 #! run_as_group specifies the group ID that will own the local-user-authenticator process
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
api_group_suffix: pinniped.dev

View File

@@ -1,17 +1,24 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:overlay", "overlay")
#@ load("helpers.lib.yaml", "labels")
#@ load("helpers.lib.yaml", "labels", "pinnipedDevAPIGroupWithPrefix")
#@ load("@ytt:data", "data")
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"federationdomains.config.supervisor.pinniped.dev"}}), expects=1
---
metadata:
#@overlay/match missing_ok=True
labels: #@ labels()
name: #@ pinnipedDevAPIGroupWithPrefix("federationdomains.config.supervisor")
spec:
group: #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcidentityproviders.idp.supervisor.pinniped.dev"}}), expects=1
---
metadata:
#@overlay/match missing_ok=True
labels: #@ labels()
name: #@ pinnipedDevAPIGroupWithPrefix("oidcidentityproviders.idp.supervisor")
spec:
group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")

17
go.mod
View File

@@ -5,7 +5,7 @@ go 1.14
require (
cloud.google.com/go v0.60.0 // indirect
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.0.0
github.com/davecgh/go-spew v1.1.1
github.com/go-logr/logr v0.3.0
github.com/go-logr/stdr v0.2.0
@@ -16,6 +16,7 @@ require (
github.com/gorilla/securecookie v1.1.1
github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/oleiade/reflections v1.0.1 // indirect
github.com/onsi/ginkgo v1.13.0 // indirect
github.com/ory/fosite v0.36.0
github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23
@@ -30,17 +31,19 @@ require (
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
k8s.io/api v0.20.7
k8s.io/apimachinery v0.20.7
k8s.io/apiserver v0.20.7
k8s.io/client-go v0.20.7
k8s.io/component-base v0.20.7
k8s.io/api v0.20.1
k8s.io/apimachinery v0.20.1
k8s.io/apiserver v0.20.1
k8s.io/client-go v0.20.1
k8s.io/component-base v0.20.1
k8s.io/gengo v0.0.0-20201113003025-83324d819ded
k8s.io/klog/v2 v2.4.0
k8s.io/kube-aggregator v0.20.7
k8s.io/kube-aggregator v0.20.1
k8s.io/utils v0.0.0-20201110183641-67b214c5f920
sigs.k8s.io/yaml v1.2.0
)

54
go.sum
View File

@@ -127,9 +127,10 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ=
github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -467,9 +468,8 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/gddo v0.0.0-20180828051604-96d2a289f41e/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
@@ -656,7 +656,6 @@ github.com/karrick/godirwalk v1.15.5/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1q
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -771,8 +770,9 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oleiade/reflections v1.0.0 h1:0ir4pc6v8/PJ0yw5AEtMddfXpWBXg9cnG7SgSoJuCgY=
github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -1020,7 +1020,6 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63M
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0=
@@ -1164,11 +1163,11 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1186,7 +1185,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1350,12 +1348,11 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200721223218-6123e77877b2/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1503,28 +1500,26 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
k8s.io/api v0.20.1 h1:ud1c3W3YNzGd6ABJlbFfKXBKXO+1KdGfcgGGNgFR03E=
k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
k8s.io/api v0.20.7 h1:wOEPJ3NoimUfR9v9sAO2JosPiEP9IGFNplf7zZvYzPU=
k8s.io/api v0.20.7/go.mod h1:4x0yErUkcEWYG+O0S4QdrYa2+PLEeY2M7aeQe++2nmk=
k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ=
k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
k8s.io/apimachinery v0.20.7 h1:tBfhql7OggSCahvASeEpLRzvxc7FK77wNivi1uXCQWM=
k8s.io/apimachinery v0.20.7/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
k8s.io/apiserver v0.20.7 h1:kmj4lX5evfdm8h07jRjuSANvRH0kPlXTq6LOSGT6n/k=
k8s.io/apiserver v0.20.7/go.mod h1:7gbB7UjDdP1/epYBGnIUE6jWY4Wpz99cZ7igfDa9rv4=
k8s.io/apiserver v0.20.1 h1:yEqdkxlnQbxi/3e74cp0X16h140fpvPrNnNRAJBDuBk=
k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
k8s.io/client-go v0.20.1 h1:Qquik0xNFbK9aUG92pxHYsyfea5/RPO9o9bSywNor+M=
k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
k8s.io/client-go v0.20.7 h1:Ot22456XfYAWrCWddw/quevMrFHqP7s1qT499FoumVU=
k8s.io/client-go v0.20.7/go.mod h1:uGl3qh/Jy3cTF1nDoIKBqUZlRWnj/EM+/leAXETKRuA=
k8s.io/code-generator v0.20.7/go.mod h1:i6FmG+QxaLxvJsezvZp0q/gAEzzOz3U53KFibghWToU=
k8s.io/component-base v0.20.7 h1:TdRMMGxxxhcArvkem+FVqBljPOczs9j+tVGpYRM6TM8=
k8s.io/component-base v0.20.7/go.mod h1:878UWprXC07P2CWFg+jjvTfxJSlkHp1v2m1MTkNQnJY=
k8s.io/code-generator v0.20.1/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg=
k8s.io/component-base v0.20.1 h1:6OQaHr205NSl24t5wOF2IhdrlxZTWEZwuGlLvBgaeIg=
k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE=
k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-aggregator v0.20.7 h1:gMTDj5zDAg0IYjo5wSNeMei0KzLvE+xGcMSFlH42DW8=
k8s.io/kube-aggregator v0.20.7/go.mod h1:jItPWEHry5RdBf0MKbeIp/r4nEwkYn4LcuSzO/mg1Yw=
k8s.io/kube-aggregator v0.20.1 h1:IPiL4l4ODmpzfte6LSYXbXuDyuYmTDZ4vQIcLS9NIZ0=
k8s.io/kube-aggregator v0.20.1/go.mod h1:1ZeyRfSg5HcRI8dihvWAc7VpXSMAw9UmZoWXBUOPyew=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw=
@@ -1538,11 +1533,10 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15 h1:4uqm9Mv+w2MmBYD+F4qf/v6tDFUdPOk29C095RbU5mY=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14 h1:TihvEz9MPj2u0KWds6E2OBUXfwaL4qRJ33c7HGiJpqk=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/structured-merge-diff/v4 v4.0.3 h1:4oyYo8NREp49LBBhKxEqCulFjg26rawYKrnCmg+Sr6c=
sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

View File

@@ -8,5 +8,5 @@ set -euo pipefail
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
cd "${ROOT}"
# To choose a specific version of kube, add this option to the command below: `--image kindest/node:v1.18.8`
# To choose a specific version of kube, add this option to the command below: `--image kindest/node:v1.18.8`.
kind create cluster --config "hack/lib/kind-config/single-node.yaml" --name pinniped

View File

@@ -25,7 +25,7 @@ local_resource(
# Render the IDP installation manifest using ytt.
k8s_yaml(local(['ytt',
'--file', '../../../test/deploy/dex',
'--data-value', 'supervisor_redirect_uri=https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/callback',
'--data-value-yaml', 'supervisor_redirect_uris=[https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/callback]',
]))
# Tell tilt to watch all of those files for changes.
watch_file('../../../test/deploy/dex')
@@ -60,6 +60,8 @@ k8s_yaml(local([
'--file', '../../../deploy/local-user-authenticator',
'--data-value', 'image_repo=image/local-user-auth',
'--data-value', 'image_tag=tilt-dev',
'--data-value-yaml', 'run_as_user=0',
'--data-value-yaml', 'run_as_group=0',
]))
# Tell tilt to watch all of those files for changes.
watch_file('../../../deploy/local-user-authenticator')
@@ -108,6 +110,8 @@ k8s_yaml(local([
'--data-value-yaml', 'service_https_nodeport_nodeport=31243',
'--data-value-yaml', 'service_https_clusterip_port=443',
'--data-value-yaml', 'custom_labels={mySupervisorCustomLabelName: mySupervisorCustomLabelValue}',
'--data-value-yaml', 'run_as_user=0',
'--data-value-yaml', 'run_as_group=0',
]))
# Tell tilt to watch all of those files for changes.
watch_file('../../../deploy/supervisor')
@@ -152,7 +156,9 @@ k8s_yaml(local([
'--data-value discovery_url=$(TERM=dumb kubectl cluster-info | awk \'/master|control plane/ {print $NF}\') ' +
'--data-value log_level=debug ' +
'--data-value-yaml replicas=1 ' +
'--data-value-yaml "custom_labels={myConciergeCustomLabelName: myConciergeCustomLabelValue}"'
'--data-value-yaml "custom_labels={myConciergeCustomLabelName: myConciergeCustomLabelValue}" ' +
'--data-value-yaml run_as_user=0 ' +
'--data-value-yaml run_as_group=0',
]))
# Tell tilt to watch all of those files for changes.
watch_file('../../../deploy/concierge')

View File

@@ -51,6 +51,7 @@ function check_dependency() {
help=no
skip_build=no
clean_kind=no
api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file
while (("$#")); do
case "$1" in
@@ -66,6 +67,16 @@ while (("$#")); do
clean_kind=yes
shift
;;
-g | --api-group-suffix)
shift
# If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error
if [[ "$#" == "0" || "$1" == -* ]]; then
log_error "-g|--api-group-suffix requires a group name to be specified"
exit 1
fi
api_group_suffix=$1
shift
;;
-*)
log_error "Unsupported flag $1" >&2
exit 1
@@ -84,6 +95,8 @@ if [[ "$help" == "yes" ]]; then
log_note
log_note "Flags:"
log_note " -h, --help: print this usage"
log_note " -c, --clean: destroy the current kind cluster and make a new one"
log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix"
log_note " -s, --skip-build: reuse the most recently built image of the app instead of building"
exit 1
fi
@@ -136,7 +149,7 @@ if ! tilt_mode; then
tag=$(uuidgen) # always a new tag to force K8s to reload the image on redeploy
if [[ "$skip_build" == "yes" ]]; then
most_recent_tag=$(docker images "$repo" --format "{{.Tag}}" | head -1)
most_recent_tag=$(docker images "$registry/$repo" --format "{{.Tag}}" | head -1)
if [[ -n "$most_recent_tag" ]]; then
tag="$most_recent_tag"
do_build=no
@@ -185,7 +198,7 @@ if ! tilt_mode; then
log_note "Deploying Dex to the cluster..."
ytt --file . >"$manifest"
ytt --file . \
--data-value "supervisor_redirect_uri=https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/callback" \
--data-value-yaml "supervisor_redirect_uris=[https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/callback]" \
>"$manifest"
kubectl apply --dry-run=client -f "$manifest" # Validate manifest schema.
@@ -226,6 +239,7 @@ if ! tilt_mode; then
ytt --file . \
--data-value "app_name=$supervisor_app_name" \
--data-value "namespace=$supervisor_namespace" \
--data-value "api_group_suffix=$api_group_suffix" \
--data-value "image_repo=$registry_repo" \
--data-value "image_tag=$tag" \
--data-value "log_level=debug" \
@@ -259,6 +273,7 @@ if ! tilt_mode; then
ytt --file . \
--data-value "app_name=$concierge_app_name" \
--data-value "namespace=$concierge_namespace" \
--data-value "api_group_suffix=$api_group_suffix" \
--data-value "log_level=debug" \
--data-value-yaml "custom_labels=$concierge_custom_labels" \
--data-value "image_repo=$registry_repo" \
@@ -314,6 +329,7 @@ export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CALLBACK_URL=https://pinniped-supe
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME=pinny@example.com
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD=password
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_EXPECTED_GROUPS= # Dex's local user store does not let us configure groups.
export PINNIPED_TEST_API_GROUP_SUFFIX='${api_group_suffix}'
read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true
${pinniped_cluster_capability_file_content}

View File

@@ -1,93 +0,0 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package client is a wrapper for interacting with Pinniped's CredentialRequest API.
package client
import (
"context"
"errors"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/generated/1.20/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/kubeclient"
)
// ErrLoginFailed is returned by ExchangeToken when the server rejects the login request.
var ErrLoginFailed = errors.New("login failed")
// ExchangeToken exchanges an opaque token using the Pinniped TokenCredentialRequest API, returning a client-go ExecCredential valid on the target cluster.
func ExchangeToken(ctx context.Context, namespace string, authenticator corev1.TypedLocalObjectReference, token string, caBundle string, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
client, err := getClient(apiEndpoint, caBundle)
if err != nil {
return nil, fmt.Errorf("could not get API client: %w", err)
}
resp, err := client.LoginV1alpha1().TokenCredentialRequests(namespace).Create(ctx, &v1alpha1.TokenCredentialRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
},
Spec: v1alpha1.TokenCredentialRequestSpec{
Token: token,
Authenticator: authenticator,
},
}, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("could not login: %w", err)
}
if resp.Status.Credential == nil || resp.Status.Message != nil {
if resp.Status.Message != nil {
return nil, fmt.Errorf("%w: %s", ErrLoginFailed, *resp.Status.Message)
}
return nil, fmt.Errorf("%w: unknown", ErrLoginFailed)
}
return &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
ExpirationTimestamp: &resp.Status.Credential.ExpirationTimestamp,
ClientCertificateData: resp.Status.Credential.ClientCertificateData,
ClientKeyData: resp.Status.Credential.ClientKeyData,
Token: resp.Status.Credential.Token,
},
}, nil
}
// getClient returns an anonymous client for the Pinniped API at the provided endpoint/CA bundle.
func getClient(apiEndpoint string, caBundle string) (versioned.Interface, error) {
cfg, err := clientcmd.NewNonInteractiveClientConfig(clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{
"cluster": {
Server: apiEndpoint,
CertificateAuthorityData: []byte(caBundle),
},
},
Contexts: map[string]*clientcmdapi.Context{
"current": {
Cluster: "cluster",
AuthInfo: "client",
},
},
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"client": {},
},
}, "current", &clientcmd.ConfigOverrides{}, nil).ClientConfig()
if err != nil {
return nil, err
}
client, err := kubeclient.New(kubeclient.WithConfig(cfg))
if err != nil {
return nil, err
}
return client.PinnipedConcierge, nil
}

View File

@@ -1,146 +0,0 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package client
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
auth1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/testutil"
)
func TestExchangeToken(t *testing.T) {
t.Parallel()
ctx := context.Background()
testAuthenticator := corev1.TypedLocalObjectReference{
APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
Kind: "WebhookAuthenticator",
Name: "test-webhook",
}
t.Run("invalid configuration", func(t *testing.T) {
t.Parallel()
got, err := ExchangeToken(ctx, "test-namespace", testAuthenticator, "", "", "")
require.EqualError(t, err, "could not get API client: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
require.Nil(t, got)
})
t.Run("server error", func(t *testing.T) {
t.Parallel()
// Start a test server that returns only 500 errors.
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("some server error"))
})
got, err := ExchangeToken(ctx, "test-namespace", testAuthenticator, "", caBundle, endpoint)
require.EqualError(t, err, `could not login: an error on the server ("some server error") has prevented the request from succeeding (post tokencredentialrequests.login.concierge.pinniped.dev)`)
require.Nil(t, got)
})
t.Run("login failure", func(t *testing.T) {
t.Parallel()
// Start a test server that returns success but with an error message
errorMessage := "some login failure"
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
Status: loginv1alpha1.TokenCredentialRequestStatus{Message: &errorMessage},
})
})
got, err := ExchangeToken(ctx, "test-namespace", testAuthenticator, "", caBundle, endpoint)
require.EqualError(t, err, `login failed: some login failure`)
require.Nil(t, got)
})
t.Run("login failure unknown error", func(t *testing.T) {
t.Parallel()
// Start a test server that returns without any error message but also without valid credentials
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
})
})
got, err := ExchangeToken(ctx, "test-namespace", testAuthenticator, "", caBundle, endpoint)
require.EqualError(t, err, `login failed: unknown`)
require.Nil(t, got)
})
t.Run("success", func(t *testing.T) {
t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second))
// Start a test server that returns successfully and asserts various properties of the request.
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/apis/login.concierge.pinniped.dev/v1alpha1/namespaces/test-namespace/tokencredentialrequests", r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t,
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.pinniped.dev/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.pinniped.dev",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`,
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
got, err := ExchangeToken(ctx, "test-namespace", testAuthenticator, "test-token", caBundle, endpoint)
require.NoError(t, err)
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
ExpirationTimestamp: &expires,
},
}, got)
})
}

View File

@@ -10,43 +10,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/pkg/version"
loginapi "go.pinniped.dev/generated/1.20/apis/concierge/login"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/registry/credentialrequest"
)
var (
//nolint: gochecknoglobals
scheme = runtime.NewScheme()
//nolint: gochecknoglobals, golint
Codecs = serializer.NewCodecFactory(scheme)
)
//nolint: gochecknoinits
func init() {
utilruntime.Must(loginv1alpha1.AddToScheme(scheme))
utilruntime.Must(loginapi.AddToScheme(scheme))
// add the options to empty v1
metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
unversioned := schema.GroupVersion{Group: "", Version: "v1"}
scheme.AddUnversionedTypes(unversioned,
&metav1.Status{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
)
}
type Config struct {
GenericConfig *genericapiserver.RecommendedConfig
ExtraConfig ExtraConfig
@@ -56,6 +27,9 @@ type ExtraConfig struct {
Authenticator credentialrequest.TokenCredentialRequestAuthenticator
Issuer credentialrequest.CertIssuer
StartControllersPostStartHook func(ctx context.Context)
Scheme *runtime.Scheme
NegotiatedSerializer runtime.NegotiatedSerializer
GroupVersion schema.GroupVersion
}
type PinnipedServer struct {
@@ -96,15 +70,15 @@ func (c completedConfig) New() (*PinnipedServer, error) {
GenericAPIServer: genericServer,
}
gvr := loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")
gvr := c.ExtraConfig.GroupVersion.WithResource("tokencredentialrequests")
storage := credentialrequest.NewREST(c.ExtraConfig.Authenticator, c.ExtraConfig.Issuer)
if err := s.GenericAPIServer.InstallAPIGroup(&genericapiserver.APIGroupInfo{
PrioritizedVersions: []schema.GroupVersion{gvr.GroupVersion()},
VersionedResourcesStorageMap: map[string]map[string]rest.Storage{gvr.Version: {gvr.Resource: storage}},
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
Scheme: scheme,
Scheme: c.ExtraConfig.Scheme,
ParameterCodec: metav1.ParameterCodec,
NegotiatedSerializer: Codecs,
NegotiatedSerializer: c.ExtraConfig.NegotiatedSerializer,
}); err != nil {
return nil, fmt.Errorf("could not install API group %s: %w", gvr.String(), err)
}

View File

@@ -11,9 +11,15 @@ import (
"time"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options"
loginapi "go.pinniped.dev/generated/1.20/apis/concierge/login"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/certauthority/dynamiccertauthority"
"go.pinniped.dev/internal/concierge/apiserver"
@@ -22,6 +28,7 @@ import (
"go.pinniped.dev/internal/controllermanager"
"go.pinniped.dev/internal/downward"
"go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/registry/credentialrequest"
@@ -36,10 +43,6 @@ type App struct {
downwardAPIPath string
}
// This is ignored for now because we turn off etcd storage below, but this is
// the right prefix in case we turn it back on.
const defaultEtcdPathPrefix = "/registry/" + loginv1alpha1.GroupName
// New constructs a new App with command line args, stdout and stderr.
func New(ctx context.Context, args []string, stdout, stderr io.Writer) *App {
app := &App{}
@@ -107,7 +110,7 @@ func (a *App) runServer(ctx context.Context) error {
}
// Initialize the cache of active authenticators.
authenticators := authncache.New()
authenticators := authncache.New(*cfg.APIGroupSuffix)
// This cert provider will provide certs to the API server and will
// be mutated by a controller to keep the certs up to date with what
@@ -125,6 +128,7 @@ func (a *App) runServer(ctx context.Context) error {
startControllersFunc, err := controllermanager.PrepareControllers(
&controllermanager.Config{
ServerInstallationInfo: podInfo,
APIGroupSuffix: *cfg.APIGroupSuffix,
NamesConfig: &cfg.NamesConfig,
Labels: cfg.Labels,
KubeCertAgentConfig: &cfg.KubeCertAgentConfig,
@@ -146,6 +150,7 @@ func (a *App) runServer(ctx context.Context) error {
authenticators,
dynamiccertauthority.New(dynamicSigningCertProvider),
startControllersFunc,
*cfg.APIGroupSuffix,
)
if err != nil {
return fmt.Errorf("could not configure aggregated API server: %w", err)
@@ -167,17 +172,31 @@ func getAggregatedAPIServerConfig(
authenticator credentialrequest.TokenCredentialRequestAuthenticator,
issuer credentialrequest.CertIssuer,
startControllersPostStartHook func(context.Context),
apiGroupSuffix string,
) (*apiserver.Config, error) {
apiGroup, ok := groupsuffix.Replace(loginv1alpha1.GroupName, apiGroupSuffix)
if !ok {
return nil, fmt.Errorf("cannot make api group from %s/%s", loginv1alpha1.GroupName, apiGroupSuffix)
}
scheme := getAggregatedAPIServerScheme(apiGroup)
codecs := serializer.NewCodecFactory(scheme)
defaultEtcdPathPrefix := fmt.Sprintf("/registry/%s", apiGroup)
groupVersion := schema.GroupVersion{
Group: apiGroup,
Version: loginv1alpha1.SchemeGroupVersion.Version,
}
recommendedOptions := genericoptions.NewRecommendedOptions(
defaultEtcdPathPrefix,
apiserver.Codecs.LegacyCodec(loginv1alpha1.SchemeGroupVersion),
// TODO we should check to see if all the other default settings are acceptable for us
codecs.LegacyCodec(groupVersion),
)
recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet
recommendedOptions.SecureServing.ServerCert.GeneratedCert = dynamicCertProvider
recommendedOptions.SecureServing.BindPort = 8443 // Don't run on default 443 because that requires root
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
// Note that among other things, this ApplyTo() function copies
// `recommendedOptions.SecureServing.ServerCert.GeneratedCert` into
// `serverConfig.SecureServing.Cert` thus making `dynamicCertProvider`
@@ -191,20 +210,65 @@ func getAggregatedAPIServerConfig(
return nil, err
}
// temporarily disable max inflight checks for mutating requests until we
// pick up a fix for https://github.com/kubernetes/kubernetes/issues/95300
// we do not need to set MaxRequestsInFlight to 0 because we are constantly
// hammered by the kubelet for /healthz and the api server for discovery
// which keeps the non-mutating request watermark histograms up to date
serverConfig.Config.MaxMutatingRequestsInFlight = 0
apiServerConfig := &apiserver.Config{
GenericConfig: serverConfig,
ExtraConfig: apiserver.ExtraConfig{
Authenticator: authenticator,
Issuer: issuer,
StartControllersPostStartHook: startControllersPostStartHook,
Scheme: scheme,
NegotiatedSerializer: codecs,
GroupVersion: groupVersion,
},
}
return apiServerConfig, nil
}
func getAggregatedAPIServerScheme(apiGroup string) *runtime.Scheme {
// standard set up of the server side scheme
scheme := runtime.NewScheme()
// add the options to empty v1
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
// nothing fancy is required if using the standard group
if apiGroup == loginv1alpha1.GroupName {
utilruntime.Must(loginv1alpha1.AddToScheme(scheme))
utilruntime.Must(loginapi.AddToScheme(scheme))
return scheme
}
// we need a temporary place to register our types to avoid double registering them
tmpScheme := runtime.NewScheme()
utilruntime.Must(loginv1alpha1.AddToScheme(tmpScheme))
utilruntime.Must(loginapi.AddToScheme(tmpScheme))
for gvk := range tmpScheme.AllKnownTypes() {
if gvk.GroupVersion() == metav1.Unversioned {
continue // metav1.AddToGroupVersion registers types outside of our aggregated API group that we need to ignore
}
if gvk.Group != loginv1alpha1.GroupName {
panic("tmp scheme has types not in the aggregated API group: " + gvk.Group) // programmer error
}
obj, err := tmpScheme.New(gvk)
if err != nil {
panic(err) // programmer error, scheme internal code is broken
}
newGVK := schema.GroupVersionKind{
Group: apiGroup,
Version: gvk.Version,
Kind: gvk.Kind,
}
// register the existing type but with the new group in the correct scheme
scheme.AddKnownTypeWithName(newGVK, obj)
}
// manually register conversions and defaulting into the correct scheme since we cannot directly call loginv1alpha1.AddToScheme
utilruntime.Must(loginv1alpha1.RegisterConversions(scheme))
utilruntime.Must(loginv1alpha1.RegisterDefaults(scheme))
return scheme
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package server
@@ -6,12 +6,19 @@ package server
import (
"bytes"
"context"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
loginapi "go.pinniped.dev/generated/1.20/apis/concierge/login"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
)
const knownGoodUsage = `
@@ -88,3 +95,129 @@ func TestCommand(t *testing.T) {
})
}
}
func Test_getAggregatedAPIServerScheme(t *testing.T) {
// the standard group
regularGV := schema.GroupVersion{
Group: "login.concierge.pinniped.dev",
Version: "v1alpha1",
}
regularGVInternal := schema.GroupVersion{
Group: "login.concierge.pinniped.dev",
Version: runtime.APIVersionInternal,
}
// the canonical other group
otherGV := schema.GroupVersion{
Group: "login.concierge.walrus.tld",
Version: "v1alpha1",
}
otherGVInternal := schema.GroupVersion{
Group: "login.concierge.walrus.tld",
Version: runtime.APIVersionInternal,
}
// kube's core internal
internalGV := schema.GroupVersion{
Group: "",
Version: runtime.APIVersionInternal,
}
tests := []struct {
name string
apiGroup string
want map[schema.GroupVersionKind]reflect.Type
}{
{
name: "regular api group",
apiGroup: "login.concierge.pinniped.dev",
want: map[schema.GroupVersionKind]reflect.Type{
// all the types that are in the aggregated API group
regularGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
regularGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
regularGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
regularGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
regularGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
regularGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
regularGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
regularGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
regularGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
regularGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
regularGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
regularGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
regularGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
// the types below this line do not really matter to us because they are in the core group
internalGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
metav1.Unversioned.WithKind("APIGroup"): reflect.TypeOf(&metav1.APIGroup{}).Elem(),
metav1.Unversioned.WithKind("APIGroupList"): reflect.TypeOf(&metav1.APIGroupList{}).Elem(),
metav1.Unversioned.WithKind("APIResourceList"): reflect.TypeOf(&metav1.APIResourceList{}).Elem(),
metav1.Unversioned.WithKind("APIVersions"): reflect.TypeOf(&metav1.APIVersions{}).Elem(),
metav1.Unversioned.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
metav1.Unversioned.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
metav1.Unversioned.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
metav1.Unversioned.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
metav1.Unversioned.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
metav1.Unversioned.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
metav1.Unversioned.WithKind("Status"): reflect.TypeOf(&metav1.Status{}).Elem(),
metav1.Unversioned.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
metav1.Unversioned.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
},
},
{
name: "other api group",
apiGroup: "login.concierge.walrus.tld",
want: map[schema.GroupVersionKind]reflect.Type{
// all the types that are in the aggregated API group
otherGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
otherGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
otherGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
otherGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
otherGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
otherGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
otherGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
otherGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
otherGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
otherGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
otherGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
otherGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
otherGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
// the types below this line do not really matter to us because they are in the core group
internalGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
metav1.Unversioned.WithKind("APIGroup"): reflect.TypeOf(&metav1.APIGroup{}).Elem(),
metav1.Unversioned.WithKind("APIGroupList"): reflect.TypeOf(&metav1.APIGroupList{}).Elem(),
metav1.Unversioned.WithKind("APIResourceList"): reflect.TypeOf(&metav1.APIResourceList{}).Elem(),
metav1.Unversioned.WithKind("APIVersions"): reflect.TypeOf(&metav1.APIVersions{}).Elem(),
metav1.Unversioned.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
metav1.Unversioned.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
metav1.Unversioned.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
metav1.Unversioned.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
metav1.Unversioned.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
metav1.Unversioned.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
metav1.Unversioned.WithKind("Status"): reflect.TypeOf(&metav1.Status{}).Elem(),
metav1.Unversioned.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
metav1.Unversioned.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
scheme := getAggregatedAPIServerScheme(tt.apiGroup)
require.Equal(t, tt.want, scheme.AllKnownTypes())
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package concierge contains functionality to load/store Config's from/to
@@ -13,6 +13,7 @@ import (
"sigs.k8s.io/yaml"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/plog"
)
@@ -40,12 +41,17 @@ func FromPath(path string) (*Config, error) {
}
maybeSetAPIDefaults(&config.APIConfig)
maybeSetAPIGroupSuffixDefault(&config.APIGroupSuffix)
maybeSetKubeCertAgentDefaults(&config.KubeCertAgentConfig)
if err := validateAPI(&config.APIConfig); err != nil {
return nil, fmt.Errorf("validate api: %w", err)
}
if err := validateAPIGroupSuffix(*config.APIGroupSuffix); err != nil {
return nil, fmt.Errorf("validate apiGroupSuffix: %w", err)
}
if err := validateNames(&config.NamesConfig); err != nil {
return nil, fmt.Errorf("validate names: %w", err)
}
@@ -71,6 +77,12 @@ func maybeSetAPIDefaults(apiConfig *APIConfigSpec) {
}
}
func maybeSetAPIGroupSuffixDefault(apiGroupSuffix **string) {
if *apiGroupSuffix == nil {
*apiGroupSuffix = stringPtr("pinniped.dev")
}
}
func maybeSetKubeCertAgentDefaults(cfg *KubeCertAgentSpec) {
if cfg.NamePrefix == nil {
cfg.NamePrefix = stringPtr("pinniped-kube-cert-agent-")
@@ -114,6 +126,10 @@ func validateAPI(apiConfig *APIConfigSpec) error {
return nil
}
func validateAPIGroupSuffix(apiGroupSuffix string) error {
return groupsuffix.Validate(apiGroupSuffix)
}
func int64Ptr(i int64) *int64 {
return &i
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package concierge
@@ -30,6 +30,7 @@ func TestFromPath(t *testing.T) {
servingCertificate:
durationSeconds: 3600
renewBeforeSeconds: 2400
apiGroupSuffix: some.suffix.com
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
@@ -53,6 +54,7 @@ func TestFromPath(t *testing.T) {
RenewBeforeSeconds: int64Ptr(2400),
},
},
APIGroupSuffix: stringPtr("some.suffix.com"),
NamesConfig: NamesConfigSpec{
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
CredentialIssuer: "pinniped-config",
@@ -82,6 +84,7 @@ func TestFromPath(t *testing.T) {
DiscoveryInfo: DiscoveryInfoSpec{
URL: nil,
},
APIGroupSuffix: stringPtr("pinniped.dev"),
APIConfig: APIConfigSpec{
ServingCertificateConfig: ServingCertificateConfigSpec{
DurationSeconds: int64Ptr(60 * 60 * 24 * 365), // about a year
@@ -172,7 +175,7 @@ func TestFromPath(t *testing.T) {
api:
servingCertificate:
durationSeconds: 2400
renewBeforeSeconds: -10
renewBeforeSeconds: 0
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
@@ -180,6 +183,22 @@ func TestFromPath(t *testing.T) {
`),
wantError: "validate api: renewBefore must be positive",
},
{
name: "InvalidAPIGroupSuffix",
yaml: here.Doc(`
---
api:
servingCertificate:
durationSeconds: 3600
renewBeforeSeconds: 2400
apiGroupSuffix: .starts.with.dot
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
apiService: pinniped-api
`),
wantError: "validate apiGroupSuffix: 1 error(s):\n- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
},
}
for _, test := range tests {
test := test

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package concierge
@@ -9,6 +9,7 @@ import "go.pinniped.dev/internal/plog"
type Config struct {
DiscoveryInfo DiscoveryInfoSpec `json:"discovery"`
APIConfig APIConfigSpec `json:"api"`
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
NamesConfig NamesConfigSpec `json:"names"`
KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"`
Labels map[string]string `json:"labels"`

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package supervisor contains functionality to load/store Config's from/to
@@ -13,6 +13,7 @@ import (
"sigs.k8s.io/yaml"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/plog"
)
@@ -34,6 +35,12 @@ func FromPath(path string) (*Config, error) {
config.Labels = make(map[string]string)
}
maybeSetAPIGroupSuffixDefault(&config.APIGroupSuffix)
if err := validateAPIGroupSuffix(*config.APIGroupSuffix); err != nil {
return nil, fmt.Errorf("validate apiGroupSuffix: %w", err)
}
if err := validateNames(&config.NamesConfig); err != nil {
return nil, fmt.Errorf("validate names: %w", err)
}
@@ -45,6 +52,16 @@ func FromPath(path string) (*Config, error) {
return &config, nil
}
func maybeSetAPIGroupSuffixDefault(apiGroupSuffix **string) {
if *apiGroupSuffix == nil {
*apiGroupSuffix = stringPtr("pinniped.dev")
}
}
func validateAPIGroupSuffix(apiGroupSuffix string) error {
return groupsuffix.Validate(apiGroupSuffix)
}
func validateNames(names *NamesConfigSpec) error {
missingNames := []string{}
if names.DefaultTLSCertificateSecret == "" {
@@ -55,3 +72,7 @@ func validateNames(names *NamesConfigSpec) error {
}
return nil
}
func stringPtr(s string) *string {
return &s
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisor
@@ -24,6 +24,7 @@ func TestFromPath(t *testing.T) {
name: "Happy",
yaml: here.Doc(`
---
apiGroupSuffix: some.suffix.com
labels:
myLabelKey1: myLabelValue1
myLabelKey2: myLabelValue2
@@ -31,6 +32,7 @@ func TestFromPath(t *testing.T) {
defaultTLSCertificateSecret: my-secret-name
`),
wantConfig: &Config{
APIGroupSuffix: stringPtr("some.suffix.com"),
Labels: map[string]string{
"myLabelKey1": "myLabelValue1",
"myLabelKey2": "myLabelValue2",
@@ -48,7 +50,8 @@ func TestFromPath(t *testing.T) {
defaultTLSCertificateSecret: my-secret-name
`),
wantConfig: &Config{
Labels: map[string]string{},
APIGroupSuffix: stringPtr("pinniped.dev"),
Labels: map[string]string{},
NamesConfig: NamesConfigSpec{
DefaultTLSCertificateSecret: "my-secret-name",
},
@@ -61,6 +64,16 @@ func TestFromPath(t *testing.T) {
`),
wantError: "validate names: missing required names: defaultTLSCertificateSecret",
},
{
name: "apiGroupSuffix is prefixed with '.'",
yaml: here.Doc(`
---
apiGroupSuffix: .starts.with.dot
names:
defaultTLSCertificateSecret: my-secret-name
`),
wantError: "validate apiGroupSuffix: 1 error(s):\n- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
},
}
for _, test := range tests {
test := test

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisor
@@ -7,9 +7,10 @@ import "go.pinniped.dev/internal/plog"
// Config contains knobs to setup an instance of the Pinniped Supervisor.
type Config struct {
Labels map[string]string `json:"labels"`
NamesConfig NamesConfigSpec `json:"names"`
LogLevel plog.LogLevel `json:"logLevel"`
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
Labels map[string]string `json:"labels"`
NamesConfig NamesConfigSpec `json:"names"`
LogLevel plog.LogLevel `json:"logLevel"`
}
// NamesConfigSpec configures the names of some Kubernetes resources for the Supervisor.

View File

@@ -15,6 +15,7 @@ import (
"k8s.io/klog/v2"
loginapi "go.pinniped.dev/generated/1.20/apis/concierge/login"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/plog"
)
@@ -26,7 +27,8 @@ var (
// Cache implements the authenticator.Token interface by multiplexing across a dynamic set of authenticators
// loaded from authenticator resources.
type Cache struct {
cache sync.Map
cache sync.Map
apiGroupSuffix string
}
type Key struct {
@@ -41,8 +43,8 @@ type Value interface {
}
// New returns an empty cache.
func New() *Cache {
return &Cache{}
func New(apiGroupSuffix string) *Cache {
return &Cache{apiGroupSuffix: apiGroupSuffix}
}
// Get an authenticator by key.
@@ -90,7 +92,12 @@ func (c *Cache) AuthenticateTokenCredentialRequest(ctx context.Context, req *log
Kind: req.Spec.Authenticator.Kind,
}
if req.Spec.Authenticator.APIGroup != nil {
key.APIGroup = *req.Spec.Authenticator.APIGroup
// The key must always be API group pinniped.dev because that's what the cache filler will always use.
apiGroup, replaced := groupsuffix.Unreplace(*req.Spec.Authenticator.APIGroup, c.apiGroupSuffix)
if !replaced {
return nil, ErrNoSuchAuthenticator
}
key.APIGroup = apiGroup
}
val := c.Get(key)

View File

@@ -28,7 +28,7 @@ func TestCache(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache := New()
cache := New("pinniped.dev")
require.NotNil(t, cache)
key1 := Key{Namespace: "foo", Name: "authenticator-one"}
@@ -57,7 +57,7 @@ func TestCache(t *testing.T) {
{APIGroup: "b", Kind: "b", Namespace: "b", Name: "b"},
}
for tries := 0; tries < 10; tries++ {
cache := New()
cache := New("pinniped.dev")
for _, i := range rand.Perm(len(keysInExpectedOrder)) {
cache.Store(keysInExpectedOrder[i], nil)
}
@@ -91,46 +91,48 @@ func TestAuthenticateTokenCredentialRequest(t *testing.T) {
Name: validRequest.Spec.Authenticator.Name,
}
mockCache := func(t *testing.T, res *authenticator.Response, authenticated bool, err error) *Cache {
mockCache := func(t *testing.T, apiGroupSuffix string, expectAuthenticatorToBeCalled bool, res *authenticator.Response, authenticated bool, err error) *Cache {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
m := mocktokenauthenticator.NewMockToken(ctrl)
m.EXPECT().AuthenticateToken(audienceFreeContext{}, validRequest.Spec.Token).Return(res, authenticated, err)
c := New()
if expectAuthenticatorToBeCalled {
m.EXPECT().AuthenticateToken(audienceFreeContext{}, validRequest.Spec.Token).Return(res, authenticated, err)
}
c := New(apiGroupSuffix)
c.Store(validRequestKey, m)
return c
}
t.Run("no such authenticator", func(t *testing.T) {
c := New()
c := New("pinniped.dev")
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
require.EqualError(t, err, "no such authenticator")
require.Nil(t, res)
})
t.Run("authenticator returns error", func(t *testing.T) {
c := mockCache(t, nil, false, fmt.Errorf("some authenticator error"))
c := mockCache(t, "pinniped.dev", true, nil, false, fmt.Errorf("some authenticator error"))
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
require.EqualError(t, err, "some authenticator error")
require.Nil(t, res)
})
t.Run("authenticator returns unauthenticated without error", func(t *testing.T) {
c := mockCache(t, &authenticator.Response{}, false, nil)
c := mockCache(t, "pinniped.dev", true, &authenticator.Response{}, false, nil)
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
require.NoError(t, err)
require.Nil(t, res)
})
t.Run("authenticator returns nil response without error", func(t *testing.T) {
c := mockCache(t, nil, true, nil)
c := mockCache(t, "pinniped.dev", true, nil, true, nil)
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
require.NoError(t, err)
require.Nil(t, res)
})
t.Run("authenticator returns response with nil user", func(t *testing.T) {
c := mockCache(t, &authenticator.Response{}, true, nil)
c := mockCache(t, "pinniped.dev", true, &authenticator.Response{}, true, nil)
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
require.NoError(t, err)
require.Nil(t, res)
@@ -151,7 +153,7 @@ func TestAuthenticateTokenCredentialRequest(t *testing.T) {
}
},
)
c := New()
c := New("pinniped.dev")
c.Store(validRequestKey, m)
ctx, cancel := context.WithCancel(context.Background())
@@ -171,7 +173,7 @@ func TestAuthenticateTokenCredentialRequest(t *testing.T) {
Groups: []string{"test-group-1", "test-group-2"},
Extra: map[string][]string{"extra-key-1": {"extra-value-1", "extra-value-2"}},
}
c := mockCache(t, &authenticator.Response{User: &userInfo}, true, nil)
c := mockCache(t, "pinniped.dev", true, &authenticator.Response{User: &userInfo}, true, nil)
audienceCtx := authenticator.WithAudiences(context.Background(), authenticator.Audiences{"test-audience-1"})
res, err := c.AuthenticateTokenCredentialRequest(audienceCtx, validRequest.DeepCopy())
@@ -182,6 +184,50 @@ func TestAuthenticateTokenCredentialRequest(t *testing.T) {
require.Equal(t, []string{"test-group-1", "test-group-2"}, res.GetGroups())
require.Equal(t, map[string][]string{"extra-key-1": {"extra-value-1", "extra-value-2"}}, res.GetExtra())
})
t.Run("using a non-default API group suffix still performs the cache lookup using the pinniped.dev suffix", func(t *testing.T) {
authenticationGroupWithCustomSuffix := "authentication.concierge.custom-suffix.com"
validRequestForAlternateAPIGroup := loginapi.TokenCredentialRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-namespace",
},
Spec: loginapi.TokenCredentialRequestSpec{
Authenticator: corev1.TypedLocalObjectReference{
APIGroup: &authenticationGroupWithCustomSuffix,
Kind: "WebhookAuthenticator",
Name: "test-name",
},
Token: "test-token",
},
Status: loginapi.TokenCredentialRequestStatus{},
}
userInfo := user.DefaultInfo{
Name: "test-user",
UID: "test-uid",
Groups: []string{"test-group-1", "test-group-2"},
Extra: map[string][]string{"extra-key-1": {"extra-value-1", "extra-value-2"}},
}
c := mockCache(t, "custom-suffix.com", true, &authenticator.Response{User: &userInfo}, true, nil)
audienceCtx := authenticator.WithAudiences(context.Background(), authenticator.Audiences{"test-audience-1"})
res, err := c.AuthenticateTokenCredentialRequest(audienceCtx, validRequestForAlternateAPIGroup.DeepCopy())
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, "test-user", res.GetName())
require.Equal(t, "test-uid", res.GetUID())
require.Equal(t, []string{"test-group-1", "test-group-2"}, res.GetGroups())
require.Equal(t, map[string][]string{"extra-key-1": {"extra-value-1", "extra-value-2"}}, res.GetExtra())
})
t.Run("using a non-default API group suffix and the incoming request mentions a different API group, results in no such authenticator", func(t *testing.T) {
c := mockCache(t, "custom-suffix.com", false, &authenticator.Response{User: &user.DefaultInfo{Name: "someone"}}, true, nil)
// Note that the validRequest.Spec.Authenticator.APIGroup value uses "pinniped.dev", not "custom-suffix.com"
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
require.EqualError(t, err, "no such authenticator")
require.Nil(t, res)
})
}
type audienceFreeContext struct{}

View File

@@ -153,7 +153,7 @@ func TestController(t *testing.T) {
fakeClient := pinnipedfake.NewSimpleClientset(tt.objects...)
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
cache := authncache.New()
cache := authncache.New("pinniped.dev")
if tt.initialCache != nil {
tt.initialCache(t, cache)
}

View File

@@ -327,7 +327,7 @@ func TestController(t *testing.T) {
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
cache := authncache.New()
cache := authncache.New("pinniped.dev")
testLog := testlogger.New(t)
if tt.cache != nil {

View File

@@ -90,7 +90,7 @@ func TestController(t *testing.T) {
fakeClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
cache := authncache.New()
cache := authncache.New("pinniped.dev")
testLog := testlogger.New(t)
controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog)

View File

@@ -1,13 +1,11 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubecertagent
import (
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
@@ -72,7 +70,7 @@ func (c *deleterController) Sync(ctx controllerlib.Context) error {
if err != nil {
return err
}
if controllerManagerPod == nil || inTerminalState(agentPod) ||
if controllerManagerPod == nil ||
!isAgentPodUpToDate(agentPod, c.agentPodConfig.newAgentPod(controllerManagerPod)) {
plog.Debug("deleting agent pod", "pod", klog.KObj(agentPod))
err := c.k8sClient.
@@ -87,23 +85,3 @@ func (c *deleterController) Sync(ctx controllerlib.Context) error {
return nil
}
func inTerminalState(pod *corev1.Pod) bool {
switch pod.Status.Phase {
// Running and Pending are non-terminal states. We should not delete pods in these states.
case corev1.PodRunning, corev1.PodPending:
return false
// Succeeded and Failed are terminal states. If a pod has entered one of these states, we want to delete it so
// it can be recreated by the other controllers.
case corev1.PodSucceeded, corev1.PodFailed:
return true
// In other cases, we want to delete the pod but more carefully. We only consider the pod "terminal" if it is in
// this state more than 5 minutes after creation.
case corev1.PodUnknown:
fallthrough
default:
return time.Since(pod.CreationTimestamp.Time) > 5*time.Minute
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubecertagent
@@ -12,7 +12,6 @@ import (
"github.com/sclevine/spec/report"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
kubeinformers "k8s.io/client-go/informers"
corev1informers "k8s.io/client-go/informers/core/v1"
@@ -123,7 +122,6 @@ func TestDeleterControllerSync(t *testing.T) {
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
kubeSystemNamespace, agentPodNamespace, "ignored for this test", "ignored for this test",
)
agentPod.Status.Phase = corev1.PodRunning
podsGVR = schema.GroupVersionResource{
Group: corev1.SchemeGroupVersion.Group,
@@ -496,65 +494,6 @@ func TestDeleterControllerSync(t *testing.T) {
})
})
when("there is an unhealthy agent pod", func() {
it.Before(func() {
// The matching controller-manager pod exists.
r.NoError(kubeSystemInformerClient.Tracker().Add(controllerManagerPod))
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
})
when("in a Failed state", func() {
it.Before(func() {
// The pod is in a "Failed" state, even though it otherwise matches.
agentPod.Status.Phase = corev1.PodFailed
r.NoError(agentInformerClient.Tracker().Add(agentPod))
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
})
it("deletes the agent pod", func() {
startInformersAndController()
err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err)
requireAgentPodWasDeleted()
})
})
when("in an Unknown state but recent", func() {
it.Before(func() {
agentPod.Status.Phase = corev1.PodUnknown
agentPod.CreationTimestamp = metav1.Now()
r.NoError(agentInformerClient.Tracker().Add(agentPod))
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
})
it("does nothing", func() {
startInformersAndController()
err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err)
r.Empty(kubeAPIClient.Actions())
})
})
when("in an Unknown state and older", func() {
it.Before(func() {
agentPod.Status.Phase = corev1.PodUnknown
agentPod.CreationTimestamp = metav1.NewTime(time.Now().Add(-1 * time.Hour))
r.NoError(agentInformerClient.Tracker().Add(agentPod))
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
})
it("deletes the agent pod", func() {
startInformersAndController()
err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err)
requireAgentPodWasDeleted()
})
})
})
when("there is no agent pod", func() {
it("does nothing", func() {
startInformersAndController()

View File

@@ -129,7 +129,7 @@ func (c *AgentPodConfig) newAgentPod(controllerManagerPod *corev1.Pod) *corev1.P
Name: "sleeper",
Image: c.ContainerImage,
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{"/bin/sh", "-c", "/bin/sleep infinity"},
Command: []string{"/bin/sleep", "infinity"},
VolumeMounts: controllerManagerPod.Spec.Containers[0].VolumeMounts,
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubecertagent
@@ -101,7 +101,7 @@ func exampleControllerManagerAndAgentPods(
Image: "some-agent-image",
ImagePullPolicy: corev1.PullIfNotPresent,
VolumeMounts: controllerManagerPod.Spec.Containers[0].VolumeMounts,
Command: []string{"/bin/sh", "-c", "/bin/sleep infinity"},
Command: []string{"/bin/sleep", "infinity"},
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("16Mi"),

View File

@@ -28,7 +28,6 @@ import (
var generateKey = generateSymmetricKey
type supervisorSecretsController struct {
owner *appsv1.Deployment
labels map[string]string
kubeClient kubernetes.Interface
secretInformer corev1informers.SecretInformer
@@ -46,7 +45,6 @@ func NewSupervisorSecretsController(
initialEventFunc pinnipedcontroller.WithInitialEventOptionFunc,
) controllerlib.Controller {
c := supervisorSecretsController{
owner: owner,
labels: labels,
kubeClient: kubeClient,
secretInformer: secretInformer,
@@ -64,13 +62,7 @@ func NewSupervisorSecretsController(
if secret.Type != SupervisorCSRFSigningKeySecretType {
return false
}
ownerReferences := secret.GetOwnerReferences()
for i := range secret.GetOwnerReferences() {
if ownerReferences[i].UID == owner.GetUID() {
return true
}
}
return false
return true
}, nil),
controllerlib.InformerOption{},
),
@@ -96,7 +88,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error {
return nil
}
newSecret, err := generateSecret(ctx.Key.Namespace, ctx.Key.Name, c.labels, secretDataFunc, c.owner)
newSecret, err := generateSecret(ctx.Key.Namespace, ctx.Key.Name, c.labels, secretDataFunc)
if err != nil {
return fmt.Errorf("failed to generate secret: %w", err)
}
@@ -193,27 +185,17 @@ func secretDataFunc() (map[string][]byte, error) {
}, nil
}
func generateSecret(namespace, name string, labels map[string]string, secretDataFunc func() (map[string][]byte, error), owner metav1.Object) (*corev1.Secret, error) {
func generateSecret(namespace, name string, labels map[string]string, secretDataFunc func() (map[string][]byte, error)) (*corev1.Secret, error) {
secretData, err := secretDataFunc()
if err != nil {
return nil, err
}
deploymentGVK := appsv1.SchemeGroupVersion.WithKind("Deployment")
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: deploymentGVK.GroupVersion().String(),
Kind: deploymentGVK.Kind,
Name: owner.GetName(),
UID: owner.GetUID(),
},
},
Labels: labels,
Labels: labels,
},
Type: SupervisorCSRFSigningKeySecretType,
Data: secretData,

View File

@@ -34,12 +34,6 @@ var (
},
}
ownerGVK = schema.GroupVersionKind{
Group: appsv1.SchemeGroupVersion.Group,
Version: appsv1.SchemeGroupVersion.Version,
Kind: "Deployment",
}
labels = map[string]string{
"some-label-key-1": "some-label-value-1",
"some-label-key-2": "some-label-value-2",
@@ -57,89 +51,13 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) {
wantDelete bool
}{
{
name: "owner reference is missing",
name: "owner reference is missing but Secret type is correct",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
},
},
},
{
name: "owner reference with incorrect `APIVersion`",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
{
Name: owner.GetName(),
Kind: ownerGVK.Kind,
UID: owner.GetUID(),
},
},
},
},
wantAdd: true,
wantUpdate: true,
wantDelete: true,
},
{
name: "owner reference with incorrect `Kind`",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ownerGVK.String(),
Name: owner.GetName(),
Kind: "IncorrectKind",
UID: owner.GetUID(),
},
},
},
},
wantAdd: true,
wantUpdate: true,
wantDelete: true,
},
{
name: "expected owner reference with incorrect `UID`",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ownerGVK.String(),
Name: owner.GetName(),
Kind: ownerGVK.Kind,
UID: "DOES_NOT_MATCH",
},
},
},
},
},
{
name: "multiple owner references (expected owner reference, and one more)",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
{
Kind: "UnrelatedKind",
},
{
APIVersion: ownerGVK.String(),
Name: owner.GetName(),
Kind: ownerGVK.Kind,
UID: owner.GetUID(),
},
},
},
},
wantAdd: true,
wantUpdate: true,
wantDelete: true,
@@ -152,10 +70,8 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) {
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ownerGVK.String(),
Name: owner.GetName(),
Kind: ownerGVK.Kind,
UID: owner.GetUID(),
Name: owner.GetName(),
UID: owner.GetUID(),
},
},
},
@@ -166,32 +82,15 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) {
secret: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "some-namespace"}},
},
{
name: "owner reference with `Controller`: true",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(owner, ownerGVK),
},
},
},
wantAdd: true,
wantUpdate: true,
wantDelete: true,
},
{
name: "expected owner reference - where `Controller`: false",
name: "realistic owner reference and correct Secret type",
secret: &corev1.Secret{
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ownerGVK.String(),
Name: owner.GetName(),
Kind: ownerGVK.Kind,
UID: owner.GetUID(),
Name: owner.GetName(),
UID: owner.GetUID(),
},
},
},
@@ -272,15 +171,7 @@ func TestSupervisorSecretsControllerSync(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: generatedSecretName,
Namespace: generatedSecretNamespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ownerGVK.GroupVersion().String(),
Kind: ownerGVK.Kind,
Name: owner.GetName(),
UID: owner.GetUID(),
},
},
Labels: labels,
Labels: labels,
},
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
Data: map[string][]byte{
@@ -292,15 +183,7 @@ func TestSupervisorSecretsControllerSync(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: generatedSecretName,
Namespace: generatedSecretNamespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ownerGVK.GroupVersion().String(),
Kind: ownerGVK.Kind,
Name: owner.GetName(),
UID: owner.GetUID(),
},
},
Labels: labels,
Labels: labels,
},
Type: "secrets.pinniped.dev/supervisor-csrf-signing-key",
Data: map[string][]byte{

View File

@@ -15,7 +15,7 @@ import (
"sort"
"time"
"github.com/coreos/go-oidc"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-logr/logr"
"golang.org/x/oauth2"
corev1 "k8s.io/api/core/v1"

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@@ -12,6 +12,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api"
examplestart "go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/starter"
@@ -31,7 +32,8 @@ func TestExampleController(t *testing.T) {
err := examplestart.StartExampleController(ctx, config, secretData)
require.NoError(t, err)
client := library.NewClientset(t)
client, err := kubernetes.NewForConfig(config)
require.NoError(t, err, "unexpected failure from kubernetes.NewForConfig()")
namespaces := client.CoreV1().Namespaces()

View File

@@ -30,6 +30,7 @@ import (
"go.pinniped.dev/internal/deploymentref"
"go.pinniped.dev/internal/downward"
"go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/kubeclient"
)
@@ -45,6 +46,9 @@ type Config struct {
// ServerInstallationInfo provides the name of the pod in which Pinniped is running and the namespace in which Pinniped is deployed.
ServerInstallationInfo *downward.PodInfo
// APIGroupSuffix is the suffix of the Pinniped API that should be targeted by these controllers.
APIGroupSuffix string
// NamesConfig comes from the Pinniped config API (see api.Config). It specifies how Kubernetes
// objects should be named.
NamesConfig *concierge.NamesConfigSpec
@@ -85,7 +89,10 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
return nil, fmt.Errorf("cannot create deployment ref: %w", err)
}
client, err := kubeclient.New(dref)
client, err := kubeclient.New(
dref,
kubeclient.WithMiddleware(groupsuffix.New(c.APIGroupSuffix)),
)
if err != nil {
return nil, fmt.Errorf("could not create clients for the controllers: %w", err)
}
@@ -106,6 +113,12 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
Name: c.NamesConfig.CredentialIssuer,
}
groupName, ok := groupsuffix.Replace(loginv1alpha1.GroupName, c.APIGroupSuffix)
if !ok {
return nil, fmt.Errorf("cannot make api group from %s/%s", loginv1alpha1.GroupName, c.APIGroupSuffix)
}
apiServiceName := loginv1alpha1.SchemeGroupVersion.Version + "." + groupName
// Create controller manager.
controllerManager := controllerlib.
NewManager().
@@ -145,7 +158,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
apicerts.NewAPIServiceUpdaterController(
c.ServerInstallationInfo.Namespace,
c.NamesConfig.ServingCertificateSecret,
loginv1alpha1.SchemeGroupVersion.Version+"."+loginv1alpha1.GroupName,
apiServiceName,
client.Aggregation,
informers.installationNamespaceK8s.Core().V1().Secrets(),
controllerlib.WithInformer,

View File

@@ -17,24 +17,35 @@ import (
"go.pinniped.dev/internal/ownerref"
)
// getTempClient is stubbed out for testing.
//
// We would normally inject a kubernetes.Interface into New(), but the client we want to create in
// the calling code depends on the return value of New() (i.e., on the kubeclient.Option for the
// OwnerReference).
//nolint: gochecknoglobals
var getTempClient = func() (kubernetes.Interface, error) {
client, err := kubeclient.New()
if err != nil {
return nil, err
}
return client.Kubernetes, nil
}
func New(podInfo *downward.PodInfo) (kubeclient.Option, *appsv1.Deployment, error) {
tempClient, err := kubeclient.New()
tempClient, err := getTempClient()
if err != nil {
return nil, nil, fmt.Errorf("cannot create temp client: %w", err)
}
deployment, err := getDeployment(tempClient.Kubernetes, podInfo)
deployment, err := getDeployment(tempClient, podInfo)
if err != nil {
return nil, nil, fmt.Errorf("cannot get deployment: %w", err)
}
ref := metav1.OwnerReference{
Name: deployment.Name,
UID: deployment.UID,
}
ref.APIVersion, ref.Kind = appsv1.SchemeGroupVersion.WithKind("Deployment").ToAPIVersionAndKind()
// work around stupid behavior of WithoutVersionDecoder.Decode
deployment.APIVersion, deployment.Kind = appsv1.SchemeGroupVersion.WithKind("Deployment").ToAPIVersionAndKind()
return kubeclient.WithMiddleware(ownerref.New(ref)), deployment, nil
return kubeclient.WithMiddleware(ownerref.New(deployment)), deployment, nil
}
func getDeployment(kubeClient kubernetes.Interface, podInfo *downward.PodInfo) (*appsv1.Deployment, error) {

View File

@@ -0,0 +1,127 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package deploymentref
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
kubefake "k8s.io/client-go/kubernetes/fake"
kubetesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/downward"
)
func TestNew(t *testing.T) {
troo := true
goodDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "Deployment",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
Name: "some-name",
},
}
tests := []struct {
name string
apiObjects []runtime.Object
client func(*kubefake.Clientset)
createClientErr error
podInfo *downward.PodInfo
wantDeployment *appsv1.Deployment
wantError string
}{
{
name: "happy",
apiObjects: []runtime.Object{
goodDeployment,
&appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
Name: "some-name-rsname",
OwnerReferences: []metav1.OwnerReference{
{
Controller: &troo,
Name: "some-name",
},
},
},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
Name: "some-name-rsname-podhash",
OwnerReferences: []metav1.OwnerReference{
{
Controller: &troo,
Name: "some-name-rsname",
},
},
},
},
},
podInfo: &downward.PodInfo{
Namespace: "some-namespace",
Name: "some-name-rsname-podhash",
},
wantDeployment: goodDeployment,
},
{
name: "failed to create client",
createClientErr: errors.New("some create error"),
podInfo: &downward.PodInfo{
Namespace: "some-namespace",
Name: "some-name-rsname-podhash",
},
wantError: "cannot create temp client: some create error",
},
{
name: "failed to talk to api",
client: func(c *kubefake.Clientset) {
c.PrependReactor(
"get",
"pods",
func(_ kubetesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("get failed")
},
)
},
podInfo: &downward.PodInfo{
Namespace: "some-namespace",
Name: "some-name-rsname-podhash",
},
wantError: "cannot get deployment: could not get pod: get failed",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
client := kubefake.NewSimpleClientset(test.apiObjects...)
if test.client != nil {
test.client(client)
}
getTempClient = func() (kubernetes.Interface, error) {
return client, test.createClientErr
}
_, d, err := New(test.podInfo)
if test.wantError != "" {
require.EqualError(t, err, test.wantError)
return
}
require.NoError(t, err)
require.Equal(t, test.wantDeployment, d)
})
}
}

View File

@@ -0,0 +1,143 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package groupsuffix
import (
"context"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/multierror"
)
const (
pinnipedDefaultSuffix = "pinniped.dev"
pinnipedDefaultSuffixWithDot = ".pinniped.dev"
)
func New(apiGroupSuffix string) kubeclient.Middleware {
// return a no-op middleware by default
if len(apiGroupSuffix) == 0 || apiGroupSuffix == pinnipedDefaultSuffix {
return nil
}
return kubeclient.Middlewares{
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
group := rt.Resource().Group
newGroup, ok := Replace(group, apiGroupSuffix)
if !ok {
return // ignore APIs that do not have our group
}
rt.MutateRequest(func(obj kubeclient.Object) {
typeMeta := obj.GetObjectKind()
origGVK := typeMeta.GroupVersionKind()
newGVK := schema.GroupVersionKind{
Group: newGroup,
Version: origGVK.Version,
Kind: origGVK.Kind,
}
typeMeta.SetGroupVersionKind(newGVK)
})
}),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// we should not mess with owner refs on things we did not create
if rt.Verb() != kubeclient.VerbCreate {
return
}
// we probably do not want mess with an owner ref on a subresource
if len(rt.Subresource()) != 0 {
return
}
rt.MutateRequest(mutateOwnerRefs(Replace, apiGroupSuffix))
}),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// always unreplace owner refs with apiGroupSuffix because we can consume those objects across all verbs
rt.MutateResponse(mutateOwnerRefs(Unreplace, apiGroupSuffix))
}),
}
}
func mutateOwnerRefs(replaceFunc func(baseAPIGroup, apiGroupSuffix string) (string, bool), apiGroupSuffix string) func(kubeclient.Object) {
return func(obj kubeclient.Object) {
// fix up owner refs because they are consumed by external and internal actors
oldRefs := obj.GetOwnerReferences()
if len(oldRefs) == 0 {
return
}
var changedGroup bool
newRefs := make([]metav1.OwnerReference, 0, len(oldRefs))
for _, ref := range oldRefs {
ref := *ref.DeepCopy()
gv, _ := schema.ParseGroupVersion(ref.APIVersion) // error is safe to ignore, empty gv is fine
if newGroup, ok := replaceFunc(gv.Group, apiGroupSuffix); ok {
changedGroup = true
gv.Group = newGroup
ref.APIVersion = gv.String()
}
newRefs = append(newRefs, ref)
}
if !changedGroup {
return
}
obj.SetOwnerReferences(newRefs)
}
}
// Replace constructs an API group from a baseAPIGroup and a parameterized apiGroupSuffix.
//
// We assume that all baseAPIGroup's will end in "pinniped.dev", and therefore we can safely replace
// the reference to "pinniped.dev" with the provided apiGroupSuffix. If the provided baseAPIGroup
// does not end in "pinniped.dev", then this function will return an empty string and false.
//
// See ExampleReplace_loginv1alpha1 and ExampleReplace_string for more information on input/output pairs.
func Replace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
if !strings.HasSuffix(baseAPIGroup, pinnipedDefaultSuffixWithDot) {
return "", false
}
return strings.TrimSuffix(baseAPIGroup, pinnipedDefaultSuffix) + apiGroupSuffix, true
}
// Unreplace is like performing an undo of Replace().
func Unreplace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
if !strings.HasSuffix(baseAPIGroup, "."+apiGroupSuffix) {
return "", false
}
return strings.TrimSuffix(baseAPIGroup, apiGroupSuffix) + pinnipedDefaultSuffix, true
}
// Validate validates the provided apiGroupSuffix is usable as an API group suffix. Specifically, it
// makes sure that the provided apiGroupSuffix is a valid DNS-1123 subdomain with at least one dot,
// to match Kubernetes behavior.
func Validate(apiGroupSuffix string) error {
err := multierror.New()
if len(strings.Split(apiGroupSuffix, ".")) < 2 {
err.Add(constable.Error("must contain '.'"))
}
errorStrings := validation.IsDNS1123Subdomain(apiGroupSuffix)
for _, errorString := range errorStrings {
errorString := errorString
err.Add(constable.Error(errorString))
}
return err.ErrOrNil()
}

View File

@@ -0,0 +1,531 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package groupsuffix
import (
"context"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/testutil"
)
func ExampleReplace_loginv1alpha1() {
s, _ := Replace(loginv1alpha1.GroupName, "tuna.fish.io")
fmt.Println(s)
// Output: login.concierge.tuna.fish.io
}
func ExampleReplace_string() {
s, _ := Replace("idp.supervisor.pinniped.dev", "marlin.io")
fmt.Println(s)
// Output: idp.supervisor.marlin.io
}
func TestMiddlware(t *testing.T) {
const newSuffix = "some.suffix.com"
podWithoutOwner := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{},
},
}
nonPinnipedOwner := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "some-name",
UID: "some-uid",
},
}
nonPinnipedOwnerGVK := corev1.SchemeGroupVersion.WithKind("Pod")
podWithNonPinnipedOwner := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
},
},
}
var ok bool
pinnipedOwner := &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "some-name",
UID: "some-uid",
},
}
pinnipedOwnerGVK := configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain")
pinnipedOwnerWithNewGroupGVK := configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain")
pinnipedOwnerWithNewGroupGVK.Group, ok = Replace(pinnipedOwnerWithNewGroupGVK.Group, newSuffix)
require.True(t, ok)
podWithPinnipedOwner := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerGVK),
// make sure we don't update the non-pinniped owner
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
},
},
}
podWithPinnipedOwnerWithNewGroup := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerWithNewGroupGVK),
// make sure we don't update the non-pinniped owner
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
},
},
}
federationDomainWithPinnipedOwner := &configv1alpha1.FederationDomain{
TypeMeta: metav1.TypeMeta{
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
Kind: "FederationDomain",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerGVK),
// make sure we don't update the non-pinniped owner
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
},
},
}
federationDomainWithPinnipedOwnerWithNewGroup := &configv1alpha1.FederationDomain{
TypeMeta: metav1.TypeMeta{
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
Kind: "FederationDomain",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerWithNewGroupGVK),
// make sure we don't update the non-pinniped owner
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
},
},
}
federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup := &configv1alpha1.FederationDomain{
TypeMeta: metav1.TypeMeta{
APIVersion: replaceGV(t, configv1alpha1.SchemeGroupVersion, newSuffix),
Kind: "FederationDomain",
},
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerWithNewGroupGVK),
// make sure we don't update the non-pinniped owner
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
},
},
}
tokenCredentialRequest := &loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: loginv1alpha1.SchemeGroupVersion.String(),
Kind: "TokenCredentialRequest",
},
}
tokenCredentialRequestWithNewGroup := &loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: replaceGV(t, loginv1alpha1.SchemeGroupVersion, newSuffix),
Kind: "TokenCredentialRequest",
},
}
tests := []struct {
name string
apiGroupSuffix string
rt *testutil.RoundTrip
requestObj, responseObj kubeclient.Object
wantNilMiddleware bool
wantMutateRequests, wantMutateResponses int
wantRequestObj, wantResponseObj kubeclient.Object
skip bool
}{
{
name: "api group suffix is empty",
apiGroupSuffix: "",
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
wantNilMiddleware: true,
},
{
name: "api group suffix is default",
apiGroupSuffix: "pinniped.dev",
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
wantNilMiddleware: true,
},
{
name: "get resource without pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithoutOwner,
wantMutateResponses: 1,
wantResponseObj: podWithoutOwner,
},
{
name: "get resource without pinniped.dev with non-pinniped.dev owner ref",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithNonPinnipedOwner,
wantMutateResponses: 1,
wantResponseObj: podWithNonPinnipedOwner,
},
{
name: "get resource without pinniped.dev with pinniped.dev owner ref",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithPinnipedOwnerWithNewGroup,
wantMutateResponses: 1,
wantResponseObj: podWithPinnipedOwner,
},
{
name: "get resource with pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest,
},
{
name: "create resource without pinniped.dev and without owner ref",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
requestObj: podWithoutOwner,
responseObj: podWithoutOwner,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: podWithoutOwner,
wantResponseObj: podWithoutOwner,
},
{
name: "create resource without pinniped.dev and with owner ref that has no pinniped.dev owner",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
requestObj: podWithNonPinnipedOwner,
responseObj: podWithNonPinnipedOwner,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: podWithNonPinnipedOwner,
wantResponseObj: podWithNonPinnipedOwner,
},
{
name: "create resource without pinniped.dev and with owner ref that has pinniped.dev owner",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
requestObj: podWithPinnipedOwner,
responseObj: podWithPinnipedOwnerWithNewGroup,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: podWithPinnipedOwnerWithNewGroup,
wantResponseObj: podWithPinnipedOwner,
},
{
name: "create subresource without pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")).
WithSubresource("some-subresource"),
responseObj: podWithPinnipedOwner,
wantMutateResponses: 1,
wantResponseObj: podWithPinnipedOwner,
},
{
// test that both of our middleware request mutations play nicely with each other
name: "create resource with pinniped.dev and with owner ref that has pinniped.dev owner",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithNamespace("some-namespace").
WithResource(configv1alpha1.SchemeGroupVersion.WithResource("federationdomains")),
requestObj: federationDomainWithPinnipedOwner,
responseObj: federationDomainWithPinnipedOwnerWithNewGroup,
wantMutateRequests: 2,
wantMutateResponses: 1,
wantRequestObj: federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup,
wantResponseObj: federationDomainWithPinnipedOwner,
},
{
name: "update resource without pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbUpdate).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithoutOwner,
wantMutateResponses: 1,
wantResponseObj: podWithoutOwner,
},
{
name: "update resource with pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbUpdate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest,
},
{
name: "list resource without pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbList).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithoutOwner,
wantMutateResponses: 1,
wantResponseObj: podWithoutOwner,
},
{
name: "list resource with pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbList).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest,
},
{
name: "watch resource without pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbWatch).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithoutOwner,
wantMutateResponses: 1,
wantResponseObj: podWithoutOwner,
},
{
name: "watch resource with pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbList).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest,
},
{
name: "patch resource without pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbPatch).
WithNamespace("some-namespace").
WithResource(corev1.SchemeGroupVersion.WithResource("pods")),
responseObj: podWithoutOwner,
wantMutateResponses: 1,
wantResponseObj: podWithoutOwner,
},
{
name: "patch resource with pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbList).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest,
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
if test.skip {
t.Skip()
}
m := New(test.apiGroupSuffix)
if test.wantNilMiddleware {
require.Nil(t, m, "wanted nil middleware")
return
}
m.Handle(context.Background(), test.rt)
require.Len(t, test.rt.MutateRequests, test.wantMutateRequests)
require.Len(t, test.rt.MutateResponses, test.wantMutateResponses)
if test.wantMutateRequests != 0 {
require.NotNil(t, test.requestObj, "expected test.requestObj to be set")
objMutated := test.requestObj.DeepCopyObject().(kubeclient.Object)
for _, mutateRequest := range test.rt.MutateRequests {
mutateRequest := mutateRequest
mutateRequest(objMutated)
}
require.Equal(t, test.wantRequestObj, objMutated, "request obj did not match")
}
if test.wantMutateResponses != 0 {
require.NotNil(t, test.responseObj, "expected test.responseObj to be set")
objMutated := test.responseObj.DeepCopyObject().(kubeclient.Object)
for _, mutateResponse := range test.rt.MutateResponses {
mutateResponse := mutateResponse
mutateResponse(objMutated)
}
require.Equal(t, test.wantResponseObj, objMutated, "response obj did not match")
}
})
}
}
func TestReplaceError(t *testing.T) {
s, ok := Replace("bad-suffix-that-doesnt-end-in-pinniped-dot-dev", "shouldnt-matter.com")
require.Equal(t, "", s)
require.False(t, ok)
s, ok = Replace("bad-suffix-that-end-in.prefixed-pinniped.dev", "shouldnt-matter.com")
require.Equal(t, "", s)
require.False(t, ok)
}
func TestReplaceSuffix(t *testing.T) {
s, ok := Replace("something.pinniped.dev.something-else.pinniped.dev", "tuna.io")
require.Equal(t, "something.pinniped.dev.something-else.tuna.io", s)
require.True(t, ok)
// When the replace wasn't actually needed, it still returns true.
s, ok = Unreplace("something.pinniped.dev", "pinniped.dev")
require.Equal(t, "something.pinniped.dev", s)
require.True(t, ok)
}
func TestUnreplaceSuffix(t *testing.T) {
s, ok := Unreplace("something.pinniped.dev.something-else.tuna.io", "tuna.io")
require.Equal(t, "something.pinniped.dev.something-else.pinniped.dev", s)
require.True(t, ok)
// When the unreplace wasn't actually needed, it still returns true.
s, ok = Unreplace("something.pinniped.dev", "pinniped.dev")
require.Equal(t, "something.pinniped.dev", s)
require.True(t, ok)
// When the unreplace was needed but did not work, return false.
s, ok = Unreplace("something.pinniped.dev.something-else.tuna.io", "salmon.io")
require.Equal(t, "", s)
require.False(t, ok)
}
func TestValidate(t *testing.T) {
tests := []struct {
apiGroupSuffix string
wantErrorPrefix string
}{
{
apiGroupSuffix: "happy.suffix.com",
},
{
apiGroupSuffix: "no-dots",
wantErrorPrefix: "1 error(s):\n- must contain '.'",
},
{
apiGroupSuffix: ".starts.with.dot",
wantErrorPrefix: "1 error(s):\n- a lowercase RFC 1123 subdomain must consist",
},
{
apiGroupSuffix: "ends.with.dot.",
wantErrorPrefix: "1 error(s):\n- a lowercase RFC 1123 subdomain must consist",
},
{
apiGroupSuffix: ".multiple-issues.because-this-string-is-longer-than-the-253-character-limit-of-a-dns-1123-identifier-" + chars(253),
wantErrorPrefix: "2 error(s):\n- must be no more than 253 characters\n- a lowercase RFC 1123 subdomain must consist",
},
}
for _, test := range tests {
test := test
t.Run(test.apiGroupSuffix, func(t *testing.T) {
err := Validate(test.apiGroupSuffix)
if test.wantErrorPrefix != "" {
require.Error(t, err)
require.Truef(
t,
strings.HasPrefix(err.Error(), test.wantErrorPrefix),
"%q does not start with %q", err.Error(), test.wantErrorPrefix)
} else {
require.NoError(t, err)
}
})
}
}
func replaceGV(t *testing.T, baseGV schema.GroupVersion, apiGroupSuffix string) string {
t.Helper()
groupName, ok := Replace(baseGV.Group, apiGroupSuffix)
require.True(t, ok, "expected to be able to replace %q's suffix with %q", baseGV.Group, apiGroupSuffix)
return schema.GroupVersion{Group: groupName, Version: baseGV.Version}.String()
}
func chars(count int) string {
return strings.Repeat("a", count)
}

View File

@@ -0,0 +1,70 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"bytes"
"encoding/hex"
"fmt"
"net/url"
"k8s.io/apimachinery/pkg/runtime/schema"
restclient "k8s.io/client-go/rest"
"go.pinniped.dev/internal/plog"
)
// defaultServerUrlFor was copied from k8s.io/client-go/rest/url_utils.go.
//nolint: golint
func defaultServerUrlFor(config *restclient.Config) (*url.URL, string, error) {
hasCA := len(config.CAFile) != 0 || len(config.CAData) != 0
hasCert := len(config.CertFile) != 0 || len(config.CertData) != 0
defaultTLS := hasCA || hasCert || config.Insecure
host := config.Host
if host == "" {
host = "localhost"
}
if config.GroupVersion != nil {
return restclient.DefaultServerURL(host, config.APIPath, *config.GroupVersion, defaultTLS)
}
return restclient.DefaultServerURL(host, config.APIPath, schema.GroupVersion{}, defaultTLS)
}
// truncateBody was copied from k8s.io/client-go/rest/request.go
// ...except i changed klog invocations to analogous plog invocations
//
// truncateBody decides if the body should be truncated, based on the glog Verbosity.
func truncateBody(body string) string {
max := 0
switch {
case plog.Enabled(plog.LevelAll):
return body
case plog.Enabled(plog.LevelTrace):
max = 10240
case plog.Enabled(plog.LevelDebug):
max = 1024
}
if len(body) <= max {
return body
}
return body[:max] + fmt.Sprintf(" [truncated %d chars]", len(body)-max)
}
// glogBody logs a body output that could be either JSON or protobuf. It explicitly guards against
// allocating a new string for the body output unless necessary. Uses a simple heuristic to determine
// whether the body is printable.
func glogBody(prefix string, body []byte) {
if plog.Enabled(plog.LevelDebug) {
if bytes.IndexFunc(body, func(r rune) bool {
return r < 0x0a
}) != -1 {
plog.Debug(prefix, "body", truncateBody(hex.Dump(body)))
} else {
plog.Debug(prefix, "body", truncateBody(string(body)))
}
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func maybeRestoreGVK(serializer runtime.Serializer, respData []byte, result *mutationResult) ([]byte, error) {
if !result.gvkChanged {
return respData, nil
}
// the body could be an API status, random trash or the actual object we want
unknown := &runtime.Unknown{}
_ = runtime.DecodeInto(serializer, respData, unknown) // we do not care about the error
doesNotNeedGVKFix := len(unknown.Raw) == 0 || unknown.GroupVersionKind() != result.newGVK
if doesNotNeedGVKFix {
return respData, nil
}
return restoreGVK(serializer, unknown, result.origGVK)
}
func restoreGVK(encoder runtime.Encoder, unknown *runtime.Unknown, gvk schema.GroupVersionKind) ([]byte, error) {
typeMeta := runtime.TypeMeta{}
typeMeta.APIVersion, typeMeta.Kind = gvk.ToAPIVersionAndKind()
newUnknown := &runtime.Unknown{}
*newUnknown = *unknown
newUnknown.TypeMeta = typeMeta
switch newUnknown.ContentType {
case runtime.ContentTypeJSON:
// json is messy if we want to avoid decoding the whole object
keysOnly := map[string]json.RawMessage{}
// get the keys. this does not preserve order.
if err := json.Unmarshal(newUnknown.Raw, &keysOnly); err != nil {
return nil, fmt.Errorf("failed to unmarshall json keys: %w", err)
}
// turn the type meta into JSON bytes
typeMetaBytes, err := json.Marshal(typeMeta)
if err != nil {
return nil, fmt.Errorf("failed to marshall type meta: %w", err)
}
// overwrite the type meta keys with the new data
if err := json.Unmarshal(typeMetaBytes, &keysOnly); err != nil {
return nil, fmt.Errorf("failed to type meta keys: %w", err)
}
// marshall everything back to bytes
newRaw, err := json.Marshal(keysOnly)
if err != nil {
return nil, fmt.Errorf("failed to marshall new raw: %w", err)
}
// we could just return the bytes but it feels weird to not use the encoder
newUnknown.Raw = newRaw
case runtime.ContentTypeProtobuf:
// protobuf is easy because of the unknown wrapper
// newUnknown.Raw already contains the correct data we need
default:
return nil, fmt.Errorf("unknown content type: %s", newUnknown.ContentType) // this should never happen
}
return runtime.Encode(encoder, newUnknown)
}

View File

@@ -0,0 +1,222 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"io"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func Test_maybeRestoreGVK(t *testing.T) {
type args struct {
unknown *runtime.Unknown
origGVK, newGVK schema.GroupVersionKind
}
tests := []struct {
name string
args args
want runtime.Object
wantChanged bool
wantErr string
}{
{
name: "should update gvk via JSON",
args: args{
unknown: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "new/v1",
Kind: "Tree",
},
Raw: []byte(`{"apiVersion":"new/v1","kind":"Tree","spec":{"pandas":"love"}}`),
ContentType: runtime.ContentTypeJSON,
},
origGVK: schema.GroupVersionKind{
Group: "old",
Version: "v1",
Kind: "Tree",
},
newGVK: schema.GroupVersionKind{
Group: "new",
Version: "v1",
Kind: "Tree",
},
},
want: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "old/v1",
Kind: "Tree",
},
Raw: []byte(`{"apiVersion":"old/v1","kind":"Tree","spec":{"pandas":"love"}}`),
ContentType: runtime.ContentTypeJSON,
},
wantChanged: true,
wantErr: "",
},
{
name: "should update gvk via protobuf",
args: args{
unknown: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "new/v1",
Kind: "Tree",
},
Raw: []byte(`assumed to be valid and does not change`),
ContentType: runtime.ContentTypeProtobuf,
},
origGVK: schema.GroupVersionKind{
Group: "original",
Version: "v1",
Kind: "Tree",
},
newGVK: schema.GroupVersionKind{
Group: "new",
Version: "v1",
Kind: "Tree",
},
},
want: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "original/v1",
Kind: "Tree",
},
Raw: []byte(`assumed to be valid and does not change`),
ContentType: runtime.ContentTypeProtobuf,
},
wantChanged: true,
wantErr: "",
},
{
name: "should ignore because gvk is different",
args: args{
unknown: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "new/v1",
Kind: "Tree",
},
},
newGVK: schema.GroupVersionKind{
Group: "new",
Version: "v1",
Kind: "Forest",
},
},
want: nil,
wantChanged: false,
wantErr: "",
},
{
name: "empty raw is ignored",
args: args{
unknown: &runtime.Unknown{},
},
want: nil,
wantChanged: false,
wantErr: "",
},
{
name: "invalid content type errors",
args: args{
unknown: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "walrus.tld/v1",
Kind: "Seal",
},
Raw: []byte(`data that should be ignored because we do not used YAML`),
ContentType: runtime.ContentTypeYAML,
},
origGVK: schema.GroupVersionKind{
Group: "pinniped.dev",
Version: "v1",
Kind: "Seal",
},
newGVK: schema.GroupVersionKind{
Group: "walrus.tld",
Version: "v1",
Kind: "Seal",
},
},
want: nil,
wantChanged: false,
wantErr: "unknown content type: application/yaml",
},
{
name: "invalid JSON should error",
args: args{
unknown: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "ocean/v1",
Kind: "Water",
},
Raw: []byte(`lol not JSON`),
ContentType: runtime.ContentTypeJSON,
},
origGVK: schema.GroupVersionKind{
Group: "dirt",
Version: "v1",
Kind: "Land",
},
newGVK: schema.GroupVersionKind{
Group: "ocean",
Version: "v1",
Kind: "Water",
},
},
want: nil,
wantChanged: false,
wantErr: "failed to unmarshall json keys: invalid character 'l' looking for beginning of value",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
serializer := &testSerializer{unknown: tt.args.unknown}
respData := []byte(`original`)
result := &mutationResult{origGVK: tt.args.origGVK, newGVK: tt.args.newGVK, gvkChanged: tt.args.origGVK != tt.args.newGVK}
newRespData, err := maybeRestoreGVK(serializer, respData, result)
if len(tt.wantErr) > 0 {
require.EqualError(t, err, tt.wantErr)
require.Nil(t, newRespData)
require.Nil(t, serializer.obj)
return
}
require.NoError(t, err)
if tt.wantChanged {
require.Equal(t, []byte(`changed`), newRespData)
} else {
require.Equal(t, []byte(`original`), newRespData)
}
require.Equal(t, tt.want, serializer.obj)
})
}
}
type testSerializer struct {
unknown *runtime.Unknown
obj runtime.Object
}
func (s *testSerializer) Encode(obj runtime.Object, w io.Writer) error {
s.obj = obj
_, err := w.Write([]byte(`changed`))
return err
}
func (s *testSerializer) Decode(_ []byte, _ *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
u := into.(*runtime.Unknown)
*u = *s.unknown
return u, nil, nil
}
func (s *testSerializer) Identifier() runtime.Identifier {
panic("not called")
}

View File

@@ -6,7 +6,6 @@ package kubeclient
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
kubescheme "k8s.io/client-go/kubernetes/scheme"
@@ -29,12 +28,6 @@ type Client struct {
JSONConfig, ProtoConfig *restclient.Config
}
// TODO expand this interface to address more complex use cases.
type Middleware interface {
Handles(httpMethod string) bool
Mutate(obj metav1.Object) (mutated bool)
}
func New(opts ...Option) (*Client, error) {
c := &clientConfig{}
@@ -58,13 +51,13 @@ func New(opts ...Option) (*Client, error) {
protoKubeConfig := createProtoKubeConfig(c.config)
// Connect to the core Kubernetes API.
k8sClient, err := kubernetes.NewForConfig(configWithWrapper(protoKubeConfig, kubescheme.Codecs, c.middlewares))
k8sClient, err := kubernetes.NewForConfig(configWithWrapper(protoKubeConfig, kubescheme.Scheme, kubescheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize Kubernetes client: %w", err)
}
// Connect to the Kubernetes aggregation API.
aggregatorClient, err := aggregatorclient.NewForConfig(configWithWrapper(protoKubeConfig, aggregatorclientscheme.Codecs, c.middlewares))
aggregatorClient, err := aggregatorclient.NewForConfig(configWithWrapper(protoKubeConfig, aggregatorclientscheme.Scheme, aggregatorclientscheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize aggregation client: %w", err)
}
@@ -72,8 +65,7 @@ func New(opts ...Option) (*Client, error) {
// Connect to the pinniped concierge API.
// We cannot use protobuf encoding here because we are using CRDs
// (for which protobuf encoding is not yet supported).
// TODO we should try to add protobuf support to TokenCredentialRequests since it is an aggregated API
pinnipedConciergeClient, err := pinnipedconciergeclientset.NewForConfig(configWithWrapper(jsonKubeConfig, pinnipedconciergeclientsetscheme.Codecs, c.middlewares))
pinnipedConciergeClient, err := pinnipedconciergeclientset.NewForConfig(configWithWrapper(jsonKubeConfig, pinnipedconciergeclientsetscheme.Scheme, pinnipedconciergeclientsetscheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize pinniped client: %w", err)
}
@@ -81,7 +73,7 @@ func New(opts ...Option) (*Client, error) {
// Connect to the pinniped supervisor API.
// We cannot use protobuf encoding here because we are using CRDs
// (for which protobuf encoding is not yet supported).
pinnipedSupervisorClient, err := pinnipedsupervisorclientset.NewForConfig(configWithWrapper(jsonKubeConfig, pinnipedsupervisorclientsetscheme.Codecs, c.middlewares))
pinnipedSupervisorClient, err := pinnipedsupervisorclientset.NewForConfig(configWithWrapper(jsonKubeConfig, pinnipedsupervisorclientsetscheme.Scheme, pinnipedsupervisorclientsetscheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize pinniped client: %w", err)
}

View File

@@ -0,0 +1,809 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"context"
"fmt"
"io"
"net"
"net/http"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
"go.pinniped.dev/internal/testutil/fakekubeapi"
)
const (
someClusterName = "some cluster name"
)
var (
podGVK = corev1.SchemeGroupVersion.WithKind("Pod")
goodPod = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "good-pod",
Namespace: "good-namespace",
},
}
apiServiceGVK = apiregistrationv1.SchemeGroupVersion.WithKind("APIService")
goodAPIService = &apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{
Name: "good-api-service",
},
}
tokenCredentialRequestGVK = loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest")
goodTokenCredentialRequest = &loginv1alpha1.TokenCredentialRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "good-token-credential-request",
Namespace: "good-namespace",
},
}
federationDomainGVK = configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain")
goodFederationDomain = &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "good-federation-domain",
Namespace: "good-namespace",
},
}
middlewareAnnotations = map[string]string{"some-annotation": "thing 1"}
middlewareLabels = map[string]string{"some-label": "thing 2"}
)
func TestKubeclient(t *testing.T) {
// plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug) // uncomment me to get some more debug logs
tests := []struct {
name string
editRestConfig func(t *testing.T, restConfig *rest.Config)
middlewares func(t *testing.T) []*spyMiddleware
reallyRunTest func(t *testing.T, c *Client)
wantMiddlewareReqs, wantMiddlewareResps [][]Object
}{
{
name: "crud core api",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
},
reallyRunTest: func(t *testing.T, c *Client) {
// create
pod, err := c.Kubernetes.
CoreV1().
Pods(goodPod.Namespace).
Create(context.Background(), goodPod, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, goodPod, pod)
// read
pod, err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Get(context.Background(), pod.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, with(goodPod, annotations(), labels()), pod)
// read when not found
_, err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Get(context.Background(), "this-pod-does-not-exist", metav1.GetOptions{})
require.EqualError(t, err, `couldn't find object for path "/api/v1/namespaces/good-namespace/pods/this-pod-does-not-exist"`)
// update
goodPodWithAnnotationsAndLabelsAndClusterName := with(goodPod, annotations(), labels(), clusterName()).(*corev1.Pod)
pod, err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Update(context.Background(), goodPodWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, goodPodWithAnnotationsAndLabelsAndClusterName, pod)
// delete
err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
require.NoError(t, err)
},
wantMiddlewareReqs: [][]Object{
{
with(goodPod, gvk(podGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
},
{
with(goodPod, annotations(), gvk(podGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
},
},
wantMiddlewareResps: [][]Object{
{
with(goodPod, annotations(), labels(), gvk(podGVK)),
with(goodPod, annotations(), labels(), gvk(podGVK)),
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
},
{
with(goodPod, emptyAnnotations(), labels(), gvk(podGVK)),
with(goodPod, annotations(), labels(), gvk(podGVK)),
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
},
},
},
{
name: "crud core api without middlewares",
reallyRunTest: func(t *testing.T, c *Client) {
// create
pod, err := c.Kubernetes.
CoreV1().
Pods(goodPod.Namespace).
Create(context.Background(), goodPod, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, goodPod, pod)
// read
pod, err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Get(context.Background(), pod.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, with(goodPod), pod)
// update
pod, err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Update(context.Background(), goodPod, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, goodPod, pod)
// delete
err = c.Kubernetes.
CoreV1().
Pods(pod.Namespace).
Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
require.NoError(t, err)
},
},
{
name: "crud aggregation api",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
},
reallyRunTest: func(t *testing.T, c *Client) {
// create
apiService, err := c.Aggregation.
ApiregistrationV1().
APIServices().
Create(context.Background(), goodAPIService, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, goodAPIService, apiService)
// read
apiService, err = c.Aggregation.
ApiregistrationV1().
APIServices().
Get(context.Background(), apiService.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, with(goodAPIService, annotations(), labels()), apiService)
// update
goodAPIServiceWithAnnotationsAndLabelsAndClusterName := with(goodAPIService, annotations(), labels(), clusterName()).(*apiregistrationv1.APIService)
apiService, err = c.Aggregation.
ApiregistrationV1().
APIServices().
Update(context.Background(), goodAPIServiceWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, goodAPIServiceWithAnnotationsAndLabelsAndClusterName, apiService)
// delete
err = c.Aggregation.
ApiregistrationV1().
APIServices().
Delete(context.Background(), apiService.Name, metav1.DeleteOptions{})
require.NoError(t, err)
},
wantMiddlewareReqs: [][]Object{
{
with(goodAPIService, gvk(apiServiceGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
},
{
with(goodAPIService, annotations(), gvk(apiServiceGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
},
},
wantMiddlewareResps: [][]Object{
{
with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)),
with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)),
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
},
{
with(goodAPIService, emptyAnnotations(), labels(), gvk(apiServiceGVK)),
with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)),
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
},
},
},
{
name: "crud concierge api",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
},
reallyRunTest: func(t *testing.T, c *Client) {
// create
tokenCredentialRequest, err := c.PinnipedConcierge.
LoginV1alpha1().
TokenCredentialRequests(goodTokenCredentialRequest.Namespace).
Create(context.Background(), goodTokenCredentialRequest, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, goodTokenCredentialRequest, tokenCredentialRequest)
// read
tokenCredentialRequest, err = c.PinnipedConcierge.
LoginV1alpha1().
TokenCredentialRequests(tokenCredentialRequest.Namespace).
Get(context.Background(), tokenCredentialRequest.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, with(goodTokenCredentialRequest, annotations(), labels()), tokenCredentialRequest)
// update
goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName := with(goodTokenCredentialRequest, annotations(), labels(), clusterName()).(*loginv1alpha1.TokenCredentialRequest)
tokenCredentialRequest, err = c.PinnipedConcierge.
LoginV1alpha1().
TokenCredentialRequests(tokenCredentialRequest.Namespace).
Update(context.Background(), goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName, tokenCredentialRequest)
// delete
err = c.PinnipedConcierge.
LoginV1alpha1().
TokenCredentialRequests(tokenCredentialRequest.Namespace).
Delete(context.Background(), tokenCredentialRequest.Name, metav1.DeleteOptions{})
require.NoError(t, err)
},
wantMiddlewareReqs: [][]Object{
{
with(goodTokenCredentialRequest, gvk(tokenCredentialRequestGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
},
{
with(goodTokenCredentialRequest, annotations(), gvk(tokenCredentialRequestGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
},
},
wantMiddlewareResps: [][]Object{
{
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
},
{
with(goodTokenCredentialRequest, emptyAnnotations(), labels(), gvk(tokenCredentialRequestGVK)),
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
},
},
},
{
name: "crud supervisor api",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
},
reallyRunTest: func(t *testing.T, c *Client) {
// create
federationDomain, err := c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, goodFederationDomain, federationDomain)
// read
federationDomain, err = c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(federationDomain.Namespace).
Get(context.Background(), federationDomain.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, with(goodFederationDomain, annotations(), labels()), federationDomain)
// update
goodFederationDomainWithAnnotationsAndLabelsAndClusterName := with(goodFederationDomain, annotations(), labels(), clusterName()).(*configv1alpha1.FederationDomain)
federationDomain, err = c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(federationDomain.Namespace).
Update(context.Background(), goodFederationDomainWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, goodFederationDomainWithAnnotationsAndLabelsAndClusterName, federationDomain)
// delete
err = c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(federationDomain.Namespace).
Delete(context.Background(), federationDomain.Name, metav1.DeleteOptions{})
require.NoError(t, err)
},
wantMiddlewareReqs: [][]Object{
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, annotations(), gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
},
wantMiddlewareResps: [][]Object{
{
with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)),
with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)),
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, emptyAnnotations(), labels(), gvk(federationDomainGVK)),
with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)),
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
},
},
},
{
name: "we don't call any middleware if there are no mutation funcs",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newSimpleMiddleware(t, false, false, false), newSimpleMiddleware(t, false, false, false)}
},
reallyRunTest: createGetFederationDomainTest,
wantMiddlewareReqs: [][]Object{nil, nil},
wantMiddlewareResps: [][]Object{nil, nil},
},
{
name: "we don't call any resp middleware if there was no req mutations done and there are no resp mutation funcs",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newSimpleMiddleware(t, true, false, false), newSimpleMiddleware(t, true, false, false)}
},
reallyRunTest: createGetFederationDomainTest,
wantMiddlewareReqs: [][]Object{
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
},
wantMiddlewareResps: [][]Object{nil, nil},
},
{
name: "we don't call any resp middleware if there are no resp mutation funcs even if there was req mutations done",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newSimpleMiddleware(t, true, true, false), newSimpleMiddleware(t, true, true, false)}
},
reallyRunTest: func(t *testing.T, c *Client) {
// create
federationDomain, err := c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, with(goodFederationDomain, clusterName()), federationDomain)
// read
federationDomain, err = c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(federationDomain.Namespace).
Get(context.Background(), federationDomain.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, with(goodFederationDomain, clusterName()), federationDomain)
},
wantMiddlewareReqs: [][]Object{
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, clusterName(), gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
},
wantMiddlewareResps: [][]Object{nil, nil},
},
{
name: "we still call resp middleware if there is a resp mutation func even if there were req mutation funcs",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newSimpleMiddleware(t, false, false, true), newSimpleMiddleware(t, false, false, true)}
},
reallyRunTest: createGetFederationDomainTest,
wantMiddlewareReqs: [][]Object{nil, nil},
wantMiddlewareResps: [][]Object{
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(goodFederationDomain, gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(goodFederationDomain, gvk(federationDomainGVK)),
},
},
},
{
name: "we still call resp middleware if there is a resp mutation func even if there was no req mutation",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newSimpleMiddleware(t, true, false, true), newSimpleMiddleware(t, true, false, true)}
},
reallyRunTest: createGetFederationDomainTest,
wantMiddlewareReqs: [][]Object{
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
},
},
wantMiddlewareResps: [][]Object{
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(goodFederationDomain, gvk(federationDomainGVK)),
},
{
with(goodFederationDomain, gvk(federationDomainGVK)),
with(goodFederationDomain, gvk(federationDomainGVK)),
},
},
},
{
name: "mutating object meta on a get request is not allowed since that isn't pertinent to the api request",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{{
name: "non-pertinent mutater",
t: t,
mutateReq: func(rt RoundTrip, obj Object) {
clusterName()(obj)
},
}}
},
reallyRunTest: func(t *testing.T, c *Client) {
_, err := c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Get(context.Background(), goodFederationDomain.Name, metav1.GetOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid object meta mutation")
},
wantMiddlewareReqs: [][]Object{{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))}},
wantMiddlewareResps: [][]Object{nil},
},
{
name: "when the client gets errors from the api server",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{newSimpleMiddleware(t, true, false, false)}
},
editRestConfig: func(t *testing.T, restConfig *rest.Config) {
restConfig.Dial = func(_ context.Context, _, _ string) (net.Conn, error) {
return nil, fmt.Errorf("some fake connection error")
}
},
reallyRunTest: func(t *testing.T, c *Client) {
_, err := c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Get(context.Background(), goodFederationDomain.Name, metav1.GetOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), ": some fake connection error")
},
wantMiddlewareReqs: [][]Object{{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))}},
wantMiddlewareResps: [][]Object{nil},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
server, restConfig := fakekubeapi.Start(t, nil)
defer server.Close()
if test.editRestConfig != nil {
test.editRestConfig(t, restConfig)
}
var middlewares []*spyMiddleware
if test.middlewares != nil {
middlewares = test.middlewares(t)
}
// our rt chain is:
// wantCloseReq -> kubeclient -> wantCloseResp -> http.DefaultTransport -> wantCloseResp -> kubeclient -> wantCloseReq
restConfig.Wrap(wantCloseRespWrapper(t))
opts := []Option{WithConfig(restConfig), WithTransportWrapper(wantCloseReqWrapper(t))}
for _, middleware := range middlewares {
opts = append(opts, WithMiddleware(middleware))
}
client, err := New(opts...)
require.NoError(t, err)
test.reallyRunTest(t, client)
for i, spyMiddleware := range middlewares {
require.Equalf(t, test.wantMiddlewareReqs[i], spyMiddleware.reqObjs, "unexpected req obj in middleware %q (index %d)", spyMiddleware.name, i)
require.Equalf(t, test.wantMiddlewareResps[i], spyMiddleware.respObjs, "unexpected resp obj in middleware %q (index %d)", spyMiddleware.name, i)
}
})
}
}
type spyMiddleware struct {
name string
t *testing.T
mutateReq func(RoundTrip, Object)
mutateResp func(RoundTrip, Object)
reqObjs []Object
respObjs []Object
}
func (s *spyMiddleware) Handle(_ context.Context, rt RoundTrip) {
s.t.Log(s.name, "handling", reqStr(rt, nil))
if s.mutateReq != nil {
rt.MutateRequest(func(obj Object) {
s.t.Log(s.name, "mutating request", reqStr(rt, obj))
s.reqObjs = append(s.reqObjs, obj.DeepCopyObject().(Object))
s.mutateReq(rt, obj)
})
}
if s.mutateResp != nil {
rt.MutateResponse(func(obj Object) {
s.t.Log(s.name, "mutating response", reqStr(rt, obj))
s.respObjs = append(s.respObjs, obj.DeepCopyObject().(Object))
s.mutateResp(rt, obj)
})
}
}
func reqStr(rt RoundTrip, obj Object) string {
b := strings.Builder{}
fmt.Fprintf(&b, "%s /%s", rt.Verb(), rt.Resource().GroupVersion())
if rt.NamespaceScoped() {
fmt.Fprintf(&b, "/namespaces/%s", rt.Namespace())
}
fmt.Fprintf(&b, "/%s", rt.Resource().Resource)
if obj != nil {
fmt.Fprintf(&b, "/%s", obj.GetName())
}
return b.String()
}
func newAnnotationMiddleware(t *testing.T) *spyMiddleware {
return &spyMiddleware{
name: "annotater",
t: t,
mutateReq: func(rt RoundTrip, obj Object) {
if rt.Verb() == VerbCreate {
annotations()(obj)
}
},
mutateResp: func(rt RoundTrip, obj Object) {
if rt.Verb() == VerbCreate {
for key := range middlewareAnnotations {
delete(obj.GetAnnotations(), key)
}
}
},
}
}
func newLabelMiddleware(t *testing.T) *spyMiddleware {
return &spyMiddleware{
name: "labeler",
t: t,
mutateReq: func(rt RoundTrip, obj Object) {
if rt.Verb() == VerbCreate {
labels()(obj)
}
},
mutateResp: func(rt RoundTrip, obj Object) {
if rt.Verb() == VerbCreate {
for key := range middlewareLabels {
delete(obj.GetLabels(), key)
}
}
},
}
}
func newSimpleMiddleware(t *testing.T, hasMutateReqFunc, mutatedReq, hasMutateRespFunc bool) *spyMiddleware {
m := &spyMiddleware{
name: "nop",
t: t,
}
if hasMutateReqFunc {
m.mutateReq = func(rt RoundTrip, obj Object) {
if mutatedReq {
if rt.Verb() == VerbCreate {
obj.SetClusterName(someClusterName)
}
}
}
}
if hasMutateRespFunc {
m.mutateResp = func(rt RoundTrip, obj Object) {}
}
return m
}
type wantCloser struct {
io.ReadCloser
closeCount int
closeCalls []string
couldReadBytesJustBeforeClosing bool
}
func (wc *wantCloser) Close() error {
wc.closeCount++
wc.closeCalls = append(wc.closeCalls, getCaller())
n, _ := wc.ReadCloser.Read([]byte{0})
if n > 0 {
// there were still bytes left to be read
wc.couldReadBytesJustBeforeClosing = true
}
return wc.ReadCloser.Close()
}
func getCaller() string {
_, file, line, ok := runtime.Caller(2)
if !ok {
file = "???"
line = 0
}
return fmt.Sprintf("%s:%d", file, line)
}
// wantCloseReqWrapper returns a transport.WrapperFunc that validates that the http.Request
// passed to the underlying http.RoundTripper is closed properly.
func wantCloseReqWrapper(t *testing.T) transport.WrapperFunc {
caller := getCaller()
return func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (bool, *http.Response, error) {
if req.Body != nil {
wc := &wantCloser{ReadCloser: req.Body}
t.Cleanup(func() {
require.Equalf(t, wc.closeCount, 1, "did not close req body expected number of times at %s for req %#v; actual calls = %s", caller, req, wc.closeCalls)
})
req.Body = wc
}
if req.GetBody != nil {
originalBodyCopy, originalErr := req.GetBody()
req.GetBody = func() (io.ReadCloser, error) {
if originalErr != nil {
return nil, originalErr
}
wc := &wantCloser{ReadCloser: originalBodyCopy}
t.Cleanup(func() {
require.Equalf(t, wc.closeCount, 1, "did not close req body copy expected number of times at %s for req %#v; actual calls = %s", caller, req, wc.closeCalls)
})
return wc, nil
}
}
resp, err := rt.RoundTrip(req)
return false, resp, err
})
}
}
// wantCloseRespWrapper returns a transport.WrapperFunc that validates that the http.Response
// returned by the underlying http.RoundTripper is closed properly.
func wantCloseRespWrapper(t *testing.T) transport.WrapperFunc {
caller := getCaller()
return func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (bool, *http.Response, error) {
resp, err := rt.RoundTrip(req)
if err != nil {
// request failed, so there is no response body to watch for Close() calls on
return false, resp, err
}
wc := &wantCloser{ReadCloser: resp.Body}
t.Cleanup(func() {
require.False(t, wc.couldReadBytesJustBeforeClosing, "did not consume all response body bytes before closing %s", caller)
require.Equalf(t, wc.closeCount, 1, "did not close resp body expected number of times at %s for req %#v; actual calls = %s", caller, req, wc.closeCalls)
})
resp.Body = wc
return false, resp, err
})
}
}
type withFunc func(obj Object)
func with(obj Object, withFuncs ...withFunc) Object {
obj = obj.DeepCopyObject().(Object)
for _, withFunc := range withFuncs {
withFunc(obj)
}
return obj
}
func gvk(gvk schema.GroupVersionKind) withFunc {
return func(obj Object) {
obj.GetObjectKind().SetGroupVersionKind(gvk)
}
}
func annotations() withFunc {
return func(obj Object) {
obj.SetAnnotations(middlewareAnnotations)
}
}
func emptyAnnotations() withFunc {
return func(obj Object) {
obj.SetAnnotations(make(map[string]string))
}
}
func labels() withFunc {
return func(obj Object) {
obj.SetLabels(middlewareLabels)
}
}
func clusterName() withFunc {
return func(obj Object) {
obj.SetClusterName(someClusterName)
}
}
func createGetFederationDomainTest(t *testing.T, client *Client) {
t.Helper()
// create
federationDomain, err := client.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, goodFederationDomain, federationDomain)
// read
federationDomain, err = client.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(federationDomain.Namespace).
Get(context.Background(), federationDomain.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, goodFederationDomain, federationDomain)
}

View File

@@ -0,0 +1,147 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type Middleware interface {
Handle(ctx context.Context, rt RoundTrip)
}
var _ Middleware = MiddlewareFunc(nil)
type MiddlewareFunc func(ctx context.Context, rt RoundTrip)
func (f MiddlewareFunc) Handle(ctx context.Context, rt RoundTrip) {
f(ctx, rt)
}
var _ Middleware = Middlewares{}
type Middlewares []Middleware
func (m Middlewares) Handle(ctx context.Context, rt RoundTrip) {
for _, middleware := range m {
middleware := middleware
middleware.Handle(ctx, rt)
}
}
type RoundTrip interface {
Verb() Verb
Namespace() string // this is the only valid way to check namespace, Object.GetNamespace() will almost always be empty
NamespaceScoped() bool
Resource() schema.GroupVersionResource
Subresource() string
MutateRequest(f func(obj Object))
MutateResponse(f func(obj Object))
}
type Object interface {
runtime.Object // generic access to TypeMeta
metav1.Object // generic access to ObjectMeta
}
var _ RoundTrip = &request{}
type request struct {
verb Verb
namespace string
resource schema.GroupVersionResource
reqFuncs, respFuncs []func(obj Object)
subresource string
}
func (r *request) Verb() Verb {
return r.verb
}
func (r *request) Namespace() string {
return r.namespace
}
//nolint: gochecknoglobals
var namespaceGVR = corev1.SchemeGroupVersion.WithResource("namespaces")
func (r *request) NamespaceScoped() bool {
if r.Resource() == namespaceGVR {
return false // always consider namespaces to be cluster scoped
}
return len(r.Namespace()) != 0
}
func (r *request) Resource() schema.GroupVersionResource {
return r.resource
}
func (r *request) Subresource() string {
return r.subresource
}
func (r *request) MutateRequest(f func(obj Object)) {
r.reqFuncs = append(r.reqFuncs, f)
}
func (r *request) MutateResponse(f func(obj Object)) {
r.respFuncs = append(r.respFuncs, f)
}
type mutationResult struct {
origGVK, newGVK schema.GroupVersionKind
gvkChanged, mutated bool
}
func (r *request) mutateRequest(obj Object) (*mutationResult, error) {
origGVK := obj.GetObjectKind().GroupVersionKind()
if origGVK.Empty() {
return nil, fmt.Errorf("invalid empty orig GVK for %T: %#v", obj, r)
}
origObj, ok := obj.DeepCopyObject().(Object)
if !ok {
return nil, fmt.Errorf("invalid deep copy semantics for %T: %#v", obj, r)
}
for _, reqFunc := range r.reqFuncs {
reqFunc := reqFunc
reqFunc(obj)
}
newGVK := obj.GetObjectKind().GroupVersionKind()
if newGVK.Empty() {
return nil, fmt.Errorf("invalid empty new GVK for %T: %#v", obj, r)
}
return &mutationResult{
origGVK: origGVK,
newGVK: newGVK,
gvkChanged: origGVK != newGVK,
mutated: len(r.respFuncs) != 0 || !apiequality.Semantic.DeepEqual(origObj, obj),
}, nil
}
func (r *request) mutateResponse(obj Object) (bool, error) {
origObj, ok := obj.DeepCopyObject().(Object)
if !ok {
return false, fmt.Errorf("invalid deep copy semantics for %T: %#v", obj, r)
}
for _, respFunc := range r.respFuncs {
respFunc := respFunc
respFunc(obj)
}
mutated := !apiequality.Semantic.DeepEqual(origObj, obj)
return mutated, nil
}

View File

@@ -0,0 +1,74 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func Test_request_mutate(t *testing.T) {
tests := []struct {
name string
reqFuncs []func(Object)
obj Object
want *mutationResult
wantObj Object
wantErr string
}{
{
name: "mutate config map data",
reqFuncs: []func(Object){
func(obj Object) {
cm := obj.(*corev1.ConfigMap)
cm.Data = map[string]string{"new": "stuff"}
},
},
obj: &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
Data: map[string]string{"old": "things"},
BinaryData: map[string][]byte{"weee": nil},
},
want: &mutationResult{
origGVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
newGVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
gvkChanged: false,
mutated: true,
},
wantObj: &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
Data: map[string]string{"new": "stuff"},
BinaryData: map[string][]byte{"weee": nil},
},
wantErr: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
r := &request{reqFuncs: tt.reqFuncs}
orig := tt.obj.DeepCopyObject()
got, err := r.mutateRequest(tt.obj)
if len(tt.wantErr) > 0 {
require.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
if tt.wantObj != nil {
require.Equal(t, tt.wantObj, tt.obj)
} else {
require.Equal(t, orig, tt.obj)
}
})
}
}

View File

@@ -3,13 +3,17 @@
package kubeclient
import restclient "k8s.io/client-go/rest"
import (
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport"
)
type Option func(*clientConfig)
type clientConfig struct {
config *restclient.Config
middlewares []Middleware
config *restclient.Config
middlewares []Middleware
transportWrapper transport.WrapperFunc
}
func WithConfig(config *restclient.Config) Option {
@@ -20,6 +24,19 @@ func WithConfig(config *restclient.Config) Option {
func WithMiddleware(middleware Middleware) Option {
return func(c *clientConfig) {
if middleware == nil {
return // support passing in a nil middleware as a no-op
}
c.middlewares = append(c.middlewares, middleware)
}
}
// WithTransportWrapper will wrap the client-go http.RoundTripper chain *after* the middleware
// wrapper is applied. I.e., this wrapper has the opportunity to supply an http.RoundTripper that
// runs first in the client-go http.RoundTripper chain.
func WithTransportWrapper(wrapper transport.WrapperFunc) Option {
return func(c *clientConfig) {
c.transportWrapper = wrapper
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"fmt"
"net/http"
"net/url"
"path"
"strings"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
restclient "k8s.io/client-go/rest"
)
func updatePathNewGVK(reqURL *url.URL, result *mutationResult, apiPathPrefix string, reqInfo *genericapirequest.RequestInfo) (*url.URL, error) {
if !result.gvkChanged {
return reqURL, nil
}
if len(result.origGVK.Group) == 0 {
return nil, fmt.Errorf("invalid attempt to change core group")
}
newURL := &url.URL{}
*newURL = *reqURL
// replace old GVK with new GVK
apiRoot := path.Join(apiPathPrefix, reqInfo.APIPrefix)
oldPrefix := restclient.DefaultVersionedAPIPath(apiRoot, result.origGVK.GroupVersion())
newPrefix := restclient.DefaultVersionedAPIPath(apiRoot, result.newGVK.GroupVersion())
newURL.Path = path.Join(newPrefix, strings.TrimPrefix(newURL.Path, oldPrefix))
return newURL, nil
}
func getHostAndAPIPathPrefix(config *restclient.Config) (string, string, error) {
hostURL, _, err := defaultServerUrlFor(config)
if err != nil {
return "", "", fmt.Errorf("failed to parse host URL from rest config: %w", err)
}
return hostURL.String(), hostURL.Path, nil
}
func reqWithoutPrefix(req *http.Request, hostURL, apiPathPrefix string) *http.Request {
if len(apiPathPrefix) == 0 {
return req
}
if !strings.HasSuffix(hostURL, "/") {
hostURL += "/"
}
if !strings.HasPrefix(req.URL.String(), hostURL) {
return req
}
if !strings.HasPrefix(apiPathPrefix, "/") {
apiPathPrefix = "/" + apiPathPrefix
}
if !strings.HasSuffix(apiPathPrefix, "/") {
apiPathPrefix += "/"
}
reqCopy := req.WithContext(req.Context())
urlCopy := &url.URL{}
*urlCopy = *reqCopy.URL
urlCopy.Path = "/" + strings.TrimPrefix(urlCopy.Path, apiPathPrefix)
reqCopy.URL = urlCopy
return reqCopy
}

View File

@@ -0,0 +1,231 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
)
func Test_updatePathNewGVK(t *testing.T) {
type args struct {
reqURL *url.URL
result *mutationResult
apiPathPrefix string
reqInfo *genericapirequest.RequestInfo
}
tests := []struct {
name string
args args
want *url.URL
wantErr bool
}{
{
name: "no gvk change",
args: args{
reqURL: mustParse(t, "https://walrus.tld/api/v1/pods"),
result: &mutationResult{},
},
want: mustParse(t, "https://walrus.tld/api/v1/pods"),
},
{
name: "no original gvk group",
args: args{
result: &mutationResult{
origGVK: schema.GroupVersionKind{
Group: "",
},
gvkChanged: true,
},
},
wantErr: true,
},
{
name: "cluster-scoped list path",
args: args{
reqURL: mustParse(t, "https://walrus.tld/apis/"+loginv1alpha1.SchemeGroupVersion.String()+"/tokencredentialrequests"),
result: &mutationResult{
origGVK: loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest"),
newGVK: schema.GroupVersionKind{
Group: "login.concierge.tuna.io",
Version: loginv1alpha1.SchemeGroupVersion.Version,
Kind: "TokenCredentialRequest",
},
gvkChanged: true,
},
apiPathPrefix: "/apis",
reqInfo: &genericapirequest.RequestInfo{},
},
want: mustParse(t, "https://walrus.tld/apis/login.concierge.tuna.io/v1alpha1/tokencredentialrequests"),
},
{
name: "cluster-scoped get path",
args: args{
reqURL: mustParse(t, "https://walrus.tld/apis/"+loginv1alpha1.SchemeGroupVersion.String()+"/tokencredentialrequests/some-name"),
result: &mutationResult{
origGVK: loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest"),
newGVK: schema.GroupVersionKind{
Group: "login.concierge.tuna.io",
Version: loginv1alpha1.SchemeGroupVersion.Version,
Kind: "TokenCredentialRequest",
},
gvkChanged: true,
},
apiPathPrefix: "/apis",
reqInfo: &genericapirequest.RequestInfo{},
},
want: mustParse(t, "https://walrus.tld/apis/login.concierge.tuna.io/v1alpha1/tokencredentialrequests/some-name"),
},
{
name: "namespace-scoped list path",
args: args{
reqURL: mustParse(t, "https://walrus.tld/apis/"+configv1alpha1.SchemeGroupVersion.String()+"/namespaces/default/federationdomains"),
result: &mutationResult{
origGVK: configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain"),
newGVK: schema.GroupVersionKind{
Group: "config.supervisor.tuna.io",
Version: configv1alpha1.SchemeGroupVersion.Version,
Kind: "FederationDomain",
},
gvkChanged: true,
},
apiPathPrefix: "/apis",
reqInfo: &genericapirequest.RequestInfo{},
},
want: mustParse(t, "https://walrus.tld/apis/config.supervisor.tuna.io/v1alpha1/namespaces/default/federationdomains"),
},
{
name: "namespace-scoped get path",
args: args{
reqURL: mustParse(t, "https://walrus.tld/apis/"+configv1alpha1.SchemeGroupVersion.String()+"/namespaces/default/federationdomains/some-name"),
result: &mutationResult{
origGVK: configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain"),
newGVK: schema.GroupVersionKind{
Group: "config.supervisor.tuna.io",
Version: configv1alpha1.SchemeGroupVersion.Version,
Kind: "FederationDomain",
},
gvkChanged: true,
},
apiPathPrefix: "/apis",
reqInfo: &genericapirequest.RequestInfo{},
},
want: mustParse(t, "https://walrus.tld/apis/config.supervisor.tuna.io/v1alpha1/namespaces/default/federationdomains/some-name"),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := updatePathNewGVK(tt.args.reqURL, tt.args.result, tt.args.apiPathPrefix, tt.args.reqInfo)
if (err != nil) != tt.wantErr {
t.Errorf("updatePathNewGVK() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("updatePathNewGVK() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_reqWithoutPrefix(t *testing.T) {
body := ioutil.NopCloser(bytes.NewBuffer([]byte("some body")))
newReq := func(rawurl string) *http.Request {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawurl, body)
require.NoError(t, err)
return req
}
type args struct {
req *http.Request
hostURL string
apiPathPrefix string
}
tests := []struct {
name string
args args
want *http.Request
}{
{
name: "happy path",
args: args{
req: newReq("https://walrus.tld/apis/some/path"),
hostURL: "https://walrus.tld",
apiPathPrefix: "/apis",
},
want: newReq("https://walrus.tld/some/path"),
},
{
name: "host url already has slash suffix",
args: args{
req: newReq("https://walrus.tld/apis/some/path"),
hostURL: "https://walrus.tld/",
apiPathPrefix: "/apis",
},
want: newReq("https://walrus.tld/some/path"),
},
{
name: "api prefix already has slash prefix",
args: args{
req: newReq("https://walrus.tld/apis/some/path"),
hostURL: "https://walrus.tld",
apiPathPrefix: "apis",
},
want: newReq("https://walrus.tld/some/path"),
},
{
name: "api prefix already has slash suffix",
args: args{
req: newReq("https://walrus.tld/apis/some/path"),
hostURL: "https://walrus.tld",
apiPathPrefix: "/apis/",
},
want: newReq("https://walrus.tld/some/path"),
},
{
name: "no api path prefix",
args: args{
req: newReq("https://walrus.tld"),
},
want: newReq("https://walrus.tld"),
},
{
name: "hostURL and req URL mismatch",
args: args{
req: newReq("https://walrus.tld.some-other-url/some/path"),
hostURL: "https://walrus.tld",
apiPathPrefix: "/apis",
},
want: newReq("https://walrus.tld.some-other-url/some/path"),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
req := *tt.args.req
if got := reqWithoutPrefix(&req, tt.args.hostURL, tt.args.apiPathPrefix); !reflect.DeepEqual(got, tt.want) {
t.Errorf("reqWithoutPrefix() = %v, want %v", got, tt.want)
}
})
}
}
func mustParse(t *testing.T, rawurl string) *url.URL {
t.Helper()
url, err := url.Parse(rawurl)
require.NoError(t, err)
return url
}

View File

@@ -9,14 +9,27 @@ import (
"io/ioutil"
"net/http"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/server"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"go.pinniped.dev/internal/plog"
)
// TODO unit test
func configWithWrapper(config *restclient.Config, scheme *runtime.Scheme, negotiatedSerializer runtime.NegotiatedSerializer, middlewares []Middleware, wrapper transport.WrapperFunc) *restclient.Config {
hostURL, apiPathPrefix, err := getHostAndAPIPathPrefix(config)
if err != nil {
plog.DebugErr("invalid rest config", err)
return config // invalid input config, will fail existing client-go validation
}
func configWithWrapper(config *restclient.Config, negotiatedSerializer runtime.NegotiatedSerializer, middlewares []Middleware) *restclient.Config {
// no need for any wrapping when we have no middleware to inject
if len(middlewares) == 0 {
return config
@@ -26,97 +39,349 @@ func configWithWrapper(config *restclient.Config, negotiatedSerializer runtime.N
if !ok {
panic(fmt.Errorf("unknown content type: %s ", config.ContentType)) // static input, programmer error
}
serializer := info.Serializer // should perform no conversion
regSerializer := info.Serializer // should perform no conversion
f := func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
// ignore everything that has an unreadable body
if req.GetBody == nil {
return rt.RoundTrip(req)
}
resolver := server.NewRequestInfoResolver(server.NewConfig(serializer.CodecFactory{}))
var reqMiddlewares []Middleware
for _, middleware := range middlewares {
middleware := middleware
if middleware.Handles(req.Method) {
reqMiddlewares = append(reqMiddlewares, middleware)
}
}
schemeRestMapperFunc := schemeRestMapper(scheme)
// no middleware to handle this request
if len(reqMiddlewares) == 0 {
return rt.RoundTrip(req)
}
body, err := req.GetBody()
if err != nil {
return nil, fmt.Errorf("get body failed: %w", err)
}
defer body.Close()
data, err := ioutil.ReadAll(body)
if err != nil {
return nil, fmt.Errorf("read body failed: %w", err)
}
// attempt to decode with no defaults or into specified, i.e. defer to the decoder
// this should result in the a straight decode with no conversion
obj, _, err := serializer.Decode(data, nil, nil)
if err != nil {
return nil, fmt.Errorf("body decode failed: %w", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
return rt.RoundTrip(req) // ignore everything that has no object meta for now
}
// run all the mutating operations
var reqMutated bool
for _, reqMiddleware := range reqMiddlewares {
mutated := reqMiddleware.Mutate(accessor)
reqMutated = mutated || reqMutated
}
// no mutation occurred, keep the original request
if !reqMutated {
return rt.RoundTrip(req)
}
// we plan on making a new request so make sure to close the original request's body
_ = req.Body.Close()
newData, err := runtime.Encode(serializer, obj)
if err != nil {
return nil, fmt.Errorf("new body encode failed: %w", err)
}
// TODO log newData at high loglevel similar to REST client
// simplest way to reuse the body creation logic
newReqForBody, err := http.NewRequest(req.Method, req.URL.String(), bytes.NewReader(newData))
if err != nil {
return nil, fmt.Errorf("failed to create new req for body: %w", err) // this should never happen
}
// shallow copy because we want to preserve all the headers and such but not mutate the original request
newReq := req.WithContext(req.Context())
// replace the body with the new data
newReq.ContentLength = newReqForBody.ContentLength
newReq.Body = newReqForBody.Body
newReq.GetBody = newReqForBody.GetBody
return rt.RoundTrip(newReq)
})
}
f := newWrapper(hostURL, apiPathPrefix, config, resolver, regSerializer, negotiatedSerializer, schemeRestMapperFunc, middlewares)
cc := restclient.CopyConfig(config)
cc.Wrap(f)
if wrapper != nil {
cc.Wrap(wrapper)
}
return cc
}
type roundTripperFunc func(req *http.Request) (*http.Response, error)
type roundTripperFunc func(req *http.Request) (bool, *http.Response, error)
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
// always attempt to close the body, as long as we are the ones that handled the request
// see http.RoundTripper doc:
// "RoundTrip must always close the body, including on errors, ..."
handled, resp, err := f(req)
if handled && req.Body != nil {
_ = req.Body.Close()
}
return resp, err
}
func newWrapper(
hostURL, apiPathPrefix string,
config *restclient.Config,
resolver genericapirequest.RequestInfoResolver,
regSerializer runtime.Serializer,
negotiatedSerializer runtime.NegotiatedSerializer,
schemeRestMapperFunc func(schema.GroupVersionResource, Verb) (schema.GroupVersionKind, bool),
middlewares []Middleware,
) transport.WrapperFunc {
return func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (bool, *http.Response, error) {
reqInfo, err := resolver.NewRequestInfo(reqWithoutPrefix(req, hostURL, apiPathPrefix))
if err != nil || !reqInfo.IsResourceRequest {
resp, err := rt.RoundTrip(req) // we only handle kube resource requests
return false, resp, err
}
middlewareReq := &request{
verb: verb(reqInfo.Verb),
namespace: reqInfo.Namespace,
resource: schema.GroupVersionResource{
Group: reqInfo.APIGroup,
Version: reqInfo.APIVersion,
Resource: reqInfo.Resource,
},
subresource: reqInfo.Subresource,
}
for _, middleware := range middlewares {
middleware := middleware
middleware.Handle(req.Context(), middlewareReq)
}
if len(middlewareReq.reqFuncs) == 0 && len(middlewareReq.respFuncs) == 0 {
resp, err := rt.RoundTrip(req) // no middleware wanted to mutate this request
return false, resp, err
}
switch v := middlewareReq.Verb(); v {
case VerbCreate, VerbUpdate:
return handleCreateOrUpdate(req, middlewareReq, regSerializer, rt, apiPathPrefix, reqInfo, config, negotiatedSerializer)
case VerbGet, VerbList, VerbDelete, VerbDeleteCollection, VerbPatch, VerbWatch:
return handleOtherVerbs(v, req, middlewareReq, schemeRestMapperFunc, rt, apiPathPrefix, reqInfo, config, negotiatedSerializer)
case VerbProxy: // for now we do not support proxy interception
fallthrough
default:
resp, err := rt.RoundTrip(req) // we only handle certain verbs
return false, resp, err
}
})
}
}
func handleOtherVerbs(
v Verb,
req *http.Request,
middlewareReq *request,
schemeRestMapperFunc func(schema.GroupVersionResource, Verb) (schema.GroupVersionKind, bool),
rt http.RoundTripper,
apiPathPrefix string,
reqInfo *genericapirequest.RequestInfo,
config *restclient.Config,
negotiatedSerializer runtime.NegotiatedSerializer,
) (bool, *http.Response, error) {
mapperGVK, ok := schemeRestMapperFunc(middlewareReq.Resource(), v)
if !ok {
return true, nil, fmt.Errorf("unable to determine GVK for middleware request %#v", middlewareReq)
}
// no need to do anything with object meta since we only support GVK changes
obj := &metav1.PartialObjectMetadata{}
obj.APIVersion, obj.Kind = mapperGVK.ToAPIVersionAndKind()
result, err := middlewareReq.mutateRequest(obj)
if err != nil {
return true, nil, err
}
if !result.mutated {
resp, err := rt.RoundTrip(req) // no middleware mutated the request
return false, resp, err
}
// sanity check to make sure mutation is to type meta and/or the response
unexpectedMutation := len(middlewareReq.respFuncs) == 0 && !result.gvkChanged
metaIsZero := apiequality.Semantic.DeepEqual(obj.ObjectMeta, metav1.ObjectMeta{})
if unexpectedMutation || !metaIsZero {
return true, nil, fmt.Errorf("invalid object meta mutation: %#v", middlewareReq)
}
reqURL, err := updatePathNewGVK(req.URL, result, apiPathPrefix, reqInfo)
if err != nil {
return true, nil, err
}
// shallow copy because we want to preserve all the headers and such but not mutate the original request
newReq := req.WithContext(req.Context())
// replace the body and path with the new data
newReq.URL = reqURL
glogBody("mutated request url", []byte(reqURL.String()))
resp, err := rt.RoundTrip(newReq)
if err != nil {
return false, nil, fmt.Errorf("middleware request for %#v failed: %w", middlewareReq, err)
}
switch v {
case VerbDelete, VerbDeleteCollection:
return false, resp, nil // we do not need to fix the response on delete
case VerbWatch:
resp, err := handleWatchResponseNewGVK(config, negotiatedSerializer, resp, middlewareReq, result)
return false, resp, err
default: // VerbGet, VerbList, VerbPatch
resp, err := handleResponseNewGVK(config, negotiatedSerializer, resp, middlewareReq, result)
return false, resp, err
}
}
func handleCreateOrUpdate(
req *http.Request,
middlewareReq *request,
regSerializer runtime.Serializer,
rt http.RoundTripper,
apiPathPrefix string,
reqInfo *genericapirequest.RequestInfo,
config *restclient.Config,
negotiatedSerializer runtime.NegotiatedSerializer,
) (bool, *http.Response, error) {
if req.GetBody == nil {
return true, nil, fmt.Errorf("unreadible body for request: %#v", middlewareReq) // this should never happen
}
body, err := req.GetBody()
if err != nil {
return true, nil, fmt.Errorf("get body failed: %w", err)
}
defer body.Close()
data, err := ioutil.ReadAll(body)
if err != nil {
return true, nil, fmt.Errorf("read body failed: %w", err)
}
// attempt to decode with no defaults or into specified, i.e. defer to the decoder
// this should result in the a straight decode with no conversion
decodedObj, err := runtime.Decode(regSerializer, data)
if err != nil {
return true, nil, fmt.Errorf("body decode failed: %w", err)
}
obj, ok := decodedObj.(Object)
if !ok {
return true, nil, fmt.Errorf("middleware request for %#v has invalid object semantics: %T", middlewareReq, decodedObj)
}
result, err := middlewareReq.mutateRequest(obj)
if err != nil {
return true, nil, err
}
if !result.mutated {
resp, err := rt.RoundTrip(req) // no middleware mutated the request
return false, resp, err
}
reqURL, err := updatePathNewGVK(req.URL, result, apiPathPrefix, reqInfo)
if err != nil {
return true, nil, err
}
newData, err := runtime.Encode(regSerializer, obj)
if err != nil {
return true, nil, fmt.Errorf("new body encode failed: %w", err)
}
// simplest way to reuse the body creation logic
newReqForBody, err := http.NewRequest(req.Method, reqURL.String(), bytes.NewReader(newData))
if err != nil {
return true, nil, fmt.Errorf("failed to create new req for body: %w", err) // this should never happen
}
// shallow copy because we want to preserve all the headers and such but not mutate the original request
newReq := req.WithContext(req.Context())
// replace the body and path with the new data
newReq.URL = reqURL
newReq.ContentLength = newReqForBody.ContentLength
newReq.Body = newReqForBody.Body
newReq.GetBody = newReqForBody.GetBody
glogBody("mutated request", newData)
resp, err := rt.RoundTrip(newReq)
if err != nil {
return true, nil, fmt.Errorf("middleware request for %#v failed: %w", middlewareReq, err)
}
if !result.gvkChanged && len(middlewareReq.respFuncs) == 0 {
return true, resp, nil // we did not change the GVK, so we do not need to mess with the incoming data
}
resp, err = handleResponseNewGVK(config, negotiatedSerializer, resp, middlewareReq, result)
return true, resp, err
}
func handleResponseNewGVK(
config *restclient.Config,
negotiatedSerializer runtime.NegotiatedSerializer,
resp *http.Response,
middlewareReq *request,
result *mutationResult,
) (*http.Response, error) {
// defer these status codes to client-go
switch {
case resp.StatusCode == http.StatusSwitchingProtocols,
resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
return resp, nil
}
// always make sure we close the body, even if reading from it fails
defer resp.Body.Close()
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
serializerInfo, err := getSerializerInfo(config, negotiatedSerializer, resp, middlewareReq)
if err != nil {
return nil, err
}
fixedRespData, err := maybeRestoreGVK(serializerInfo.Serializer, respData, result)
if err != nil {
return nil, fmt.Errorf("unable to restore GVK for %#v: %w", middlewareReq, err)
}
fixedRespData, err = maybeMutateResponse(serializerInfo.Serializer, fixedRespData, middlewareReq, result)
if err != nil {
return nil, fmt.Errorf("unable to mutate response for %#v: %w", middlewareReq, err)
}
newResp := &http.Response{}
*newResp = *resp
newResp.Body = ioutil.NopCloser(bytes.NewBuffer(fixedRespData))
return newResp, nil
}
func maybeMutateResponse(serializer runtime.Serializer, fixedRespData []byte, middlewareReq *request, result *mutationResult) ([]byte, error) {
if len(middlewareReq.respFuncs) == 0 {
return fixedRespData, nil
}
decodedObj, err := runtime.Decode(serializer, fixedRespData)
if err != nil {
return fixedRespData, nil // if we cannot decode it, it is not for us - let client-go figure out what to do
}
if decodedObj.GetObjectKind().GroupVersionKind() != result.origGVK {
return fixedRespData, nil
}
var mutated bool
switch middlewareReq.Verb() {
case VerbList:
if err := meta.EachListItem(decodedObj, func(listObj runtime.Object) error {
obj, ok := listObj.(Object)
if !ok {
return fmt.Errorf("middleware request for %#v has invalid object semantics: %T", middlewareReq, decodedObj)
}
singleMutated, err := middlewareReq.mutateResponse(obj)
if err != nil {
return fmt.Errorf("response mutation failed for %#v: %w", middlewareReq, err)
}
mutated = mutated || singleMutated
return nil
}); err != nil {
return nil, fmt.Errorf("failed to iterate over list for %#v: %T", middlewareReq, decodedObj)
}
default:
obj, ok := decodedObj.(Object)
if !ok {
return nil, fmt.Errorf("middleware request for %#v has invalid object semantics: %T", middlewareReq, decodedObj)
}
mutated, err = middlewareReq.mutateResponse(obj)
if err != nil {
return nil, fmt.Errorf("response mutation failed for %#v: %w", middlewareReq, err)
}
}
if !mutated {
return fixedRespData, nil
}
newData, err := runtime.Encode(serializer, decodedObj)
if err != nil {
return nil, fmt.Errorf("new body encode failed: %w", err)
}
// only log if we mutated the response; we only need to log the unmutated response since client-go
// will log the mutated response for us
glogBody("unmutated response", fixedRespData)
return newData, nil
}

View File

@@ -0,0 +1,84 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
)
type objectList interface {
runtime.Object // generic access to TypeMeta
metav1.ListInterface // generic access to ListMeta
}
func schemeRestMapper(scheme *runtime.Scheme) func(schema.GroupVersionResource, Verb) (schema.GroupVersionKind, bool) {
// we are assuming that no code uses the `// +resourceName=CUSTOM_RESOURCE_NAME` directive
// and that no Kube code generator is passed a --plural-exceptions argument
pluralExceptions := map[string]string{"Endpoints": "Endpoints"} // default copied from client-gen
lowercaseNamer := namer.NewAllLowercasePluralNamer(pluralExceptions)
listVerbMapping := map[schema.GroupVersionResource]schema.GroupVersionKind{}
nonListVerbMapping := map[schema.GroupVersionResource]schema.GroupVersionKind{}
for gvk := range scheme.AllKnownTypes() {
obj, err := scheme.New(gvk)
if err != nil {
panic(err) // programmer error (internal scheme code is broken)
}
switch t := obj.(type) {
case interface {
Object
objectList
}:
panic(fmt.Errorf("type is both list and non-list: %T", t))
case Object:
resource := lowercaseNamer.Name(types.Ref("ignored", gvk.Kind))
gvr := gvk.GroupVersion().WithResource(resource)
nonListVerbMapping[gvr] = gvk
case objectList:
if _, ok := t.(*metav1.Status); ok {
continue // ignore status since it does not have an Items field
}
itemsPtr, err := meta.GetItemsPtr(obj)
if err != nil {
panic(err) // programmer error (internal scheme code is broken)
}
items, err := conversion.EnforcePtr(itemsPtr)
if err != nil {
panic(err) // programmer error (internal scheme code is broken)
}
nonListKind := items.Type().Elem().Name()
resource := lowercaseNamer.Name(types.Ref("ignored", nonListKind))
gvr := gvk.GroupVersion().WithResource(resource)
listVerbMapping[gvr] = gvk
default:
// ignore stuff like ListOptions
}
}
return func(resource schema.GroupVersionResource, v Verb) (schema.GroupVersionKind, bool) {
switch v {
case VerbList:
gvk, ok := listVerbMapping[resource]
return gvk, ok
default:
gvk, ok := nonListVerbMapping[resource]
return gvk, ok
}
}
}

View File

@@ -0,0 +1,156 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kubescheme "k8s.io/client-go/kubernetes/scheme"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
aggregatorclientscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/idp/v1alpha1"
pinnipedconciergeclientsetscheme "go.pinniped.dev/generated/1.20/client/concierge/clientset/versioned/scheme"
pinnipedsupervisorclientsetscheme "go.pinniped.dev/generated/1.20/client/supervisor/clientset/versioned/scheme"
)
func Test_schemeRestMapper(t *testing.T) {
type args struct {
scheme *runtime.Scheme
gvr schema.GroupVersionResource
v Verb
}
tests := []struct {
name string
args args
want schema.GroupVersionKind
}{
{
name: "config map get",
args: args{
scheme: kubescheme.Scheme,
gvr: corev1.SchemeGroupVersion.WithResource("configmaps"),
v: VerbGet,
},
want: corev1.SchemeGroupVersion.WithKind("ConfigMap"),
},
{
name: "config map list",
args: args{
scheme: kubescheme.Scheme,
gvr: corev1.SchemeGroupVersion.WithResource("configmaps"),
v: VerbList,
},
want: corev1.SchemeGroupVersion.WithKind("ConfigMapList"),
},
{
name: "endpoints patch",
args: args{
scheme: kubescheme.Scheme,
gvr: corev1.SchemeGroupVersion.WithResource("endpoints"),
v: VerbPatch,
},
want: corev1.SchemeGroupVersion.WithKind("Endpoints"),
},
{
name: "endpoints list",
args: args{
scheme: kubescheme.Scheme,
gvr: corev1.SchemeGroupVersion.WithResource("endpoints"),
v: VerbList,
},
want: corev1.SchemeGroupVersion.WithKind("EndpointsList"),
},
{
name: "api service create",
args: args{
scheme: aggregatorclientscheme.Scheme,
gvr: apiregistrationv1.SchemeGroupVersion.WithResource("apiservices"),
v: VerbCreate,
},
want: apiregistrationv1.SchemeGroupVersion.WithKind("APIService"),
},
{
name: "api service create - wrong scheme",
args: args{
scheme: kubescheme.Scheme,
gvr: apiregistrationv1.SchemeGroupVersion.WithResource("apiservices"),
v: VerbCreate,
},
},
{
name: "api service list",
args: args{
scheme: aggregatorclientscheme.Scheme,
gvr: apiregistrationv1.SchemeGroupVersion.WithResource("apiservices"),
v: VerbList,
},
want: apiregistrationv1.SchemeGroupVersion.WithKind("APIServiceList"),
},
{
name: "token credential delete",
args: args{
scheme: pinnipedconciergeclientsetscheme.Scheme,
gvr: loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests"),
v: VerbDelete,
},
want: loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest"),
},
{
name: "token credential list",
args: args{
scheme: pinnipedconciergeclientsetscheme.Scheme,
gvr: loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests"),
v: VerbList,
},
want: loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequestList"),
},
{
name: "oidc idp update",
args: args{
scheme: pinnipedsupervisorclientsetscheme.Scheme,
gvr: idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"),
v: VerbUpdate,
},
want: idpv1alpha1.SchemeGroupVersion.WithKind("OIDCIdentityProvider"),
},
{
name: "oidc idp list",
args: args{
scheme: pinnipedsupervisorclientsetscheme.Scheme,
gvr: idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"),
v: VerbList,
},
want: idpv1alpha1.SchemeGroupVersion.WithKind("OIDCIdentityProviderList"),
},
{
name: "oidc idp list - wrong scheme",
args: args{
scheme: pinnipedconciergeclientsetscheme.Scheme,
gvr: idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"),
v: VerbList,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
schemeRestMapperFunc := schemeRestMapper(tt.args.scheme)
gvk, ok := schemeRestMapperFunc(tt.args.gvr, tt.args.v)
if tt.want.Empty() {
require.True(t, gvk.Empty())
require.False(t, ok)
} else {
require.Equal(t, tt.want, gvk)
require.True(t, ok)
}
})
}
}

View File

@@ -0,0 +1,39 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"fmt"
"mime"
"net/http"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
restclient "k8s.io/client-go/rest"
)
type passthroughDecoder struct{}
func (d passthroughDecoder) Decode(data []byte, _ *schema.GroupVersionKind, _ runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
return &runtime.Unknown{Raw: data}, &schema.GroupVersionKind{}, nil
}
func getSerializerInfo(config *restclient.Config, negotiatedSerializer runtime.NegotiatedSerializer, resp *http.Response, middlewareReq *request) (runtime.SerializerInfo, error) {
contentType := resp.Header.Get("Content-Type")
if len(contentType) == 0 {
contentType = config.ContentType
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return runtime.SerializerInfo{}, fmt.Errorf("failed to parse content type for %#v: %w", middlewareReq, err)
}
respInfo, ok := runtime.SerializerInfoForMediaType(negotiatedSerializer.SupportedMediaTypes(), mediaType)
if !ok || respInfo.Serializer == nil || respInfo.StreamSerializer == nil || respInfo.StreamSerializer.Serializer == nil || respInfo.StreamSerializer.Framer == nil {
return runtime.SerializerInfo{}, fmt.Errorf("unable to find resp serialier for %#v with content-type %s", middlewareReq, mediaType)
}
return respInfo, nil
}

View File

@@ -0,0 +1,27 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
type Verb interface {
verb() // private method to prevent creation of verbs outside this package
}
const (
VerbCreate verb = "create"
VerbUpdate verb = "update"
VerbDelete verb = "delete"
VerbDeleteCollection verb = "deletecollection"
VerbGet verb = "get"
VerbList verb = "list"
VerbWatch verb = "watch"
VerbPatch verb = "patch"
VerbProxy verb = "proxy" // proxy unsupported for now
)
var _, _ Verb = VerbGet, verb("")
type verb string
func (verb) verb() {}

View File

@@ -0,0 +1,54 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func Test_verb(t *testing.T) {
tests := []struct {
name string
f func() string
want string
}{
{
name: "error: string format",
f: func() string {
return fmt.Errorf("%s", VerbGet).Error()
},
want: "get",
},
{
name: "error: value format",
f: func() string {
return fmt.Errorf("%v", VerbUpdate).Error()
},
want: "update",
},
{
name: "error: go value format",
f: func() string {
return fmt.Errorf("%#v", VerbDelete).Error()
},
want: `"delete"`,
},
{
name: "error: go value format in middelware request",
f: func() string {
return fmt.Errorf("%#v", request{verb: VerbPatch}).Error()
},
want: `kubeclient.request{verb:"patch", namespace:"", resource:schema.GroupVersionResource{Group:"", Version:"", Resource:""}, reqFuncs:[]func(kubeclient.Object)(nil), respFuncs:[]func(kubeclient.Object)(nil), subresource:""}`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, tt.f())
})
}
}

View File

@@ -0,0 +1,163 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
stderrors "errors"
"fmt"
"io"
"io/ioutil"
"net/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
"k8s.io/apimachinery/pkg/util/net"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/watch"
restclient "k8s.io/client-go/rest"
restclientwatch "k8s.io/client-go/rest/watch"
"go.pinniped.dev/internal/plog"
)
func handleWatchResponseNewGVK(
config *restclient.Config,
negotiatedSerializer runtime.NegotiatedSerializer,
resp *http.Response,
middlewareReq *request,
result *mutationResult,
) (*http.Response, error) {
// defer non-success cases to client-go
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var goRoutineStarted bool
defer func() {
if goRoutineStarted {
return
}
// always drain and close the body if we do not get to the point of starting our go routine
drainAndMaybeCloseBody(resp, true)
}()
serializerInfo, err := getSerializerInfo(config, negotiatedSerializer, resp, middlewareReq)
if err != nil {
return nil, err
}
newResp := &http.Response{}
*newResp = *resp
newBodyReader, newBodyWriter := io.Pipe()
newResp.Body = newBodyReader // client-go is responsible for closing this reader
goRoutineStarted = true
go func() {
var sourceDecoder watch.Decoder
defer utilruntime.HandleCrash()
defer func() {
// the sourceDecoder will close the resp body. we want to make sure the drain the body before
// we do that
drainAndMaybeCloseBody(resp, false)
if sourceDecoder != nil {
sourceDecoder.Close()
}
}()
defer newBodyWriter.Close()
frameReader := serializerInfo.StreamSerializer.Framer.NewFrameReader(resp.Body)
watchEventDecoder := streaming.NewDecoder(frameReader, serializerInfo.StreamSerializer.Serializer)
sourceDecoder = restclientwatch.NewDecoder(watchEventDecoder, &passthroughDecoder{})
defer sourceDecoder.Close()
frameWriter := serializerInfo.StreamSerializer.Framer.NewFrameWriter(newBodyWriter)
watchEventEncoder := streaming.NewEncoder(frameWriter, serializerInfo.StreamSerializer.Serializer)
for {
ok, err := sendWatchEvent(sourceDecoder, serializerInfo.Serializer, middlewareReq, result, watchEventEncoder)
if err != nil {
if stderrors.Is(err, io.ErrClosedPipe) {
return // calling newBodyReader.Close() will send this to all newBodyWriter.Write()
}
// CloseWithError always returns nil
// all newBodyReader.Read() will get this error
_ = newBodyWriter.CloseWithError(err)
return
}
if !ok {
return
}
}
}()
return newResp, nil
}
func sendWatchEvent(sourceDecoder watch.Decoder, s runtime.Serializer, middlewareReq *request, result *mutationResult, watchEventEncoder streaming.Encoder) (bool, error) {
// partially copied from watch.NewStreamWatcher.receive
eventType, obj, err := sourceDecoder.Decode()
if err != nil {
switch {
case stderrors.Is(err, io.EOF):
// watch closed normally
case stderrors.Is(err, io.ErrUnexpectedEOF):
plog.InfoErr("Unexpected EOF during watch stream event decoding", err)
case net.IsProbableEOF(err), net.IsTimeout(err):
plog.TraceErr("Unable to decode an event from the watch stream", err)
default:
return false, fmt.Errorf("unexpected watch decode error for %#v: %w", middlewareReq, err)
}
return false, nil // all errors end watch
}
unknown, ok := obj.(*runtime.Unknown)
if !ok || len(unknown.Raw) == 0 {
return false, fmt.Errorf("unexpected decode type: %T", obj)
}
respData := unknown.Raw
fixedRespData, err := maybeRestoreGVK(s, respData, result)
if err != nil {
return false, fmt.Errorf("unable to restore GVK for %#v: %w", middlewareReq, err)
}
fixedRespData, err = maybeMutateResponse(s, fixedRespData, middlewareReq, result)
if err != nil {
return false, fmt.Errorf("unable to mutate response for %#v: %w", middlewareReq, err)
}
event := &metav1.WatchEvent{
Type: string(eventType),
Object: runtime.RawExtension{Raw: fixedRespData},
}
if err := watchEventEncoder.Encode(event); err != nil {
return false, fmt.Errorf("failed to encode watch event for %#v: %w", middlewareReq, err)
}
return true, nil
}
// drainAndMaybeCloseBody attempts to drain and optionallt close the provided body.
//
// We want to drain used HTTP response bodies so that the underlying TCP connection can be
// reused. However, if the underlying response body is extremely large or a never-ending stream,
// then we don't want to wait for the read to finish. In these cases, we give up on the TCP
// connection and just close the body.
func drainAndMaybeCloseBody(resp *http.Response, close bool) {
// from k8s.io/client-go/rest/request.go...
const maxBodySlurpSize = 2 << 10
if resp.ContentLength <= maxBodySlurpSize {
_, _ = io.Copy(ioutil.Discard, &io.LimitedReader{R: resp.Body, N: maxBodySlurpSize})
}
if close {
resp.Body.Close()
}
}

View File

@@ -3,4 +3,4 @@
package mockkeyset
//go:generate go run -v github.com/golang/mock/mockgen -destination=mockkeyset.go -package=mockkeyset -copyright_file=../../../hack/header.txt github.com/coreos/go-oidc KeySet
//go:generate go run -v github.com/golang/mock/mockgen -destination=mockkeyset.go -package=mockkeyset -copyright_file=../../../hack/header.txt github.com/coreos/go-oidc/v3/oidc KeySet

View File

@@ -9,7 +9,7 @@ import (
"net/http"
"time"
coreosoidc "github.com/coreos/go-oidc"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"

View File

@@ -11,7 +11,7 @@ import (
"net/url"
"time"
coreosoidc "github.com/coreos/go-oidc"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"

View File

@@ -0,0 +1,22 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package csrftoken
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
func TestCSRFToken(t *testing.T) {
tok, err := Generate()
require.NoError(t, err)
require.Len(t, tok, 64)
var empty bytes.Buffer
tok, err = generate(&empty)
require.EqualError(t, err, "could not generate CSRFToken: EOF")
require.Empty(t, tok)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package oidc contains common OIDC functionality needed by Pinniped.
@@ -7,7 +7,7 @@ package oidc
import (
"time"
coreosoidc "github.com/coreos/go-oidc"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
@@ -199,7 +199,7 @@ func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration {
AuthorizationCodeSessionStorageLifetime: authorizationCodeLifespan + refreshTokenLifespan,
PKCESessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute),
OIDCSessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute),
AccessTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan,
AccessTokenSessionStorageLifetime: accessTokenLifespan + (1 * time.Minute),
RefreshTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan,
}
}

View File

@@ -11,7 +11,7 @@ import (
"net/url"
"testing"
coreosoidc "github.com/coreos/go-oidc"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"

View File

@@ -7,7 +7,7 @@ import (
"context"
"net/url"
"github.com/coreos/go-oidc"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
"github.com/ory/fosite/handler/oauth2"

View File

@@ -4,36 +4,68 @@
package ownerref
import (
"net/http"
"context"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/internal/kubeclient"
)
func New(ref metav1.OwnerReference) kubeclient.Middleware {
return ownerRefMiddleware(ref)
}
func New(refObj kubeclient.Object) kubeclient.Middleware {
ref := metav1.OwnerReference{
Name: refObj.GetName(),
UID: refObj.GetUID(),
}
ref.APIVersion, ref.Kind = refObj.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind()
refNamespace := refObj.GetNamespace()
var _ kubeclient.Middleware = ownerRefMiddleware(metav1.OwnerReference{})
// if refNamespace is empty, we assume the owner ref is to a cluster scoped object which can own any object
refIsNamespaced := len(refNamespace) != 0
type ownerRefMiddleware metav1.OwnerReference
func (o ownerRefMiddleware) Handles(httpMethod string) bool {
return httpMethod == http.MethodPost // only handle create requests
}
// TODO this func assumes all objects are namespace scoped and are in the same namespace.
// i.e. it assumes all objects are safe to set an owner ref on
// i.e. the owner could be namespace scoped and thus cannot own a cluster scoped object
// this could be fixed by using a rest mapper to confirm the REST scoping
// or we could always use an owner ref to a cluster scoped object
func (o ownerRefMiddleware) Mutate(obj metav1.Object) (mutated bool) {
// we only want to set the owner ref on create and when one is not already present
if len(obj.GetOwnerReferences()) != 0 {
return false
// special handling of namespaces to treat them as namespace scoped to themselves
if isNamespace(refObj) {
refNamespace = refObj.GetName()
refIsNamespaced = true
}
obj.SetOwnerReferences([]metav1.OwnerReference{metav1.OwnerReference(o)})
return true
return kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// we should not mess with owner refs on things we did not create
if rt.Verb() != kubeclient.VerbCreate {
return
}
// we probably do not want to set an owner ref on a subresource
if len(rt.Subresource()) != 0 {
return
}
// when ref is not cluster scoped, we ignore cluster scoped resources
if refIsNamespaced && !rt.NamespaceScoped() {
return
}
// when ref is not cluster scoped, we require refNamespace to match
// the request namespace since cross namespace ownership is disallowed
if refIsNamespaced && refNamespace != rt.Namespace() {
return
}
rt.MutateRequest(func(obj kubeclient.Object) {
// we only want to set the owner ref on create and when one is not already present
if len(obj.GetOwnerReferences()) != 0 {
return
}
obj.SetOwnerReferences([]metav1.OwnerReference{ref})
})
})
}
//nolint: gochecknoglobals
var namespaceGVK = corev1.SchemeGroupVersion.WithKind("Namespace")
func isNamespace(obj kubeclient.Object) bool {
_, ok := obj.(*corev1.Namespace)
return ok || obj.GetObjectKind().GroupVersionKind() == namespaceGVK
}

View File

@@ -4,46 +4,47 @@
package ownerref
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/testutil"
)
func TestOwnerReferenceMiddleware(t *testing.T) {
ref1 := metav1.OwnerReference{
Name: "earth",
UID: "0x11",
}
ref2 := metav1.OwnerReference{
Name: "mars",
UID: "0x12",
}
ref3 := metav1.OwnerReference{
Name: "sun",
UID: "0x13",
}
ref1 := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "earth", Namespace: "some-namespace", UID: "0x11"}}
ref2 := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "mars", Namespace: "some-namespace", UID: "0x12"}}
ref3 := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "sun", Namespace: "some-namespace", UID: "0x13"}}
clusterRef := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: "bananas", UID: "0x13"}}
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "twizzlers"}}
configMap := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "pandas"}}
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "twizzlers", Namespace: "some-namespace"}}
secretOtherNamespace := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "twizzlers", Namespace: "some-other-namespace"}}
configMap := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "pandas", Namespace: "some-namespace"}}
clusterRole := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: "bananas"}}
secretWithOwner := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "twizzlers", OwnerReferences: []metav1.OwnerReference{ref3}}}
configMapWithOwner := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "pandas", OwnerReferences: []metav1.OwnerReference{ref3}}}
secretWithOwner := withOwnerRef(t, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "twizzlers", Namespace: "some-namespace"}}, ref3)
configMapWithOwner := withOwnerRef(t, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "pandas", Namespace: "some-namespace"}}, ref3)
namespaceRef := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "solar-system", UID: "0x42"}}
secretInSameNamespaceAsNamespaceRef := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "venus", Namespace: "solar-system", UID: "0x11"}}
type args struct {
ref metav1.OwnerReference
httpMethod string
obj metav1.Object
ref kubeclient.Object
httpMethod string
subresource string
obj kubeclient.Object
}
tests := []struct {
name string
args args
wantHandles, wantMutates bool
wantObj metav1.Object
wantObj kubeclient.Object
}{
{
name: "on update",
@@ -54,7 +55,36 @@ func TestOwnerReferenceMiddleware(t *testing.T) {
},
wantHandles: false,
wantMutates: false,
wantObj: nil,
},
{
name: "on get",
args: args{
ref: ref1,
httpMethod: http.MethodGet,
obj: secret.DeepCopy(),
},
wantHandles: false,
wantMutates: false,
},
{
name: "on delete",
args: args{
ref: ref1,
httpMethod: http.MethodDelete,
obj: secret.DeepCopy(),
},
wantHandles: false,
wantMutates: false,
},
{
name: "on patch",
args: args{
ref: ref1,
httpMethod: http.MethodPatch,
obj: secret.DeepCopy(),
},
wantHandles: false,
wantMutates: false,
},
{
name: "on create",
@@ -67,6 +97,17 @@ func TestOwnerReferenceMiddleware(t *testing.T) {
wantMutates: true,
wantObj: withOwnerRef(t, secret, ref1),
},
{
name: "on create when the ref object is a namespace",
args: args{
ref: namespaceRef,
httpMethod: http.MethodPost,
obj: secretInSameNamespaceAsNamespaceRef.DeepCopy(),
},
wantHandles: true,
wantMutates: true,
wantObj: withOwnerRef(t, secretInSameNamespaceAsNamespaceRef, namespaceRef),
},
{
name: "on create config map",
args: args{
@@ -78,27 +119,78 @@ func TestOwnerReferenceMiddleware(t *testing.T) {
wantMutates: true,
wantObj: withOwnerRef(t, configMap, ref2),
},
{
name: "on create with cluster-scoped owner",
args: args{
ref: clusterRef,
httpMethod: http.MethodPost,
obj: secret.DeepCopy(),
},
wantHandles: true,
wantMutates: true,
wantObj: withOwnerRef(t, secret, clusterRef),
},
{
name: "on create of cluster-scoped resource with namespace-scoped owner",
args: args{
ref: ref1,
httpMethod: http.MethodPost,
obj: clusterRole.DeepCopy(),
},
wantHandles: false,
wantMutates: false,
},
{
name: "on create of cluster-scoped resource with cluster-scoped owner",
args: args{
ref: clusterRef,
httpMethod: http.MethodPost,
obj: clusterRole.DeepCopy(),
},
wantHandles: true,
wantMutates: true,
wantObj: withOwnerRef(t, clusterRole, clusterRef),
},
{
name: "on create with pre-existing ref",
args: args{
ref: ref1,
httpMethod: http.MethodPost,
obj: secretWithOwner.DeepCopy(),
obj: secretWithOwner.DeepCopyObject().(kubeclient.Object),
},
wantHandles: true,
wantMutates: false,
wantObj: nil,
},
{
name: "on create with pre-existing ref config map",
args: args{
ref: ref2,
httpMethod: http.MethodPost,
obj: configMapWithOwner.DeepCopy(),
obj: configMapWithOwner.DeepCopyObject().(kubeclient.Object),
},
wantHandles: true,
wantMutates: false,
wantObj: nil,
},
{
name: "on create of subresource",
args: args{
ref: ref2,
httpMethod: http.MethodPost,
subresource: "some-subresource",
obj: configMapWithOwner.DeepCopyObject().(kubeclient.Object),
},
wantHandles: false,
wantMutates: false,
},
{
name: "on create with namespace mismatch",
args: args{
ref: ref2,
httpMethod: http.MethodPost,
obj: secretOtherNamespace.DeepCopyObject().(kubeclient.Object),
},
wantHandles: false,
wantMutates: false,
},
}
for _, tt := range tests {
@@ -106,37 +198,63 @@ func TestOwnerReferenceMiddleware(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
middleware := New(tt.args.ref)
handles := middleware.Handles(tt.args.httpMethod)
require.Equal(t, tt.wantHandles, handles)
if !handles {
rt := (&testutil.RoundTrip{}).
WithVerb(verb(t, tt.args.httpMethod)).
WithNamespace(tt.args.obj.GetNamespace()).
WithSubresource(tt.args.subresource)
middleware.Handle(context.Background(), rt)
require.Empty(t, rt.MutateResponses, 1)
if !tt.wantHandles {
require.Empty(t, rt.MutateRequests)
return
}
require.Len(t, rt.MutateRequests, 1)
orig := tt.args.obj.(runtime.Object).DeepCopyObject()
mutates := middleware.Mutate(tt.args.obj)
require.Equal(t, tt.wantMutates, mutates)
if mutates {
orig := tt.args.obj.DeepCopyObject().(kubeclient.Object)
for _, mutateRequest := range rt.MutateRequests {
mutateRequest := mutateRequest
mutateRequest(tt.args.obj)
}
if !tt.wantMutates {
require.Equal(t, orig, tt.args.obj)
} else {
require.NotEqual(t, orig, tt.args.obj)
require.Equal(t, tt.wantObj, tt.args.obj)
} else {
require.Equal(t, orig, tt.args.obj)
}
})
}
}
func withOwnerRef(t *testing.T, obj runtime.Object, ref metav1.OwnerReference) metav1.Object {
func withOwnerRef(t *testing.T, obj kubeclient.Object, ref kubeclient.Object) kubeclient.Object {
t.Helper()
obj = obj.DeepCopyObject()
accessor, err := meta.Accessor(obj)
require.NoError(t, err)
ownerRef := metav1.OwnerReference{
Name: ref.GetName(),
UID: ref.GetUID(),
}
require.Len(t, accessor.GetOwnerReferences(), 0)
accessor.SetOwnerReferences([]metav1.OwnerReference{ref})
obj = obj.DeepCopyObject().(kubeclient.Object)
require.Len(t, obj.GetOwnerReferences(), 0)
obj.SetOwnerReferences([]metav1.OwnerReference{ownerRef})
return accessor
return obj
}
func verb(t *testing.T, v string) kubeclient.Verb {
t.Helper()
switch v {
case http.MethodGet:
return kubeclient.VerbGet
case http.MethodPut:
return kubeclient.VerbUpdate
case http.MethodPost:
return kubeclient.VerbCreate
case http.MethodDelete:
return kubeclient.VerbDelete
case http.MethodPatch:
return kubeclient.VerbPatch
default:
require.FailNowf(t, "unknown verb", "unknown verb: %q", v)
return kubeclient.VerbGet // shouldn't get here
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
@@ -7,6 +7,7 @@ import (
"sync"
"github.com/spf13/pflag"
"k8s.io/klog/v2"
)
//nolint: gochecknoglobals
@@ -30,3 +31,34 @@ func RemoveKlogGlobalFlags() {
panic("unsupported global klog flag set")
}
}
func klogLevelForPlogLevel(plogLevel LogLevel) (klogLevel klog.Level) {
switch plogLevel {
case LevelWarning:
klogLevel = klogLevelWarning // unset means minimal logs (Error and Warning)
case LevelInfo:
klogLevel = klogLevelInfo
case LevelDebug:
klogLevel = klogLevelDebug
case LevelTrace:
klogLevel = klogLevelTrace
case LevelAll:
klogLevel = klogLevelAll + 100 // make all really mean all
default:
klogLevel = -1
}
return
}
func getKlogLevel() klog.Level {
// hack around klog not exposing a Get method
for i := klog.Level(0); i < 256; i++ {
if klog.V(i).Enabled() {
continue
}
return i - 1
}
return -1
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
@@ -39,26 +39,20 @@ const (
)
func ValidateAndSetLogLevelGlobally(level LogLevel) error {
var klogLogLevel int
switch level {
case LevelWarning:
klogLogLevel = klogLevelWarning // unset means minimal logs (Error and Warning)
case LevelInfo:
klogLogLevel = klogLevelInfo
case LevelDebug:
klogLogLevel = klogLevelDebug
case LevelTrace:
klogLogLevel = klogLevelTrace
case LevelAll:
klogLogLevel = klogLevelAll + 100 // make all really mean all
default:
klogLevel := klogLevelForPlogLevel(level)
if klogLevel < 0 {
return errInvalidLogLevel
}
if _, err := logs.GlogSetter(strconv.Itoa(klogLogLevel)); err != nil {
if _, err := logs.GlogSetter(strconv.Itoa(int(klogLevel))); err != nil {
panic(err) // programmer error
}
return nil
}
// Enabled returns whether the provided plog level is enabled, i.e., whether print statements at the
// provided level will show up.
func Enabled(level LogLevel) bool {
return getKlogLevel() >= klogLevelForPlogLevel(level)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
@@ -13,42 +13,50 @@ import (
)
func TestValidateAndSetLogLevelGlobally(t *testing.T) {
originalLogLevel := getKlogLevel(t)
originalLogLevel := getKlogLevel()
require.GreaterOrEqual(t, int(originalLogLevel), int(klog.Level(0)), "cannot get klog level")
tests := []struct {
name string
level LogLevel
wantLevel klog.Level
wantErr string
name string
level LogLevel
wantLevel klog.Level
wantEnabled []LogLevel
wantErr string
}{
{
name: "unset",
wantLevel: 0,
name: "unset",
wantLevel: 0,
wantEnabled: []LogLevel{LevelWarning},
},
{
name: "warning",
level: LevelWarning,
wantLevel: 0,
name: "warning",
level: LevelWarning,
wantLevel: 0,
wantEnabled: []LogLevel{LevelWarning},
},
{
name: "info",
level: LevelInfo,
wantLevel: 2,
name: "info",
level: LevelInfo,
wantLevel: 2,
wantEnabled: []LogLevel{LevelWarning, LevelInfo},
},
{
name: "debug",
level: LevelDebug,
wantLevel: 4,
name: "debug",
level: LevelDebug,
wantLevel: 4,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug},
},
{
name: "trace",
level: LevelTrace,
wantLevel: 6,
name: "trace",
level: LevelTrace,
wantLevel: 6,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace},
},
{
name: "all",
level: LevelAll,
wantLevel: 108,
name: "all",
level: LevelAll,
wantLevel: 108,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll},
},
{
name: "invalid level",
@@ -66,26 +74,31 @@ func TestValidateAndSetLogLevelGlobally(t *testing.T) {
err := ValidateAndSetLogLevelGlobally(tt.level)
require.Equal(t, tt.wantErr, errString(err))
require.Equal(t, tt.wantLevel, getKlogLevel(t))
require.Equal(t, tt.wantLevel, getKlogLevel())
if tt.wantEnabled != nil {
allLevels := []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll}
for _, level := range allLevels {
if contains(tt.wantEnabled, level) {
require.Truef(t, Enabled(level), "wanted %q to be enabled", level)
} else {
require.False(t, Enabled(level), "did not want %q to be enabled", level)
}
}
}
})
}
require.Equal(t, originalLogLevel, getKlogLevel(t))
require.Equal(t, originalLogLevel, getKlogLevel())
}
func getKlogLevel(t *testing.T) klog.Level {
t.Helper()
// hack around klog not exposing a Get method
for i := klog.Level(0); i < 256; i++ {
if klog.V(i).Enabled() {
continue
func contains(haystack []LogLevel, needle LogLevel) bool {
for _, hay := range haystack {
if hay == needle {
return true
}
return i - 1
}
t.Fatal("unknown log level")
return 0
return false
}
func errString(err error) string {

View File

@@ -46,7 +46,7 @@ func Warning(msg string, keysAndValues ...interface{}) {
// Use WarningErr to issue a Warning message with an error object as part of the message.
func WarningErr(msg string, err error, keysAndValues ...interface{}) {
Warning(msg, append([]interface{}{errorKey, err}, keysAndValues)...)
Warning(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func Info(msg string, keysAndValues ...interface{}) {
@@ -55,7 +55,7 @@ func Info(msg string, keysAndValues ...interface{}) {
// Use InfoErr to log an expected error, e.g. validation failure of an http parameter.
func InfoErr(msg string, err error, keysAndValues ...interface{}) {
Info(msg, append([]interface{}{errorKey, err}, keysAndValues)...)
Info(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func Debug(msg string, keysAndValues ...interface{}) {
@@ -64,7 +64,7 @@ func Debug(msg string, keysAndValues ...interface{}) {
// Use DebugErr to issue a Debug message with an error object as part of the message.
func DebugErr(msg string, err error, keysAndValues ...interface{}) {
Debug(msg, append([]interface{}{errorKey, err}, keysAndValues)...)
Debug(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func Trace(msg string, keysAndValues ...interface{}) {
@@ -73,7 +73,7 @@ func Trace(msg string, keysAndValues ...interface{}) {
// Use TraceErr to issue a Trace message with an error object as part of the message.
func TraceErr(msg string, err error, keysAndValues ...interface{}) {
Trace(msg, append([]interface{}{errorKey, err}, keysAndValues)...)
Trace(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func All(msg string, keysAndValues ...interface{}) {

7
internal/testutil/doc.go Normal file
View File

@@ -0,0 +1,7 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package testutil contains shared test utilities for the Pinniped project.
//
// As of right now, it is more or less a dumping ground for our test utilities.
package testutil

View File

@@ -0,0 +1,213 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package fakekubeapi contains a *very* simple httptest.Server that can be used to stand in for
// a real Kube API server in tests.
//
// Usage:
// func TestSomething(t *testing.T) {
// resources := map[string]kubeclient.Object{
// // store preexisting resources here
// "/api/v1/namespaces/default/pods/some-pod-name": &corev1.Pod{...},
// }
// server, restConfig := fakekubeapi.Start(t, resources)
// defer server.Close()
// client := kubeclient.New(kubeclient.WithConfig(restConfig))
// // do stuff with client...
// }
package fakekubeapi
import (
"encoding/pem"
"fmt"
"io/ioutil"
"mime"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kubescheme "k8s.io/client-go/kubernetes/scheme"
restclient "k8s.io/client-go/rest"
aggregatorclientscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
pinnipedconciergeclientsetscheme "go.pinniped.dev/generated/1.20/client/concierge/clientset/versioned/scheme"
pinnipedsupervisorclientsetscheme "go.pinniped.dev/generated/1.20/client/supervisor/clientset/versioned/scheme"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/multierror"
)
// Start starts an httptest.Server (with TLS) that pretends to be a Kube API server.
//
// The server uses the provided resources map to store API Object's. The map should be from API path
// to Object (e.g., /api/v1/namespaces/default/pods/some-pod-name => &corev1.Pod{}).
//
// Start returns an already started httptest.Server and a restclient.Config that can be used to talk
// to the server.
//
// Note! Only these following verbs are (partially) supported: create, get, update, delete.
func Start(t *testing.T, resources map[string]runtime.Object) (*httptest.Server, *restclient.Config) {
if resources == nil {
resources = make(map[string]runtime.Object)
}
server := httptest.NewTLSServer(httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (err error) {
obj, err := decodeObj(r)
if err != nil {
return err
}
obj, err = handleObj(r, obj, resources)
if err != nil {
return err
}
if obj == nil {
obj = newNotFoundStatus(r.URL.Path)
}
if err := encodeObj(w, r, obj); err != nil {
return err
}
return nil
}))
restConfig := &restclient.Config{
Host: server.URL,
TLSClientConfig: restclient.TLSClientConfig{
CAData: pem.EncodeToMemory(&pem.Block{Bytes: server.Certificate().Raw, Type: "CERTIFICATE"}),
},
}
return server, restConfig
}
func decodeObj(r *http.Request) (runtime.Object, error) {
switch r.Method {
case http.MethodPut, http.MethodPost:
default:
return nil, nil
}
contentType := r.Header.Get("Content-Type")
if len(contentType) == 0 {
return nil, httperr.New(http.StatusUnsupportedMediaType, "empty content-type header is not allowed")
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, httperr.Wrap(http.StatusUnsupportedMediaType, "could not parse mime type from content-type header", err)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, httperr.Wrap(http.StatusInternalServerError, "read body", err)
}
var obj runtime.Object
multiErr := multierror.New()
codecsThatWeUseInOurCode := []runtime.NegotiatedSerializer{
kubescheme.Codecs,
aggregatorclientscheme.Codecs,
pinnipedconciergeclientsetscheme.Codecs,
pinnipedsupervisorclientsetscheme.Codecs,
}
for _, codec := range codecsThatWeUseInOurCode {
obj, err = tryDecodeObj(mediaType, body, codec)
if err == nil {
return obj, nil
}
multiErr.Add(err)
}
return nil, multiErr.ErrOrNil()
}
func tryDecodeObj(
mediaType string,
body []byte,
negotiatedSerializer runtime.NegotiatedSerializer,
) (runtime.Object, error) {
serializerInfo, ok := runtime.SerializerInfoForMediaType(negotiatedSerializer.SupportedMediaTypes(), mediaType)
if !ok {
return nil, httperr.Newf(http.StatusInternalServerError, "unable to find serialier with content-type %s", mediaType)
}
obj, err := runtime.Decode(serializerInfo.Serializer, body)
if err != nil {
return nil, httperr.Wrap(http.StatusInternalServerError, "decode obj", err)
}
return obj, nil
}
func handleObj(r *http.Request, obj runtime.Object, resources map[string]runtime.Object) (runtime.Object, error) {
switch r.Method {
case http.MethodGet:
obj = resources[r.URL.Path]
case http.MethodPost, http.MethodPut:
resources[path.Join(r.URL.Path, obj.(metav1.Object).GetName())] = obj
case http.MethodDelete:
obj = resources[r.URL.Path]
delete(resources, r.URL.Path)
default:
return nil, httperr.New(http.StatusMethodNotAllowed, "check source code for methods supported")
}
return obj, nil
}
func newNotFoundStatus(path string) runtime.Object {
status := &metav1.Status{
Status: metav1.StatusFailure,
Message: fmt.Sprintf("couldn't find object for path %q", path),
Reason: metav1.StatusReasonNotFound,
Code: http.StatusNotFound,
}
status.APIVersion, status.Kind = metav1.SchemeGroupVersion.WithKind("Status").ToAPIVersionAndKind()
return status
}
func encodeObj(w http.ResponseWriter, r *http.Request, obj runtime.Object) error {
if r.Method == http.MethodDelete {
return nil
}
accepts := strings.Split(r.Header.Get("Accept"), ",")
contentType := findGoodContentType(accepts)
if len(contentType) == 0 {
return httperr.Newf(http.StatusUnsupportedMediaType, "can't find good content type in %s", accepts)
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return httperr.Wrap(http.StatusUnsupportedMediaType, "could not parse mime type from accept header", err)
}
serializerInfo, ok := runtime.SerializerInfoForMediaType(kubescheme.Codecs.SupportedMediaTypes(), mediaType)
if !ok {
return httperr.Newf(http.StatusInternalServerError, "unable to find serialier with content-type %s", mediaType)
}
data, err := runtime.Encode(serializerInfo.Serializer, obj.(runtime.Object))
if err != nil {
return httperr.Wrap(http.StatusInternalServerError, "decode obj", err)
}
w.Header().Set("Content-Type", contentType)
if _, err := w.Write(data); err != nil {
return httperr.Wrap(http.StatusInternalServerError, "write response", err)
}
return nil
}
func findGoodContentType(contentTypes []string) string {
for _, contentType := range contentTypes {
if strings.Contains(contentType, "json") || strings.Contains(contentType, "yaml") || strings.Contains(contentType, "protobuf") {
return contentType
}
}
return ""
}

View File

@@ -0,0 +1,70 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package testutil
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"go.pinniped.dev/internal/kubeclient"
)
// RoundTrip is an implementation of kubeclient.RoundTrip that is easy to use in tests.
type RoundTrip struct {
verb kubeclient.Verb
namespace string
namespaceScoped bool
resource schema.GroupVersionResource
subresource string
MutateRequests, MutateResponses []func(kubeclient.Object)
}
func (rt *RoundTrip) WithVerb(verb kubeclient.Verb) *RoundTrip {
rt.verb = verb
return rt
}
func (rt *RoundTrip) Verb() kubeclient.Verb {
return rt.verb
}
func (rt *RoundTrip) WithNamespace(namespace string) *RoundTrip {
rt.namespace = namespace
rt.namespaceScoped = len(namespace) != 0
return rt
}
func (rt *RoundTrip) Namespace() string {
return rt.namespace
}
func (rt *RoundTrip) NamespaceScoped() bool {
return rt.namespaceScoped
}
func (rt *RoundTrip) WithResource(resource schema.GroupVersionResource) *RoundTrip {
rt.resource = resource
return rt
}
func (rt *RoundTrip) Resource() schema.GroupVersionResource {
return rt.resource
}
func (rt *RoundTrip) WithSubresource(subresource string) *RoundTrip {
rt.subresource = subresource
return rt
}
func (rt *RoundTrip) Subresource() string {
return rt.subresource
}
func (rt *RoundTrip) MutateRequest(fn func(kubeclient.Object)) {
rt.MutateRequests = append(rt.MutateRequests, fn)
}
func (rt *RoundTrip) MutateResponse(fn func(kubeclient.Object)) {
rt.MutateResponses = append(rt.MutateResponses, fn)
}

View File

@@ -9,7 +9,7 @@ import (
"net/http"
"net/url"
coreosoidc "github.com/coreos/go-oidc"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

View File

@@ -15,7 +15,7 @@ import (
"time"
"unsafe"
"github.com/coreos/go-oidc"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"

View File

@@ -12,7 +12,7 @@ import (
"net/url"
"strings"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/clientcmd"
@@ -22,6 +22,7 @@ import (
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
conciergeclientset "go.pinniped.dev/generated/1.20/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/kubeclient"
)
@@ -33,10 +34,12 @@ type Option func(*Client) error
// Client is a configuration for talking to the Pinniped concierge.
type Client struct {
namespace string
authenticator *corev1.TypedLocalObjectReference
caBundle string
endpoint *url.URL
namespace string
authenticatorName string
authenticatorKind string
caBundle string
endpoint *url.URL
apiGroupSuffix string
}
// WithNamespace configures the namespace where the TokenCredentialRequest is to be sent.
@@ -53,18 +56,15 @@ func WithAuthenticator(authType, authName string) Option {
if authName == "" {
return fmt.Errorf("authenticator name must not be empty")
}
authenticator := corev1.TypedLocalObjectReference{Name: authName}
c.authenticatorName = authName
switch strings.ToLower(authType) {
case "webhook":
authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
authenticator.Kind = "WebhookAuthenticator"
c.authenticatorKind = "WebhookAuthenticator"
case "jwt":
authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
authenticator.Kind = "JWTAuthenticator"
c.authenticatorKind = "JWTAuthenticator"
default:
return fmt.Errorf(`invalid authenticator type: %q, supported values are "webhook" and "jwt"`, authType)
}
c.authenticator = &authenticator
return nil
}
}
@@ -112,15 +112,26 @@ func WithEndpoint(endpoint string) Option {
}
}
// WithAPIGroupSuffix configures the concierge's API group suffix (e.g., "pinniped.dev").
func WithAPIGroupSuffix(apiGroupSuffix string) Option {
return func(c *Client) error {
if err := groupsuffix.Validate(apiGroupSuffix); err != nil {
return fmt.Errorf("invalid api group suffix: %w", err)
}
c.apiGroupSuffix = apiGroupSuffix
return nil
}
}
// New validates the specified options and returns a newly initialized *Client.
func New(opts ...Option) (*Client, error) {
c := Client{namespace: "pinniped-concierge"}
c := Client{namespace: "pinniped-concierge", apiGroupSuffix: "pinniped.dev"}
for _, opt := range opts {
if err := opt(&c); err != nil {
return nil, err
}
}
if c.authenticator == nil {
if c.authenticatorName == "" {
return nil, fmt.Errorf("WithAuthenticator must be specified")
}
if c.endpoint == nil {
@@ -151,7 +162,10 @@ func (c *Client) clientset() (conciergeclientset.Interface, error) {
if err != nil {
return nil, err
}
client, err := kubeclient.New(kubeclient.WithConfig(cfg))
client, err := kubeclient.New(
kubeclient.WithConfig(cfg),
kubeclient.WithMiddleware(groupsuffix.New(c.apiGroupSuffix)),
)
if err != nil {
return nil, err
}
@@ -164,13 +178,18 @@ func (c *Client) ExchangeToken(ctx context.Context, token string) (*clientauthen
if err != nil {
return nil, err
}
replacedAPIGroupName, _ := groupsuffix.Replace(auth1alpha1.SchemeGroupVersion.Group, c.apiGroupSuffix)
resp, err := clientset.LoginV1alpha1().TokenCredentialRequests(c.namespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: c.namespace,
},
Spec: loginv1alpha1.TokenCredentialRequestSpec{
Token: token,
Authenticator: *c.authenticator,
Token: token,
Authenticator: v1.TypedLocalObjectReference{
APIGroup: &replacedAPIGroupName,
Kind: c.authenticatorKind,
Name: c.authenticatorName,
},
},
}, metav1.CreateOptions{})
if err != nil {

View File

@@ -21,6 +21,7 @@ import (
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/testutil"
)
@@ -104,6 +105,24 @@ func TestNew(t *testing.T) {
},
wantErr: "WithEndpoint must be specified",
},
{
name: "empty api group suffix",
opts: []Option{
WithAuthenticator("jwt", "test-authenticator"),
WithEndpoint("https://example.com"),
WithAPIGroupSuffix(""),
},
wantErr: "invalid api group suffix: 2 error(s):\n- must contain '.'\n- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
},
{
name: "invalid api group suffix",
opts: []Option{
WithAuthenticator("jwt", "test-authenticator"),
WithEndpoint("https://example.com"),
WithAPIGroupSuffix(".starts.with.dot"),
},
wantErr: "invalid api group suffix: 1 error(s):\n- a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
},
{
name: "valid",
opts: []Option{
@@ -114,6 +133,7 @@ func TestNew(t *testing.T) {
WithBase64CABundle(base64.StdEncoding.EncodeToString(testCA.Bundle())),
WithAuthenticator("jwt", "test-authenticator"),
WithAuthenticator("webhook", "test-authenticator"),
WithAPIGroupSuffix("suffix.com"),
},
},
}
@@ -201,47 +221,7 @@ func TestExchangeToken(t *testing.T) {
t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second))
// Start a test server that returns successfully and asserts various properties of the request.
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/apis/login.concierge.pinniped.dev/v1alpha1/namespaces/test-namespace/tokencredentialrequests", r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t,
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.pinniped.dev/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.pinniped.dev",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`,
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
caBundle, endpoint := runFakeServer(t, expires, "pinniped.dev")
client, err := New(WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
require.NoError(t, err)
@@ -260,4 +240,78 @@ func TestExchangeToken(t *testing.T) {
},
}, got)
})
t.Run("changing the API group suffix for the client sends the custom suffix on the CredentialRequest's APIGroup and on its spec.Authenticator.APIGroup", func(t *testing.T) {
t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second))
caBundle, endpoint := runFakeServer(t, expires, "suffix.com")
client, err := New(WithAPIGroupSuffix("suffix.com"), WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
require.NoError(t, err)
got, err := client.ExchangeToken(ctx, "test-token")
require.NoError(t, err)
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
ExpirationTimestamp: &expires,
},
}, got)
})
}
// Start a test server that returns successfully and asserts various properties of the request.
func runFakeServer(t *testing.T, expires metav1.Time, pinnipedAPIGroupSuffix string) (string, string) {
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t,
fmt.Sprintf("/apis/login.concierge.%s/v1alpha1/namespaces/test-namespace/tokencredentialrequests", pinnipedAPIGroupSuffix),
r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t, here.Docf(
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.%s/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.%s",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`, pinnipedAPIGroupSuffix, pinnipedAPIGroupSuffix),
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: fmt.Sprintf("login.concierge.%s/v1alpha1", pinnipedAPIGroupSuffix),
Kind: "TokenCredentialRequest",
},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
return caBundle, endpoint
}

Some files were not shown because too many files have changed in this diff Show More