mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-24 14:42:22 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9addb4d6e0 | ||
|
|
2a921f7090 | ||
|
|
bb8b65cca6 | ||
|
|
5c331e9002 | ||
|
|
1382fc6e5f | ||
|
|
cc8c917249 | ||
|
|
ae498f14b4 | ||
|
|
288d9c999e | ||
|
|
26922307ad | ||
|
|
5549a262b9 | ||
|
|
c5df66fbd5 | ||
|
|
300d7bd99c | ||
|
|
012bebd66e | ||
|
|
e1d06ce4d8 | ||
|
|
52b98bdb87 | ||
|
|
62c117421a | ||
|
|
efe1fa89fe | ||
|
|
93d25a349f | ||
|
|
93ebd0f949 | ||
|
|
74a8005f92 | ||
|
|
5b4e58f0b8 | ||
|
|
b871a02ca3 | ||
|
|
6a20bbf607 | ||
|
|
dfa4d639e6 | ||
|
|
8b4024bf82 | ||
|
|
d89c6546e7 | ||
|
|
2710591429 | ||
|
|
02815cfb26 | ||
|
|
3f7cb5d9f8 | ||
|
|
46ad41e813 | ||
|
|
d4eca3a82a | ||
|
|
c03a088399 | ||
|
|
f81dda4eda | ||
|
|
1ceef5874e | ||
|
|
1b224bc4f2 | ||
|
|
530d6961c2 | ||
|
|
fe500882ef | ||
|
|
8358c26107 | ||
|
|
ad9a187522 | ||
|
|
8a41419b94 | ||
|
|
6ef7ec21cd | ||
|
|
df1d15ebd1 | ||
|
|
b3732e8b6c | ||
|
|
7e887666ce | ||
|
|
d6e6f51ced | ||
|
|
9e21de9c47 | ||
|
|
04c4cd9534 | ||
|
|
5821faec03 | ||
|
|
8bca244d59 | ||
|
|
79fa96cfbc | ||
|
|
b5cbe018e3 | ||
|
|
33f4b671d1 | ||
|
|
50c3e4c00f | ||
|
|
5486427d88 | ||
|
|
906bfa023c | ||
|
|
1c3518e18a | ||
|
|
88fd9e5c5e | ||
|
|
616211c1bc | ||
|
|
7a9c0e8c69 | ||
|
|
c09020102c | ||
|
|
af11d8cd58 | ||
|
|
93ba1b54f2 | ||
|
|
792bb98680 |
13
.dockerignore
Normal file
13
.dockerignore
Normal 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
13
.github/codecov.yml
vendored
Normal 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
|
||||
@@ -1,4 +0,0 @@
|
||||
- id: validate-copyright-year
|
||||
name: Validate copyright year
|
||||
entry: hack/check-copyright-year.sh
|
||||
language: script
|
||||
@@ -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
|
||||
|
||||
|
||||
53
Dockerfile
53
Dockerfile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ]
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
17
go.mod
@@ -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
54
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
127
internal/deploymentref/deploymentref_test.go
Normal file
127
internal/deploymentref/deploymentref_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
143
internal/groupsuffix/groupsuffix.go
Normal file
143
internal/groupsuffix/groupsuffix.go
Normal 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()
|
||||
}
|
||||
531
internal/groupsuffix/groupsuffix_test.go
Normal file
531
internal/groupsuffix/groupsuffix_test.go
Normal 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)
|
||||
}
|
||||
70
internal/kubeclient/copied.go
Normal file
70
internal/kubeclient/copied.go
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
79
internal/kubeclient/gvk.go
Normal file
79
internal/kubeclient/gvk.go
Normal 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)
|
||||
}
|
||||
222
internal/kubeclient/gvk_test.go
Normal file
222
internal/kubeclient/gvk_test.go
Normal 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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
809
internal/kubeclient/kubeclient_test.go
Normal file
809
internal/kubeclient/kubeclient_test.go
Normal 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)
|
||||
}
|
||||
147
internal/kubeclient/middleware.go
Normal file
147
internal/kubeclient/middleware.go
Normal 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
|
||||
}
|
||||
74
internal/kubeclient/middleware_test.go
Normal file
74
internal/kubeclient/middleware_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
75
internal/kubeclient/path.go
Normal file
75
internal/kubeclient/path.go
Normal 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
|
||||
}
|
||||
231
internal/kubeclient/path_test.go
Normal file
231
internal/kubeclient/path_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
84
internal/kubeclient/scheme.go
Normal file
84
internal/kubeclient/scheme.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
156
internal/kubeclient/scheme_test.go
Normal file
156
internal/kubeclient/scheme_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
39
internal/kubeclient/serializer.go
Normal file
39
internal/kubeclient/serializer.go
Normal 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
|
||||
}
|
||||
27
internal/kubeclient/verb.go
Normal file
27
internal/kubeclient/verb.go
Normal 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() {}
|
||||
54
internal/kubeclient/verb_test.go
Normal file
54
internal/kubeclient/verb_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
163
internal/kubeclient/watch.go
Normal file
163
internal/kubeclient/watch.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
22
internal/oidc/csrftoken/csrftoken_test.go
Normal file
22
internal/oidc/csrftoken/csrftoken_test.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
7
internal/testutil/doc.go
Normal 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
|
||||
213
internal/testutil/fakekubeapi/fakekubeapi.go
Normal file
213
internal/testutil/fakekubeapi/fakekubeapi.go
Normal 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 ""
|
||||
}
|
||||
70
internal/testutil/round_trip.go
Normal file
70
internal/testutil/round_trip.go
Normal 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)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user