Merge pull request #355 from vmware-tanzu/impersonation-proxy
Impersonation proxy
This commit is contained in:
commit
c0361645e2
5
.gitignore
vendored
5
.gitignore
vendored
@ -14,8 +14,11 @@
|
|||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
# goland
|
# GoLand
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Intermediate files used by Tilt
|
# Intermediate files used by Tilt
|
||||||
/hack/lib/tilt/build
|
/hack/lib/tilt/build
|
||||||
|
|
||||||
|
# MacOS Desktop Services Store
|
||||||
|
.DS_Store
|
||||||
|
@ -96,36 +96,29 @@ docker build .
|
|||||||
- [`kapp`](https://carvel.dev/#getting-started)
|
- [`kapp`](https://carvel.dev/#getting-started)
|
||||||
- [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start)
|
- [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start)
|
||||||
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||||
- [`tilt`](https://docs.tilt.dev/install.html)
|
|
||||||
- [`ytt`](https://carvel.dev/#getting-started)
|
- [`ytt`](https://carvel.dev/#getting-started)
|
||||||
|
|
||||||
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
|
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install kind tilt-dev/tap/tilt k14s/tap/ytt k14s/tap/kapp kubectl chromedriver && brew cask install docker
|
brew install kind k14s/tap/ytt k14s/tap/kapp kubectl chromedriver && brew cask install docker
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Create a local Kubernetes cluster using `kind`:
|
1. Create a kind cluster, compile, create container images, and install Pinniped and supporting dependencies using:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./hack/kind-up.sh
|
./hack/prepare-for-integration-tests.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Install Pinniped and supporting dependencies using `tilt`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./hack/tilt-up.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Tilt will continue running and live-updating the Pinniped deployment whenever the code changes.
|
|
||||||
|
|
||||||
1. Run the Pinniped integration tests:
|
1. Run the Pinniped integration tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source /tmp/integration-test-env && go test -v -count 1 ./test/integration
|
source /tmp/integration-test-env && go test -v -count 1 -timeout 0 ./test/integration
|
||||||
```
|
```
|
||||||
|
|
||||||
To uninstall the test environment, run `./hack/tilt-down.sh`.
|
1. After making production code changes, recompile, redeploy, and run tests again by repeating the same
|
||||||
|
commands described above. If there are only test code changes, then simply run the tests again.
|
||||||
|
|
||||||
To destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
|
To destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
|
||||||
|
|
||||||
### Observing Tests on the Continuous Integration Environment
|
### Observing Tests on the Continuous Integration Environment
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||||
type StrategyType string
|
type StrategyType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||||
type FrontendType string
|
type FrontendType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=Success;Error
|
// +kubebuilder:validation:Enum=Success;Error
|
||||||
type StrategyStatus string
|
type StrategyStatus string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||||
type StrategyReason string
|
type StrategyReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||||
|
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||||
|
|
||||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||||
|
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||||
|
|
||||||
SuccessStrategyStatus = StrategyStatus("Success")
|
SuccessStrategyStatus = StrategyStatus("Success")
|
||||||
ErrorStrategyStatus = StrategyStatus("Error")
|
ErrorStrategyStatus = StrategyStatus("Error")
|
||||||
|
|
||||||
|
ListeningStrategyReason = StrategyReason("Listening")
|
||||||
|
PendingStrategyReason = StrategyReason("Pending")
|
||||||
|
DisabledStrategyReason = StrategyReason("Disabled")
|
||||||
|
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
|||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
// This field is only set when Type is "ImpersonationProxy".
|
||||||
|
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
|
||||||
// CertificateAuthorityData is the Kubernetes API server CA bundle.
|
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
type ImpersonationProxyInfo struct {
|
||||||
|
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
|
||||||
|
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
}
|
}
|
||||||
|
@ -54,12 +54,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type webhook struct {
|
type webhook struct {
|
||||||
certProvider dynamiccert.Provider
|
certProvider dynamiccert.Private
|
||||||
secretInformer corev1informers.SecretInformer
|
secretInformer corev1informers.SecretInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWebhook(
|
func newWebhook(
|
||||||
certProvider dynamiccert.Provider,
|
certProvider dynamiccert.Private,
|
||||||
secretInformer corev1informers.SecretInformer,
|
secretInformer corev1informers.SecretInformer,
|
||||||
) *webhook {
|
) *webhook {
|
||||||
return &webhook{
|
return &webhook{
|
||||||
@ -281,7 +281,7 @@ func respondWithAuthenticated(
|
|||||||
|
|
||||||
func startControllers(
|
func startControllers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
dynamicCertProvider dynamiccert.Provider,
|
dynamicCertProvider dynamiccert.Private,
|
||||||
kubeClient kubernetes.Interface,
|
kubeClient kubernetes.Interface,
|
||||||
kubeInformers kubeinformers.SharedInformerFactory,
|
kubeInformers kubeinformers.SharedInformerFactory,
|
||||||
) {
|
) {
|
||||||
@ -328,7 +328,7 @@ func startControllers(
|
|||||||
func startWebhook(
|
func startWebhook(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
l net.Listener,
|
l net.Listener,
|
||||||
dynamicCertProvider dynamiccert.Provider,
|
dynamicCertProvider dynamiccert.Private,
|
||||||
secretInformer corev1informers.SecretInformer,
|
secretInformer corev1informers.SecretInformer,
|
||||||
) error {
|
) error {
|
||||||
return newWebhook(dynamicCertProvider, secretInformer).start(ctx, l)
|
return newWebhook(dynamicCertProvider, secretInformer).start(ctx, l)
|
||||||
@ -355,7 +355,7 @@ func run() error {
|
|||||||
kubeinformers.WithNamespace(namespace),
|
kubeinformers.WithNamespace(namespace),
|
||||||
)
|
)
|
||||||
|
|
||||||
dynamicCertProvider := dynamiccert.New()
|
dynamicCertProvider := dynamiccert.NewServingCert("local-user-authenticator-tls-serving-certificate")
|
||||||
|
|
||||||
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
|
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
|
||||||
plog.Debug("controllers are ready")
|
plog.Debug("controllers are ready")
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -99,7 +98,7 @@ func TestWebhook(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
secretInformer := createSecretInformer(t, kubeClient)
|
secretInformer := createSecretInformer(ctx, t, kubeClient)
|
||||||
|
|
||||||
certProvider, caBundle, serverName := newCertProvider(t)
|
certProvider, caBundle, serverName := newCertProvider(t)
|
||||||
w := newWebhook(certProvider, secretInformer)
|
w := newWebhook(certProvider, secretInformer)
|
||||||
@ -437,7 +436,7 @@ func TestWebhook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
func createSecretInformer(ctx context.Context, t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
||||||
@ -448,9 +447,6 @@ func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1i
|
|||||||
// informer factory before syncing it.
|
// informer factory before syncing it.
|
||||||
secretInformer.Informer()
|
secretInformer.Informer()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
kubeInformers.Start(ctx.Done())
|
kubeInformers.Start(ctx.Done())
|
||||||
|
|
||||||
informerTypesSynced := kubeInformers.WaitForCacheSync(ctx.Done())
|
informerTypesSynced := kubeInformers.WaitForCacheSync(ctx.Done())
|
||||||
@ -462,22 +458,23 @@ func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1i
|
|||||||
// newClientProvider returns a dynamiccert.Provider configured
|
// newClientProvider returns a dynamiccert.Provider configured
|
||||||
// with valid serving cert, the CA bundle that can be used to verify the serving
|
// with valid serving cert, the CA bundle that can be used to verify the serving
|
||||||
// cert, and the server name that can be used to verify the TLS peer.
|
// cert, and the server name that can be used to verify the TLS peer.
|
||||||
func newCertProvider(t *testing.T) (dynamiccert.Provider, []byte, string) {
|
func newCertProvider(t *testing.T) (dynamiccert.Private, []byte, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
serverName := "local-user-authenticator"
|
serverName := "local-user-authenticator"
|
||||||
|
|
||||||
ca, err := certauthority.New(pkix.Name{CommonName: serverName + " CA"}, time.Hour*24)
|
ca, err := certauthority.New(serverName+" CA", time.Hour*24)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := ca.Issue(pkix.Name{CommonName: serverName}, []string{serverName}, nil, time.Hour*24)
|
cert, err := ca.IssueServerCert([]string{serverName}, nil, time.Hour*24)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
certPEM, keyPEM, err := certauthority.ToPEM(cert)
|
certPEM, keyPEM, err := certauthority.ToPEM(cert)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
certProvider := dynamiccert.New()
|
certProvider := dynamiccert.NewServingCert(t.Name())
|
||||||
certProvider.Set(certPEM, keyPEM)
|
err = certProvider.SetCertKeyContent(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
return certProvider, ca.Bundle(), serverName
|
return certProvider, ca.Bundle(), serverName
|
||||||
}
|
}
|
||||||
|
106
cmd/pinniped/cmd/flag_types.go
Normal file
106
cmd/pinniped/cmd/flag_types.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conciergeModeFlag represents the method by which we should connect to the Concierge on a cluster during login.
|
||||||
|
// this is meant to be a valid flag.Value implementation.
|
||||||
|
type conciergeModeFlag int
|
||||||
|
|
||||||
|
var _ flag.Value = new(conciergeModeFlag)
|
||||||
|
|
||||||
|
const (
|
||||||
|
modeUnknown conciergeModeFlag = iota
|
||||||
|
modeTokenCredentialRequestAPI
|
||||||
|
modeImpersonationProxy
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *conciergeModeFlag) String() string {
|
||||||
|
switch *f {
|
||||||
|
case modeImpersonationProxy:
|
||||||
|
return "ImpersonationProxy"
|
||||||
|
case modeTokenCredentialRequestAPI:
|
||||||
|
return "TokenCredentialRequestAPI"
|
||||||
|
case modeUnknown:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return "TokenCredentialRequestAPI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *conciergeModeFlag) Set(s string) error {
|
||||||
|
if strings.EqualFold(s, "") {
|
||||||
|
*f = modeUnknown
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.EqualFold(s, "TokenCredentialRequestAPI") {
|
||||||
|
*f = modeTokenCredentialRequestAPI
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.EqualFold(s, "ImpersonationProxy") {
|
||||||
|
*f = modeImpersonationProxy
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid mode %q, valid modes are TokenCredentialRequestAPI and ImpersonationProxy", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *conciergeModeFlag) Type() string {
|
||||||
|
return "mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesFrontend returns true iff the flag matches the type of the provided frontend.
|
||||||
|
func (f *conciergeModeFlag) MatchesFrontend(frontend *configv1alpha1.CredentialIssuerFrontend) bool {
|
||||||
|
switch *f {
|
||||||
|
case modeImpersonationProxy:
|
||||||
|
return frontend.Type == configv1alpha1.ImpersonationProxyFrontendType
|
||||||
|
case modeTokenCredentialRequestAPI:
|
||||||
|
return frontend.Type == configv1alpha1.TokenCredentialRequestAPIFrontendType
|
||||||
|
case modeUnknown:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// caBundlePathsVar represents a list of CA bundle paths, which load from disk when the flag is populated.
|
||||||
|
type caBundleFlag []byte
|
||||||
|
|
||||||
|
var _ pflag.Value = new(caBundleFlag)
|
||||||
|
|
||||||
|
func (f *caBundleFlag) String() string {
|
||||||
|
return string(*f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *caBundleFlag) Set(path string) error {
|
||||||
|
pem, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not read CA bundle path: %w", err)
|
||||||
|
}
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if !pool.AppendCertsFromPEM(pem) {
|
||||||
|
return fmt.Errorf("failed to load any CA certificates from %q", path)
|
||||||
|
}
|
||||||
|
if len(*f) == 0 {
|
||||||
|
*f = pem
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*f = bytes.Join([][]byte{*f, pem}, []byte("\n"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *caBundleFlag) Type() string {
|
||||||
|
return "path"
|
||||||
|
}
|
73
cmd/pinniped/cmd/flag_types_test.go
Normal file
73
cmd/pinniped/cmd/flag_types_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConciergeModeFlag(t *testing.T) {
|
||||||
|
var f conciergeModeFlag
|
||||||
|
require.Equal(t, "mode", f.Type())
|
||||||
|
require.Equal(t, modeUnknown, f)
|
||||||
|
require.NoError(t, f.Set(""))
|
||||||
|
require.Equal(t, modeUnknown, f)
|
||||||
|
require.EqualError(t, f.Set("foo"), `invalid mode "foo", valid modes are TokenCredentialRequestAPI and ImpersonationProxy`)
|
||||||
|
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||||
|
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||||
|
|
||||||
|
require.NoError(t, f.Set("TokenCredentialRequestAPI"))
|
||||||
|
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||||
|
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||||
|
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||||
|
require.False(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||||
|
|
||||||
|
require.NoError(t, f.Set("tokencredentialrequestapi"))
|
||||||
|
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||||
|
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||||
|
|
||||||
|
require.NoError(t, f.Set("ImpersonationProxy"))
|
||||||
|
require.Equal(t, modeImpersonationProxy, f)
|
||||||
|
require.Equal(t, "ImpersonationProxy", f.String())
|
||||||
|
require.False(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||||
|
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||||
|
|
||||||
|
require.NoError(t, f.Set("impersonationproxy"))
|
||||||
|
require.Equal(t, modeImpersonationProxy, f)
|
||||||
|
require.Equal(t, "ImpersonationProxy", f.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCABundleFlag(t *testing.T) {
|
||||||
|
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tmpdir := testutil.TempDir(t)
|
||||||
|
emptyFilePath := filepath.Join(tmpdir, "empty")
|
||||||
|
require.NoError(t, ioutil.WriteFile(emptyFilePath, []byte{}, 0600))
|
||||||
|
|
||||||
|
testCAPath := filepath.Join(tmpdir, "testca.pem")
|
||||||
|
require.NoError(t, ioutil.WriteFile(testCAPath, testCA.Bundle(), 0600))
|
||||||
|
|
||||||
|
f := caBundleFlag{}
|
||||||
|
require.Equal(t, "path", f.Type())
|
||||||
|
require.Equal(t, "", f.String())
|
||||||
|
require.EqualError(t, f.Set("./does/not/exist"), "could not read CA bundle path: open ./does/not/exist: no such file or directory")
|
||||||
|
require.EqualError(t, f.Set(emptyFilePath), fmt.Sprintf("failed to load any CA certificates from %q", emptyFilePath))
|
||||||
|
|
||||||
|
require.NoError(t, f.Set(testCAPath))
|
||||||
|
require.Equal(t, 1, bytes.Count(f, []byte("BEGIN CERTIFICATE")))
|
||||||
|
|
||||||
|
require.NoError(t, f.Set(testCAPath))
|
||||||
|
require.Equal(t, 2, bytes.Count(f, []byte("BEGIN CERTIFICATE")))
|
||||||
|
}
|
@ -4,27 +4,31 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/go-logr/stdr"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
|
|
||||||
|
|
||||||
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||||
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
"go.pinniped.dev/internal/groupsuffix"
|
"go.pinniped.dev/internal/groupsuffix"
|
||||||
)
|
)
|
||||||
@ -32,12 +36,14 @@ import (
|
|||||||
type kubeconfigDeps struct {
|
type kubeconfigDeps struct {
|
||||||
getPathToSelf func() (string, error)
|
getPathToSelf func() (string, error)
|
||||||
getClientset getConciergeClientsetFunc
|
getClientset getConciergeClientsetFunc
|
||||||
|
log logr.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func kubeconfigRealDeps() kubeconfigDeps {
|
func kubeconfigRealDeps() kubeconfigDeps {
|
||||||
return kubeconfigDeps{
|
return kubeconfigDeps{
|
||||||
getPathToSelf: os.Executable,
|
getPathToSelf: os.Executable,
|
||||||
getClientset: getRealConciergeClientset,
|
getClientset: getRealConciergeClientset,
|
||||||
|
log: stdr.New(log.New(os.Stderr, "", 0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,20 +60,28 @@ type getKubeconfigOIDCParams struct {
|
|||||||
skipBrowser bool
|
skipBrowser bool
|
||||||
sessionCachePath string
|
sessionCachePath string
|
||||||
debugSessionCache bool
|
debugSessionCache bool
|
||||||
caBundlePaths []string
|
caBundle caBundleFlag
|
||||||
requestAudience string
|
requestAudience string
|
||||||
}
|
}
|
||||||
|
|
||||||
type getKubeconfigConciergeParams struct {
|
type getKubeconfigConciergeParams struct {
|
||||||
disabled bool
|
disabled bool
|
||||||
|
credentialIssuer string
|
||||||
authenticatorName string
|
authenticatorName string
|
||||||
authenticatorType string
|
authenticatorType string
|
||||||
apiGroupSuffix string
|
apiGroupSuffix string
|
||||||
|
caBundle caBundleFlag
|
||||||
|
endpoint string
|
||||||
|
mode conciergeModeFlag
|
||||||
|
skipWait bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type getKubeconfigParams struct {
|
type getKubeconfigParams struct {
|
||||||
kubeconfigPath string
|
kubeconfigPath string
|
||||||
kubeconfigContextOverride string
|
kubeconfigContextOverride string
|
||||||
|
skipValidate bool
|
||||||
|
timeout time.Duration
|
||||||
|
outputPath string
|
||||||
staticToken string
|
staticToken string
|
||||||
staticTokenEnvName string
|
staticTokenEnvName string
|
||||||
oidc getKubeconfigOIDCParams
|
oidc getKubeconfigOIDCParams
|
||||||
@ -90,11 +104,17 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
|||||||
f.StringVar(&flags.staticToken, "static-token", "", "Instead of doing an OIDC-based login, specify a static token")
|
f.StringVar(&flags.staticToken, "static-token", "", "Instead of doing an OIDC-based login, specify a static token")
|
||||||
f.StringVar(&flags.staticTokenEnvName, "static-token-env", "", "Instead of doing an OIDC-based login, read a static token from the environment")
|
f.StringVar(&flags.staticTokenEnvName, "static-token-env", "", "Instead of doing an OIDC-based login, read a static token from the environment")
|
||||||
|
|
||||||
f.BoolVar(&flags.concierge.disabled, "no-concierge", false, "Generate a configuration which does not use the concierge, but sends the credential to the cluster directly")
|
f.BoolVar(&flags.concierge.disabled, "no-concierge", false, "Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly")
|
||||||
f.StringVar(&namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
f.StringVar(&namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||||
|
f.StringVar(&flags.concierge.credentialIssuer, "concierge-credential-issuer", "", "Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover)")
|
||||||
f.StringVar(&flags.concierge.authenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)")
|
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.authenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name (default: autodiscover)")
|
||||||
f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||||
|
f.BoolVar(&flags.concierge.skipWait, "concierge-skip-wait", false, "Skip waiting for any pending Concierge strategies to become ready (default: false)")
|
||||||
|
|
||||||
|
f.Var(&flags.concierge.caBundle, "concierge-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge")
|
||||||
|
f.StringVar(&flags.concierge.endpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||||
|
f.Var(&flags.concierge.mode, "concierge-mode", "Concierge mode of operation")
|
||||||
|
|
||||||
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
|
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)")
|
f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)")
|
||||||
@ -102,26 +122,42 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
|||||||
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login")
|
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login")
|
||||||
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
|
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
|
||||||
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
|
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
|
||||||
f.StringSliceVar(&flags.oidc.caBundlePaths, "oidc-ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
f.Var(&flags.oidc.caBundle, "oidc-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||||
f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache")
|
f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache")
|
||||||
f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
||||||
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
|
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
|
||||||
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
|
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
|
||||||
|
f.BoolVar(&flags.skipValidate, "skip-validation", false, "Skip final validation of the kubeconfig (default: false)")
|
||||||
|
f.DurationVar(&flags.timeout, "timeout", 10*time.Minute, "Timeout for autodiscovery and validation")
|
||||||
|
f.StringVarP(&flags.outputPath, "output", "o", "", "Output file path (default: stdout)")
|
||||||
|
|
||||||
mustMarkHidden(cmd, "oidc-debug-session-cache")
|
mustMarkHidden(cmd, "oidc-debug-session-cache")
|
||||||
|
|
||||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||||
mustMarkHidden(cmd, "concierge-namespace")
|
mustMarkHidden(cmd, "concierge-namespace")
|
||||||
|
|
||||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runGetKubeconfig(cmd.OutOrStdout(), deps, flags) }
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
if flags.outputPath != "" {
|
||||||
|
out, err := os.Create(flags.outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open output file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = out.Close() }()
|
||||||
|
cmd.SetOut(out)
|
||||||
|
}
|
||||||
|
return runGetKubeconfig(cmd.Context(), cmd.OutOrStdout(), deps, flags)
|
||||||
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:funlen
|
//nolint:funlen
|
||||||
func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
|
func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, flags.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// Validate api group suffix and immediately return an error if it is invalid.
|
// Validate api group suffix and immediately return an error if it is invalid.
|
||||||
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
|
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
|
||||||
return fmt.Errorf("invalid api group suffix: %w", err)
|
return fmt.Errorf("invalid API group suffix: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
execConfig := clientcmdapi.ExecConfig{
|
execConfig := clientcmdapi.ExecConfig{
|
||||||
@ -137,11 +173,6 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
|||||||
}
|
}
|
||||||
execConfig.ProvideClusterInfo = true
|
execConfig.ProvideClusterInfo = true
|
||||||
|
|
||||||
oidcCABundle, err := loadCABundlePaths(flags.oidc.caBundlePaths)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not read --oidc-ca-bundle: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
|
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
|
||||||
currentKubeConfig, err := clientConfig.RawConfig()
|
currentKubeConfig, err := clientConfig.RawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -157,17 +188,39 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !flags.concierge.disabled {
|
if !flags.concierge.disabled {
|
||||||
|
credentialIssuer, err := waitForCredentialIssuer(ctx, clientset, flags, deps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
authenticator, err := lookupAuthenticator(
|
authenticator, err := lookupAuthenticator(
|
||||||
clientset,
|
clientset,
|
||||||
flags.concierge.authenticatorType,
|
flags.concierge.authenticatorType,
|
||||||
flags.concierge.authenticatorName,
|
flags.concierge.authenticatorName,
|
||||||
|
deps.log,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := configureConcierge(authenticator, &flags, cluster, &oidcCABundle, &execConfig); err != nil {
|
if err := discoverConciergeParams(credentialIssuer, &flags, cluster, deps.log); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := discoverAuthenticatorParams(authenticator, &flags, deps.log); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 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-authenticator-name="+flags.concierge.authenticatorName,
|
||||||
|
"--concierge-authenticator-type="+flags.concierge.authenticatorType,
|
||||||
|
"--concierge-endpoint="+flags.concierge.endpoint,
|
||||||
|
"--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.concierge.caBundle),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Point kubectl at the concierge endpoint.
|
||||||
|
cluster.Server = flags.concierge.endpoint
|
||||||
|
cluster.CertificateAuthorityData = flags.concierge.caBundle
|
||||||
}
|
}
|
||||||
|
|
||||||
// If one of the --static-* flags was passed, output a config that runs `pinniped login static`.
|
// If one of the --static-* flags was passed, output a config that runs `pinniped login static`.
|
||||||
@ -182,13 +235,18 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
|||||||
if flags.staticTokenEnvName != "" {
|
if flags.staticTokenEnvName != "" {
|
||||||
execConfig.Args = append(execConfig.Args, "--token-env="+flags.staticTokenEnvName)
|
execConfig.Args = append(execConfig.Args, "--token-env="+flags.staticTokenEnvName)
|
||||||
}
|
}
|
||||||
return writeConfigAsYAML(out, newExecKubeconfig(cluster, &execConfig))
|
|
||||||
|
kubeconfig := newExecKubeconfig(cluster, &execConfig)
|
||||||
|
if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeConfigAsYAML(out, kubeconfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`.
|
// Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`.
|
||||||
execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...)
|
execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...)
|
||||||
if flags.oidc.issuer == "" {
|
if flags.oidc.issuer == "" {
|
||||||
return fmt.Errorf("could not autodiscover --oidc-issuer, and none was provided")
|
return fmt.Errorf("could not autodiscover --oidc-issuer and none was provided")
|
||||||
}
|
}
|
||||||
execConfig.Args = append(execConfig.Args,
|
execConfig.Args = append(execConfig.Args,
|
||||||
"--issuer="+flags.oidc.issuer,
|
"--issuer="+flags.oidc.issuer,
|
||||||
@ -201,8 +259,8 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
|||||||
if flags.oidc.listenPort != 0 {
|
if flags.oidc.listenPort != 0 {
|
||||||
execConfig.Args = append(execConfig.Args, "--listen-port="+strconv.Itoa(int(flags.oidc.listenPort)))
|
execConfig.Args = append(execConfig.Args, "--listen-port="+strconv.Itoa(int(flags.oidc.listenPort)))
|
||||||
}
|
}
|
||||||
if oidcCABundle != "" {
|
if len(flags.oidc.caBundle) != 0 {
|
||||||
execConfig.Args = append(execConfig.Args, "--ca-bundle-data="+base64.StdEncoding.EncodeToString([]byte(oidcCABundle)))
|
execConfig.Args = append(execConfig.Args, "--ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.oidc.caBundle))
|
||||||
}
|
}
|
||||||
if flags.oidc.sessionCachePath != "" {
|
if flags.oidc.sessionCachePath != "" {
|
||||||
execConfig.Args = append(execConfig.Args, "--session-cache="+flags.oidc.sessionCachePath)
|
execConfig.Args = append(execConfig.Args, "--session-cache="+flags.oidc.sessionCachePath)
|
||||||
@ -213,15 +271,115 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
|||||||
if flags.oidc.requestAudience != "" {
|
if flags.oidc.requestAudience != "" {
|
||||||
execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience)
|
execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience)
|
||||||
}
|
}
|
||||||
return writeConfigAsYAML(out, newExecKubeconfig(cluster, &execConfig))
|
kubeconfig := newExecKubeconfig(cluster, &execConfig)
|
||||||
|
if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeConfigAsYAML(out, kubeconfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig) error {
|
func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.Interface, flags getKubeconfigParams, deps kubeconfigDeps) (*configv1alpha1.CredentialIssuer, error) {
|
||||||
|
credentialIssuer, err := lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer, deps.log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flags.concierge.skipWait {
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
deadline, _ := ctx.Deadline()
|
||||||
|
attempts := 1
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !hasPendingStrategy(credentialIssuer) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logStrategies(credentialIssuer, deps.log)
|
||||||
|
deps.log.Info("waiting for CredentialIssuer pending strategies to finish",
|
||||||
|
"attempts", attempts,
|
||||||
|
"remaining", time.Until(deadline).Round(time.Second).String(),
|
||||||
|
)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
credentialIssuer, err = lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer, deps.log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return credentialIssuer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log logr.Logger) error {
|
||||||
|
// Autodiscover the --concierge-mode.
|
||||||
|
frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode)
|
||||||
|
if err != nil {
|
||||||
|
logStrategies(credentialIssuer, log)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-set --concierge-mode if it wasn't explicitly set.
|
||||||
|
if flags.concierge.mode == modeUnknown {
|
||||||
|
switch frontend.Type {
|
||||||
|
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
|
||||||
|
log.Info("discovered Concierge operating in TokenCredentialRequest API mode")
|
||||||
|
flags.concierge.mode = modeTokenCredentialRequestAPI
|
||||||
|
case configv1alpha1.ImpersonationProxyFrontendType:
|
||||||
|
log.Info("discovered Concierge operating in impersonation proxy mode")
|
||||||
|
flags.concierge.mode = modeImpersonationProxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-set --concierge-endpoint if it wasn't explicitly set.
|
||||||
|
if flags.concierge.endpoint == "" {
|
||||||
|
switch frontend.Type {
|
||||||
|
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
|
||||||
|
flags.concierge.endpoint = v1Cluster.Server
|
||||||
|
case configv1alpha1.ImpersonationProxyFrontendType:
|
||||||
|
flags.concierge.endpoint = frontend.ImpersonationProxyInfo.Endpoint
|
||||||
|
}
|
||||||
|
log.Info("discovered Concierge endpoint", "endpoint", flags.concierge.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-set --concierge-ca-bundle if it wasn't explicitly set..
|
||||||
|
if len(flags.concierge.caBundle) == 0 {
|
||||||
|
switch frontend.Type {
|
||||||
|
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
|
||||||
|
flags.concierge.caBundle = v1Cluster.CertificateAuthorityData
|
||||||
|
case configv1alpha1.ImpersonationProxyFrontendType:
|
||||||
|
data, err := base64.StdEncoding.DecodeString(frontend.ImpersonationProxyInfo.CertificateAuthorityData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err)
|
||||||
|
}
|
||||||
|
flags.concierge.caBundle = data
|
||||||
|
}
|
||||||
|
log.Info("discovered Concierge certificate authority bundle", "roots", countCACerts(flags.concierge.caBundle))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.Logger) {
|
||||||
|
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||||
|
log.Info("found CredentialIssuer strategy",
|
||||||
|
"type", strategy.Type,
|
||||||
|
"status", strategy.Status,
|
||||||
|
"reason", strategy.Reason,
|
||||||
|
"message", strategy.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log logr.Logger) error {
|
||||||
switch auth := authenticator.(type) {
|
switch auth := authenticator.(type) {
|
||||||
case *conciergev1alpha1.WebhookAuthenticator:
|
case *conciergev1alpha1.WebhookAuthenticator:
|
||||||
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
||||||
// them to point at the discovered WebhookAuthenticator.
|
// them to point at the discovered WebhookAuthenticator.
|
||||||
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
||||||
|
log.Info("discovered WebhookAuthenticator", "name", auth.Name)
|
||||||
flags.concierge.authenticatorType = "webhook"
|
flags.concierge.authenticatorType = "webhook"
|
||||||
flags.concierge.authenticatorName = auth.Name
|
flags.concierge.authenticatorName = auth.Name
|
||||||
}
|
}
|
||||||
@ -229,56 +387,78 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams,
|
|||||||
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
||||||
// them to point at the discovered JWTAuthenticator.
|
// them to point at the discovered JWTAuthenticator.
|
||||||
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
||||||
|
log.Info("discovered JWTAuthenticator", "name", auth.Name)
|
||||||
flags.concierge.authenticatorType = "jwt"
|
flags.concierge.authenticatorType = "jwt"
|
||||||
flags.concierge.authenticatorName = auth.Name
|
flags.concierge.authenticatorName = auth.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the --oidc-issuer flag was not set explicitly, default it to the spec.issuer field of the JWTAuthenticator.
|
// If the --oidc-issuer flag was not set explicitly, default it to the spec.issuer field of the JWTAuthenticator.
|
||||||
if flags.oidc.issuer == "" {
|
if flags.oidc.issuer == "" {
|
||||||
|
log.Info("discovered OIDC issuer", "issuer", auth.Spec.Issuer)
|
||||||
flags.oidc.issuer = auth.Spec.Issuer
|
flags.oidc.issuer = auth.Spec.Issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the --oidc-request-audience flag was not set explicitly, default it to the spec.audience field of the JWTAuthenticator.
|
// If the --oidc-request-audience flag was not set explicitly, default it to the spec.audience field of the JWTAuthenticator.
|
||||||
if flags.oidc.requestAudience == "" {
|
if flags.oidc.requestAudience == "" {
|
||||||
|
log.Info("discovered OIDC audience", "audience", auth.Spec.Audience)
|
||||||
flags.oidc.requestAudience = auth.Spec.Audience
|
flags.oidc.requestAudience = auth.Spec.Audience
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the --oidc-ca-bundle flags was not set explicitly, default it to the
|
// If the --oidc-ca-bundle flags was not set explicitly, default it to the
|
||||||
// spec.tls.certificateAuthorityData field of the JWTAuthenticator.
|
// spec.tls.certificateAuthorityData field of the JWTAuthenticator.
|
||||||
if *oidcCABundle == "" && auth.Spec.TLS != nil && auth.Spec.TLS.CertificateAuthorityData != "" {
|
if len(flags.oidc.caBundle) == 0 && auth.Spec.TLS != nil && auth.Spec.TLS.CertificateAuthorityData != "" {
|
||||||
decoded, err := base64.StdEncoding.DecodeString(auth.Spec.TLS.CertificateAuthorityData)
|
decoded, err := base64.StdEncoding.DecodeString(auth.Spec.TLS.CertificateAuthorityData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", auth.Name, err)
|
return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", auth.Name, err)
|
||||||
}
|
}
|
||||||
*oidcCABundle = string(decoded)
|
log.Info("discovered OIDC CA bundle", "roots", countCACerts(decoded))
|
||||||
|
flags.oidc.caBundle = decoded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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-authenticator-name="+flags.concierge.authenticatorName,
|
|
||||||
"--concierge-authenticator-type="+flags.concierge.authenticatorType,
|
|
||||||
"--concierge-endpoint="+v1Cluster.Server,
|
|
||||||
"--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(v1Cluster.CertificateAuthorityData),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCABundlePaths(paths []string) (string, error) {
|
func getConciergeFrontend(credentialIssuer *configv1alpha1.CredentialIssuer, mode conciergeModeFlag) (*configv1alpha1.CredentialIssuerFrontend, error) {
|
||||||
if len(paths) == 0 {
|
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||||
return "", nil
|
// Skip unhealthy strategies.
|
||||||
}
|
if strategy.Status != configv1alpha1.SuccessStrategyStatus {
|
||||||
blobs := make([][]byte, 0, len(paths))
|
continue
|
||||||
for _, p := range paths {
|
|
||||||
pem, err := ioutil.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
blobs = append(blobs, pem)
|
|
||||||
|
// Backfill the .status.strategies[].frontend field from .status.kubeConfigInfo for backwards compatibility.
|
||||||
|
if strategy.Type == configv1alpha1.KubeClusterSigningCertificateStrategyType && strategy.Frontend == nil && credentialIssuer.Status.KubeConfigInfo != nil {
|
||||||
|
strategy = *strategy.DeepCopy()
|
||||||
|
strategy.Frontend = &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: credentialIssuer.Status.KubeConfigInfo.Server,
|
||||||
|
CertificateAuthorityData: credentialIssuer.Status.KubeConfigInfo.CertificateAuthorityData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the strategy frontend is still nil, skip.
|
||||||
|
if strategy.Frontend == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip any unknown frontend types.
|
||||||
|
switch strategy.Frontend.Type {
|
||||||
|
case configv1alpha1.TokenCredentialRequestAPIFrontendType, configv1alpha1.ImpersonationProxyFrontendType:
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip strategies that don't match --concierge-mode.
|
||||||
|
if !mode.MatchesFrontend(strategy.Frontend) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return strategy.Frontend, nil
|
||||||
}
|
}
|
||||||
return string(bytes.Join(blobs, []byte("\n"))), nil
|
|
||||||
|
if mode == modeUnknown {
|
||||||
|
return nil, fmt.Errorf("could not autodiscover --concierge-mode")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("could not find successful Concierge strategy matching --concierge-mode=%s", mode.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.ExecConfig) clientcmdapi.Config {
|
func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.ExecConfig) clientcmdapi.Config {
|
||||||
@ -293,7 +473,33 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string) (metav1.Object, error) {
|
func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) {
|
||||||
|
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
// If the name is specified, get that object.
|
||||||
|
if name != "" {
|
||||||
|
return clientset.ConfigV1alpha1().CredentialIssuers().Get(ctx, name, metav1.GetOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise list all the available CredentialIssuers and hope there's just a single one
|
||||||
|
results, err := clientset.ConfigV1alpha1().CredentialIssuers().List(ctx, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list CredentialIssuer objects for autodiscovery: %w", err)
|
||||||
|
}
|
||||||
|
if len(results.Items) == 0 {
|
||||||
|
return nil, fmt.Errorf("no CredentialIssuers were found")
|
||||||
|
}
|
||||||
|
if len(results.Items) > 1 {
|
||||||
|
return nil, fmt.Errorf("multiple CredentialIssuers were found, so the --concierge-credential-issuer flag must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &results.Items[0]
|
||||||
|
log.Info("discovered CredentialIssuer", "name", result.Name)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log logr.Logger) (metav1.Object, error) {
|
||||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
|
|
||||||
@ -331,6 +537,12 @@ func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authN
|
|||||||
return nil, fmt.Errorf("no authenticators were found")
|
return nil, fmt.Errorf("no authenticators were found")
|
||||||
}
|
}
|
||||||
if len(results) > 1 {
|
if len(results) > 1 {
|
||||||
|
for _, jwtAuth := range jwtAuths.Items {
|
||||||
|
log.Info("found JWTAuthenticator", "name", jwtAuth.Name)
|
||||||
|
}
|
||||||
|
for _, webhook := range webhooks.Items {
|
||||||
|
log.Info("found WebhookAuthenticator", "name", webhook.Name)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified")
|
return nil, fmt.Errorf("multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified")
|
||||||
}
|
}
|
||||||
return results[0], nil
|
return results[0], nil
|
||||||
@ -353,9 +565,101 @@ func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Con
|
|||||||
if currentContextNameOverride != "" {
|
if currentContextNameOverride != "" {
|
||||||
contextName = currentContextNameOverride
|
contextName = currentContextNameOverride
|
||||||
}
|
}
|
||||||
context := currentKubeConfig.Contexts[contextName]
|
ctx := currentKubeConfig.Contexts[contextName]
|
||||||
if context == nil {
|
if ctx == nil {
|
||||||
return nil, fmt.Errorf("no such context %q", contextName)
|
return nil, fmt.Errorf("no such context %q", contextName)
|
||||||
}
|
}
|
||||||
return currentKubeConfig.Clusters[context.Cluster], nil
|
return currentKubeConfig.Clusters[ctx.Cluster], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log logr.Logger) error {
|
||||||
|
if flags.skipValidate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeContext := kubeconfig.Contexts[kubeconfig.CurrentContext]
|
||||||
|
if kubeContext == nil {
|
||||||
|
return fmt.Errorf("invalid kubeconfig (no context)")
|
||||||
|
}
|
||||||
|
cluster := kubeconfig.Clusters[kubeContext.Cluster]
|
||||||
|
if cluster == nil {
|
||||||
|
return fmt.Errorf("invalid kubeconfig (no cluster)")
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeconfigCA := x509.NewCertPool()
|
||||||
|
if !kubeconfigCA.AppendCertsFromPEM(cluster.CertificateAuthorityData) {
|
||||||
|
return fmt.Errorf("invalid kubeconfig (no certificateAuthorityData)")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
RootCAs: kubeconfigCA,
|
||||||
|
},
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
pingCluster := func() error {
|
||||||
|
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, cluster.Server, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not form request to validate cluster: %w", err)
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 500 {
|
||||||
|
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pingCluster()
|
||||||
|
if err == nil {
|
||||||
|
log.Info("validated connection to the cluster")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("could not immediately connect to the cluster but it may be initializing, will retry until timeout")
|
||||||
|
deadline, _ := ctx.Deadline()
|
||||||
|
attempts := 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
attempts++
|
||||||
|
err := pingCluster()
|
||||||
|
if err == nil {
|
||||||
|
log.Info("validated connection to the cluster", "attempts", attempts)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Error(err, "could not connect to cluster, retrying...", "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countCACerts(pemData []byte) int {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
pool.AppendCertsFromPEM(pemData)
|
||||||
|
return len(pool.Subjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool {
|
||||||
|
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||||
|
if strategy.Reason == configv1alpha1.PendingStrategyReason {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -20,19 +19,26 @@ import (
|
|||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
|
||||||
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||||
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
fakeconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
|
fakeconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
|
"go.pinniped.dev/internal/testutil/testlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetKubeconfig(t *testing.T) {
|
func TestGetKubeconfig(t *testing.T) {
|
||||||
testCA, err := certauthority.New(pkix.Name{CommonName: "Test CA"}, 1*time.Hour)
|
testOIDCCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
tmpdir := testutil.TempDir(t)
|
tmpdir := testutil.TempDir(t)
|
||||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
testOIDCCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||||
require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
require.NoError(t, ioutil.WriteFile(testOIDCCABundlePath, testOIDCCA.Bundle(), 0600))
|
||||||
|
|
||||||
|
testConciergeCA, err := certauthority.New("Test Concierge CA", 1*time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testConciergeCABundlePath := filepath.Join(tmpdir, "testconciergeca.pem")
|
||||||
|
require.NoError(t, ioutil.WriteFile(testConciergeCABundlePath, testConciergeCA.Bundle(), 0600))
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -42,6 +48,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
getClientsetErr error
|
getClientsetErr error
|
||||||
conciergeObjects []runtime.Object
|
conciergeObjects []runtime.Object
|
||||||
conciergeReactions []kubetesting.Reactor
|
conciergeReactions []kubetesting.Reactor
|
||||||
|
wantLogs []string
|
||||||
wantError bool
|
wantError bool
|
||||||
wantStdout string
|
wantStdout string
|
||||||
wantStderr string
|
wantStderr string
|
||||||
@ -61,11 +68,16 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||||
--concierge-authenticator-name string Concierge authenticator name (default: autodiscover)
|
--concierge-authenticator-name string Concierge authenticator name (default: autodiscover)
|
||||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)
|
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)
|
||||||
|
--concierge-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge
|
||||||
|
--concierge-credential-issuer string Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover)
|
||||||
|
--concierge-endpoint string API base for the Concierge endpoint
|
||||||
|
--concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI)
|
||||||
|
--concierge-skip-wait Skip waiting for any pending Concierge strategies to become ready (default: false)
|
||||||
-h, --help help for kubeconfig
|
-h, --help help for kubeconfig
|
||||||
--kubeconfig string Path to kubeconfig file
|
--kubeconfig string Path to kubeconfig file
|
||||||
--kubeconfig-context string Kubeconfig context name (default: current active context)
|
--kubeconfig-context string Kubeconfig context name (default: current active context)
|
||||||
--no-concierge Generate a configuration which does not use the concierge, but sends the credential to the cluster directly
|
--no-concierge Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly
|
||||||
--oidc-ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
--oidc-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||||
--oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli")
|
--oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli")
|
||||||
--oidc-issuer string OpenID Connect issuer URL (default: autodiscover)
|
--oidc-issuer string OpenID Connect issuer URL (default: autodiscover)
|
||||||
--oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
--oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||||
@ -73,8 +85,11 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
--oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience])
|
--oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience])
|
||||||
--oidc-session-cache string Path to OpenID Connect session cache file
|
--oidc-session-cache string Path to OpenID Connect session cache file
|
||||||
--oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL)
|
--oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL)
|
||||||
|
-o, --output string Output file path (default: stdout)
|
||||||
|
--skip-validation Skip final validation of the kubeconfig (default: false)
|
||||||
--static-token string Instead of doing an OIDC-based login, specify a static token
|
--static-token string Instead of doing an OIDC-based login, specify a static token
|
||||||
--static-token-env string Instead of doing an OIDC-based login, read a static token from the environment
|
--static-token-env string Instead of doing an OIDC-based login, read a static token from the environment
|
||||||
|
--timeout duration Timeout for autodiscovery and validation (default 10m0s)
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -87,13 +102,24 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid CA bundle paths",
|
name: "invalid OIDC CA bundle path",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--oidc-ca-bundle", "./does/not/exist",
|
"--oidc-ca-bundle", "./does/not/exist",
|
||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: could not read --oidc-ca-bundle: open ./does/not/exist: no such file or directory
|
Error: invalid argument "./does/not/exist" for "--oidc-ca-bundle" flag: could not read CA bundle path: open ./does/not/exist: no such file or directory
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid Concierge CA bundle",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
"--concierge-ca-bundle", "./does/not/exist",
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
wantStderr: here.Doc(`
|
||||||
|
Error: invalid argument "./does/not/exist" for "--concierge-ca-bundle" flag: could not read CA bundle path: open ./does/not/exist: no such file or directory
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -128,6 +154,31 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
Error: could not configure Kubernetes client: some kube error
|
Error: could not configure Kubernetes client: some kube error
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "no credentialissuers",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
wantStderr: here.Doc(`
|
||||||
|
Error: no CredentialIssuers were found
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "credentialissuer not found",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
"--concierge-credential-issuer", "does-not-exist",
|
||||||
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
wantStderr: here.Doc(`
|
||||||
|
Error: credentialissuers.config.concierge.pinniped.dev "does-not-exist" not found
|
||||||
|
`),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "webhook authenticator not found",
|
name: "webhook authenticator not found",
|
||||||
args: []string{
|
args: []string{
|
||||||
@ -135,6 +186,12 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "webhook",
|
"--concierge-authenticator-type", "webhook",
|
||||||
"--concierge-authenticator-name", "test-authenticator",
|
"--concierge-authenticator-name", "test-authenticator",
|
||||||
},
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found
|
Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found
|
||||||
@ -147,6 +204,12 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "jwt",
|
"--concierge-authenticator-type", "jwt",
|
||||||
"--concierge-authenticator-name", "test-authenticator",
|
"--concierge-authenticator-name", "test-authenticator",
|
||||||
},
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found
|
Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found
|
||||||
@ -159,6 +222,12 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "invalid",
|
"--concierge-authenticator-type", "invalid",
|
||||||
"--concierge-authenticator-name", "test-authenticator",
|
"--concierge-authenticator-name", "test-authenticator",
|
||||||
},
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt"
|
Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt"
|
||||||
@ -169,6 +238,12 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
},
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
},
|
||||||
conciergeReactions: []kubetesting.Reactor{
|
conciergeReactions: []kubetesting.Reactor{
|
||||||
&kubetesting.SimpleReactor{
|
&kubetesting.SimpleReactor{
|
||||||
Verb: "*",
|
Verb: "*",
|
||||||
@ -188,6 +263,9 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
},
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
conciergeReactions: []kubetesting.Reactor{
|
conciergeReactions: []kubetesting.Reactor{
|
||||||
&kubetesting.SimpleReactor{
|
&kubetesting.SimpleReactor{
|
||||||
Verb: "*",
|
Verb: "*",
|
||||||
@ -197,6 +275,9 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error
|
Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error
|
||||||
@ -207,6 +288,12 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
},
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: no authenticators were found
|
Error: no authenticators were found
|
||||||
@ -218,27 +305,145 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||||
&conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-1"}},
|
&conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-1"}},
|
||||||
&conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-2"}},
|
&conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-2"}},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}},
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-4"}},
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-4"}},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-1"`,
|
||||||
|
`"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-2"`,
|
||||||
|
`"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-3"`,
|
||||||
|
`"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-4"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified
|
Error: multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "autodetect webhook authenticator, bad credential issuer with only failing strategy",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: "SomeType",
|
||||||
|
Status: configv1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: "SomeReason",
|
||||||
|
Message: "Some message",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="found CredentialIssuer strategy" "message"="Some message" "reason"="SomeReason" "status"="Error" "type"="SomeType"`,
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
wantStderr: here.Doc(`
|
||||||
|
Error: could not autodiscover --concierge-mode
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autodetect webhook authenticator, bad credential issuer with invalid impersonation CA",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{
|
||||||
|
{
|
||||||
|
Type: "SomeBrokenType",
|
||||||
|
Status: configv1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: "SomeFailureReason",
|
||||||
|
Message: "Some error message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "SomeUnknownType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeReason",
|
||||||
|
Message: "Some error message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: "SomeUnknownFrontendType",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "SomeType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeReason",
|
||||||
|
Message: "Some message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.ImpersonationProxyFrontendType,
|
||||||
|
ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{
|
||||||
|
Endpoint: "https://impersonation-endpoint",
|
||||||
|
CertificateAuthorityData: "invalid-base-64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-endpoint"`,
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
wantStderr: here.Doc(`
|
||||||
|
Error: autodiscovered Concierge CA bundle is invalid: illegal base64 data at input byte 7
|
||||||
|
`),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "autodetect webhook authenticator, missing --oidc-issuer",
|
name: "autodetect webhook authenticator, missing --oidc-issuer",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: "SomeType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeReason",
|
||||||
|
Message: "Some message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: "https://concierge-endpoint",
|
||||||
|
CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||||
|
`"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: could not autodiscover --oidc-issuer, and none was provided
|
Error: could not autodiscover --oidc-issuer and none was provided
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -247,15 +452,44 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{
|
||||||
|
Server: "https://concierge-endpoint",
|
||||||
|
CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==",
|
||||||
|
},
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: configv1alpha1.FetchedKeyStrategyReason,
|
||||||
|
Message: "Successfully fetched key",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
// Simulate a previous version of CredentialIssuer that's missing this Frontend field.
|
||||||
|
Frontend: nil,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.JWTAuthenticator{
|
&conciergev1alpha1.JWTAuthenticator{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||||
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: "https://test-issuer.example.com",
|
||||||
|
Audience: "some-test-audience",
|
||||||
TLS: &conciergev1alpha1.TLSSpec{
|
TLS: &conciergev1alpha1.TLSSpec{
|
||||||
CertificateAuthorityData: "invalid-base64",
|
CertificateAuthorityData: "invalid-base64",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||||
|
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://test-issuer.example.com"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC audience" "audience"="some-test-audience"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7
|
Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7
|
||||||
@ -269,21 +503,45 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
"--static-token-env", "TEST_TOKEN",
|
"--static-token-env", "TEST_TOKEN",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: configv1alpha1.ImpersonationProxyStrategyType,
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: configv1alpha1.ListeningStrategyReason,
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.ImpersonationProxyFrontendType,
|
||||||
|
ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{
|
||||||
|
Endpoint: "https://impersonation-proxy-endpoint.example.com",
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.example.com"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`,
|
||||||
|
`"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`,
|
||||||
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: only one of --static-token and --static-token-env can be specified
|
Error: only one of --static-token and --static-token-env can be specified
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid api group suffix",
|
name: "invalid API group suffix",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--concierge-api-group-suffix", ".starts.with.dot",
|
"--concierge-api-group-suffix", ".starts.with.dot",
|
||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: invalid api group suffix: 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])?)*')
|
Error: invalid API group suffix: 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])?)*')
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -291,10 +549,35 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
"--static-token", "test-token",
|
"--static-token", "test-token",
|
||||||
|
"--skip-validation",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: configv1alpha1.FetchedKeyStrategyReason,
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: "https://concierge-endpoint.example.com",
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||||
|
`"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`,
|
||||||
|
},
|
||||||
wantStdout: here.Doc(`
|
wantStdout: here.Doc(`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
@ -335,10 +618,35 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
"--static-token-env", "TEST_TOKEN",
|
"--static-token-env", "TEST_TOKEN",
|
||||||
|
"--skip-validation",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: configv1alpha1.FetchedKeyStrategyReason,
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: "https://concierge-endpoint.example.com",
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||||
|
`"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`,
|
||||||
|
},
|
||||||
wantStdout: here.Doc(`
|
wantStdout: here.Doc(`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
@ -378,19 +686,47 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
name: "autodetect JWT authenticator",
|
name: "autodetect JWT authenticator",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
"--skip-validation",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: configv1alpha1.FetchedKeyStrategyReason,
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: "https://concierge-endpoint.example.com",
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.JWTAuthenticator{
|
&conciergev1alpha1.JWTAuthenticator{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||||
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||||
Issuer: "https://example.com/issuer",
|
Issuer: "https://example.com/issuer",
|
||||||
Audience: "test-audience",
|
Audience: "test-audience",
|
||||||
TLS: &conciergev1alpha1.TLSSpec{
|
TLS: &conciergev1alpha1.TLSSpec{
|
||||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testCA.Bundle()),
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||||
|
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`,
|
||||||
|
},
|
||||||
wantStdout: here.Docf(`
|
wantStdout: here.Docf(`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
@ -428,34 +764,57 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
env: []
|
env: []
|
||||||
provideClusterInfo: true
|
provideClusterInfo: true
|
||||||
`, base64.StdEncoding.EncodeToString(testCA.Bundle())),
|
`, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "autodetect nothing, set a bunch of options",
|
name: "autodetect nothing, set a bunch of options",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
"--concierge-credential-issuer", "test-credential-issuer",
|
||||||
"--concierge-api-group-suffix", "tuna.io",
|
"--concierge-api-group-suffix", "tuna.io",
|
||||||
"--concierge-authenticator-type", "webhook",
|
"--concierge-authenticator-type", "webhook",
|
||||||
"--concierge-authenticator-name", "test-authenticator",
|
"--concierge-authenticator-name", "test-authenticator",
|
||||||
|
"--concierge-mode", "TokenCredentialRequestAPI",
|
||||||
|
"--concierge-endpoint", "https://explicit-concierge-endpoint.example.com",
|
||||||
|
"--concierge-ca-bundle", testConciergeCABundlePath,
|
||||||
"--oidc-issuer", "https://example.com/issuer",
|
"--oidc-issuer", "https://example.com/issuer",
|
||||||
"--oidc-skip-browser",
|
"--oidc-skip-browser",
|
||||||
"--oidc-listen-port", "1234",
|
"--oidc-listen-port", "1234",
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testOIDCCABundlePath,
|
||||||
"--oidc-session-cache", "/path/to/cache/dir/sessions.yaml",
|
"--oidc-session-cache", "/path/to/cache/dir/sessions.yaml",
|
||||||
"--oidc-debug-session-cache",
|
"--oidc-debug-session-cache",
|
||||||
"--oidc-request-audience", "test-audience",
|
"--oidc-request-audience", "test-audience",
|
||||||
|
"--skip-validation",
|
||||||
},
|
},
|
||||||
conciergeObjects: []runtime.Object{
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{{
|
||||||
|
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: configv1alpha1.FetchedKeyStrategyReason,
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: "https://concierge-endpoint.example.com",
|
||||||
|
CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
&conciergev1alpha1.WebhookAuthenticator{
|
&conciergev1alpha1.WebhookAuthenticator{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantLogs: nil,
|
||||||
wantStdout: here.Docf(`
|
wantStdout: here.Docf(`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
certificate-authority-data: %s
|
||||||
server: https://fake-server-url-value
|
server: https://explicit-concierge-endpoint.example.com
|
||||||
name: pinniped
|
name: pinniped
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
||||||
@ -477,8 +836,8 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-api-group-suffix=tuna.io
|
- --concierge-api-group-suffix=tuna.io
|
||||||
- --concierge-authenticator-name=test-authenticator
|
- --concierge-authenticator-name=test-authenticator
|
||||||
- --concierge-authenticator-type=webhook
|
- --concierge-authenticator-type=webhook
|
||||||
- --concierge-endpoint=https://fake-server-url-value
|
- --concierge-endpoint=https://explicit-concierge-endpoint.example.com
|
||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=%s
|
||||||
- --issuer=https://example.com/issuer
|
- --issuer=https://example.com/issuer
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience
|
||||||
@ -491,13 +850,229 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
env: []
|
env: []
|
||||||
provideClusterInfo: true
|
provideClusterInfo: true
|
||||||
`, base64.StdEncoding.EncodeToString(testCA.Bundle())),
|
`,
|
||||||
|
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()),
|
||||||
|
),
|
||||||
wantAPIGroupSuffix: "tuna.io",
|
wantAPIGroupSuffix: "tuna.io",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "configure impersonation proxy with autodiscovered JWT authenticator",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
"--concierge-mode", "ImpersonationProxy",
|
||||||
|
"--skip-validation",
|
||||||
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{
|
||||||
|
// This TokenCredentialRequestAPI strategy would normally be chosen, but
|
||||||
|
// --concierge-mode=ImpersonationProxy should force it to be skipped.
|
||||||
|
{
|
||||||
|
Type: "SomeType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeReason",
|
||||||
|
Message: "Some message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||||
|
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||||
|
Server: "https://token-credential-request-api-endpoint.test",
|
||||||
|
CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// The endpoint and CA from this impersonation proxy strategy should be autodiscovered.
|
||||||
|
{
|
||||||
|
Type: "SomeOtherType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeOtherReason",
|
||||||
|
Message: "Some other message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.ImpersonationProxyFrontendType,
|
||||||
|
ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{
|
||||||
|
Endpoint: "https://impersonation-proxy-endpoint.test",
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&conciergev1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||||
|
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: "https://example.com/issuer",
|
||||||
|
Audience: "test-audience",
|
||||||
|
TLS: &conciergev1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`,
|
||||||
|
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`,
|
||||||
|
},
|
||||||
|
wantStdout: here.Docf(`
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: %s
|
||||||
|
server: https://impersonation-proxy-endpoint.test
|
||||||
|
name: pinniped
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: pinniped
|
||||||
|
user: pinniped
|
||||||
|
name: pinniped
|
||||||
|
current-context: pinniped
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: pinniped
|
||||||
|
user:
|
||||||
|
exec:
|
||||||
|
apiVersion: client.authentication.k8s.io/v1beta1
|
||||||
|
args:
|
||||||
|
- login
|
||||||
|
- oidc
|
||||||
|
- --enable-concierge
|
||||||
|
- --concierge-api-group-suffix=pinniped.dev
|
||||||
|
- --concierge-authenticator-name=test-authenticator
|
||||||
|
- --concierge-authenticator-type=jwt
|
||||||
|
- --concierge-endpoint=https://impersonation-proxy-endpoint.test
|
||||||
|
- --concierge-ca-bundle-data=%s
|
||||||
|
- --issuer=https://example.com/issuer
|
||||||
|
- --client-id=pinniped-cli
|
||||||
|
- --scopes=offline_access,openid,pinniped:request-audience
|
||||||
|
- --ca-bundle-data=%s
|
||||||
|
- --request-audience=test-audience
|
||||||
|
command: '.../path/to/pinniped'
|
||||||
|
env: []
|
||||||
|
provideClusterInfo: true
|
||||||
|
`,
|
||||||
|
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||||
|
base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autodetect impersonation proxy with autodiscovered JWT authenticator",
|
||||||
|
args: []string{
|
||||||
|
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||||
|
"--skip-validation",
|
||||||
|
},
|
||||||
|
conciergeObjects: []runtime.Object{
|
||||||
|
&configv1alpha1.CredentialIssuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{
|
||||||
|
{
|
||||||
|
Type: "SomeType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeReason",
|
||||||
|
Message: "Some message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.ImpersonationProxyFrontendType,
|
||||||
|
ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{
|
||||||
|
Endpoint: "https://impersonation-proxy-endpoint.test",
|
||||||
|
CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "SomeOtherType",
|
||||||
|
Status: configv1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: "SomeOtherReason",
|
||||||
|
Message: "Some other message",
|
||||||
|
LastUpdateTime: metav1.Now(),
|
||||||
|
Frontend: &configv1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: configv1alpha1.ImpersonationProxyFrontendType,
|
||||||
|
ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{
|
||||||
|
Endpoint: "https://some-other-impersonation-endpoint",
|
||||||
|
CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&conciergev1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||||
|
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: "https://example.com/issuer",
|
||||||
|
Audience: "test-audience",
|
||||||
|
TLS: &conciergev1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`,
|
||||||
|
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||||
|
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`,
|
||||||
|
`"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`,
|
||||||
|
},
|
||||||
|
wantStdout: here.Docf(`
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: dGVzdC1jb25jaWVyZ2UtY2E=
|
||||||
|
server: https://impersonation-proxy-endpoint.test
|
||||||
|
name: pinniped
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: pinniped
|
||||||
|
user: pinniped
|
||||||
|
name: pinniped
|
||||||
|
current-context: pinniped
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: pinniped
|
||||||
|
user:
|
||||||
|
exec:
|
||||||
|
apiVersion: client.authentication.k8s.io/v1beta1
|
||||||
|
args:
|
||||||
|
- login
|
||||||
|
- oidc
|
||||||
|
- --enable-concierge
|
||||||
|
- --concierge-api-group-suffix=pinniped.dev
|
||||||
|
- --concierge-authenticator-name=test-authenticator
|
||||||
|
- --concierge-authenticator-type=jwt
|
||||||
|
- --concierge-endpoint=https://impersonation-proxy-endpoint.test
|
||||||
|
- --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E=
|
||||||
|
- --issuer=https://example.com/issuer
|
||||||
|
- --client-id=pinniped-cli
|
||||||
|
- --scopes=offline_access,openid,pinniped:request-audience
|
||||||
|
- --ca-bundle-data=%s
|
||||||
|
- --request-audience=test-audience
|
||||||
|
command: '.../path/to/pinniped'
|
||||||
|
env: []
|
||||||
|
provideClusterInfo: true
|
||||||
|
`, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testLog := testlogger.New(t)
|
||||||
cmd := kubeconfigCommand(kubeconfigDeps{
|
cmd := kubeconfigCommand(kubeconfigDeps{
|
||||||
getPathToSelf: func() (string, error) {
|
getPathToSelf: func() (string, error) {
|
||||||
if tt.getPathToSelfErr != nil {
|
if tt.getPathToSelfErr != nil {
|
||||||
@ -516,10 +1091,11 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
fake := fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects...)
|
fake := fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects...)
|
||||||
if len(tt.conciergeReactions) > 0 {
|
if len(tt.conciergeReactions) > 0 {
|
||||||
fake.ReactionChain = tt.conciergeReactions
|
fake.ReactionChain = append(tt.conciergeReactions, fake.ReactionChain...)
|
||||||
}
|
}
|
||||||
return fake, nil
|
return fake, nil
|
||||||
},
|
},
|
||||||
|
log: testLog,
|
||||||
})
|
})
|
||||||
require.NotNil(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
|
|
||||||
@ -533,6 +1109,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
testLog.Expect(tt.wantLogs)
|
||||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||||
})
|
})
|
||||||
|
@ -85,15 +85,15 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
|||||||
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
||||||
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
||||||
cmd.Flags().StringSliceVar(&flags.caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
cmd.Flags().StringSliceVar(&flags.caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||||
cmd.Flags().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 endcoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)")
|
cmd.Flags().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)")
|
||||||
cmd.Flags().BoolVar(&flags.debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache")
|
cmd.Flags().BoolVar(&flags.debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache")
|
||||||
cmd.Flags().StringVar(&flags.requestAudience, "request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
cmd.Flags().StringVar(&flags.requestAudience, "request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
||||||
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Exchange the OIDC ID token with the Pinniped concierge during login")
|
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login")
|
||||||
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
||||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
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.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
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", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||||
|
|
||||||
mustMarkHidden(cmd, "debug-session-cache")
|
mustMarkHidden(cmd, "debug-session-cache")
|
||||||
@ -144,7 +144,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
|||||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid concierge parameters: %w", err)
|
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,17 +172,18 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
|||||||
cred := tokenCredential(token)
|
cred := tokenCredential(token)
|
||||||
|
|
||||||
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if concierge != nil {
|
if concierge != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
|
cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not complete concierge credential exchange: %w", err)
|
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
||||||
pool := x509.NewCertPool()
|
pool := x509.NewCertPool()
|
||||||
for _, p := range caBundlePaths {
|
for _, p := range caBundlePaths {
|
||||||
|
@ -6,7 +6,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -29,7 +28,7 @@ import (
|
|||||||
func TestLoginOIDCCommand(t *testing.T) {
|
func TestLoginOIDCCommand(t *testing.T) {
|
||||||
cfgDir := mustGetConfigDir()
|
cfgDir := mustGetConfigDir()
|
||||||
|
|
||||||
testCA, err := certauthority.New(pkix.Name{CommonName: "Test CA"}, 1*time.Hour)
|
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
tmpdir := testutil.TempDir(t)
|
tmpdir := testutil.TempDir(t)
|
||||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||||
@ -58,14 +57,14 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
--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)
|
--ca-bundle-data strings Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
|
||||||
--client-id string OpenID Connect client ID (default "pinniped-cli")
|
--client-id string OpenID Connect client ID (default "pinniped-cli")
|
||||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||||
--concierge-authenticator-name string Concierge authenticator name
|
--concierge-authenticator-name string Concierge authenticator name
|
||||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
--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
|
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||||
--concierge-endpoint string API base for the Pinniped concierge endpoint
|
--concierge-endpoint string API base for the Concierge endpoint
|
||||||
--enable-concierge Exchange the OIDC ID token with the Pinniped concierge during login
|
--enable-concierge Use the Concierge to login
|
||||||
-h, --help help for oidc
|
-h, --help help for oidc
|
||||||
--issuer string OpenID Connect issuer URL
|
--issuer string OpenID Connect issuer URL
|
||||||
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||||
@ -92,7 +91,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: invalid concierge parameters: endpoint must not be empty
|
Error: invalid Concierge parameters: endpoint must not be empty
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -120,7 +119,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid api group suffix",
|
name: "invalid API group suffix",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--issuer", "test-issuer",
|
"--issuer", "test-issuer",
|
||||||
"--enable-concierge",
|
"--enable-concierge",
|
||||||
@ -131,7 +130,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: invalid concierge parameters: invalid api group suffix: 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])?)*')
|
Error: invalid Concierge parameters: invalid API group suffix: 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])?)*')
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -161,7 +160,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
wantOptionsCount: 3,
|
wantOptionsCount: 3,
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: could not complete concierge credential exchange: some concierge error
|
Error: could not complete Concierge credential exchange: some concierge error
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -62,13 +62,14 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
|
|||||||
)
|
)
|
||||||
cmd.Flags().StringVar(&flags.staticToken, "token", "", "Static token to present during login")
|
cmd.Flags().StringVar(&flags.staticToken, "token", "", "Static token to present during login")
|
||||||
cmd.Flags().StringVar(&flags.staticTokenEnvName, "token-env", "", "Environment variable containing a static token")
|
cmd.Flags().StringVar(&flags.staticTokenEnvName, "token-env", "", "Environment variable containing a static token")
|
||||||
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Exchange the token with the Pinniped concierge during login")
|
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login")
|
||||||
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
||||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
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.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
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", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||||
|
|
||||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
|
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
|
||||||
|
|
||||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||||
@ -92,7 +93,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
|
|||||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid concierge parameters: %w", err)
|
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +113,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
|
|||||||
}
|
}
|
||||||
cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}})
|
cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}})
|
||||||
|
|
||||||
// Exchange that token with the concierge, if configured.
|
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
||||||
if concierge != nil {
|
if concierge != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -120,7 +121,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
|
|||||||
var err error
|
var err error
|
||||||
cred, err = deps.exchangeToken(ctx, concierge, token)
|
cred, err = deps.exchangeToken(ctx, concierge, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not complete concierge credential exchange: %w", err)
|
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json.NewEncoder(out).Encode(cred)
|
return json.NewEncoder(out).Encode(cred)
|
||||||
|
@ -6,7 +6,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -24,7 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLoginStaticCommand(t *testing.T) {
|
func TestLoginStaticCommand(t *testing.T) {
|
||||||
testCA, err := certauthority.New(pkix.Name{CommonName: "Test CA"}, 1*time.Hour)
|
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
tmpdir := testutil.TempDir(t)
|
tmpdir := testutil.TempDir(t)
|
||||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||||
@ -54,9 +53,9 @@ func TestLoginStaticCommand(t *testing.T) {
|
|||||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||||
--concierge-authenticator-name string Concierge authenticator name
|
--concierge-authenticator-name string Concierge authenticator name
|
||||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
--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
|
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||||
--concierge-endpoint string API base for the Pinniped concierge endpoint
|
--concierge-endpoint string API base for the Concierge endpoint
|
||||||
--enable-concierge Exchange the token with the Pinniped concierge during login
|
--enable-concierge Use the Concierge to login
|
||||||
-h, --help help for static
|
-h, --help help for static
|
||||||
--token string Static token to present during login
|
--token string Static token to present during login
|
||||||
--token-env string Environment variable containing a static token
|
--token-env string Environment variable containing a static token
|
||||||
@ -78,7 +77,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: invalid concierge parameters: endpoint must not be empty
|
Error: invalid Concierge parameters: endpoint must not be empty
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -126,11 +125,11 @@ func TestLoginStaticCommand(t *testing.T) {
|
|||||||
conciergeErr: fmt.Errorf("some concierge error"),
|
conciergeErr: fmt.Errorf("some concierge error"),
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: could not complete concierge credential exchange: some concierge error
|
Error: could not complete Concierge credential exchange: some concierge error
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid api group suffix",
|
name: "invalid API group suffix",
|
||||||
args: []string{
|
args: []string{
|
||||||
"--token", "test-token",
|
"--token", "test-token",
|
||||||
"--enable-concierge",
|
"--enable-concierge",
|
||||||
@ -141,7 +140,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
wantStderr: here.Doc(`
|
wantStderr: here.Doc(`
|
||||||
Error: invalid concierge parameters: invalid api group suffix: 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])?)*')
|
Error: invalid Concierge parameters: invalid API group suffix: 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])?)*')
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -15,18 +15,13 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
|
||||||
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
|
||||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
conciergescheme "go.pinniped.dev/internal/concierge/scheme"
|
||||||
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
|
||||||
"go.pinniped.dev/internal/groupsuffix"
|
"go.pinniped.dev/internal/groupsuffix"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/plog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint: gochecknoinits
|
//nolint: gochecknoinits
|
||||||
@ -163,7 +158,7 @@ func writeWhoamiOutputYAML(output io.Writer, apiGroupSuffix string, whoAmI *iden
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serialize(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest, contentType string) error {
|
func serialize(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest, contentType string) error {
|
||||||
scheme, _, identityGV := conciergeschemeNew(apiGroupSuffix)
|
scheme, _, identityGV := conciergescheme.New(apiGroupSuffix)
|
||||||
codecs := serializer.NewCodecFactory(scheme)
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
respInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), contentType)
|
respInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), contentType)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -194,110 +189,3 @@ func prettyStrings(ss []string) string {
|
|||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// conciergeschemeNew is a temporary private function to stand in place for
|
|
||||||
// "go.pinniped.dev/internal/concierge/scheme".New until the later function is merged to main.
|
|
||||||
func conciergeschemeNew(apiGroupSuffix string) (_ *runtime.Scheme, login, identity schema.GroupVersion) {
|
|
||||||
// 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 suffix
|
|
||||||
if apiGroupSuffix == groupsuffix.PinnipedDefaultSuffix {
|
|
||||||
schemeBuilder := runtime.NewSchemeBuilder(
|
|
||||||
loginv1alpha1.AddToScheme,
|
|
||||||
loginapi.AddToScheme,
|
|
||||||
identityv1alpha1.AddToScheme,
|
|
||||||
identityapi.AddToScheme,
|
|
||||||
)
|
|
||||||
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
|
||||||
return scheme, loginv1alpha1.SchemeGroupVersion, identityv1alpha1.SchemeGroupVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
loginConciergeGroupData, identityConciergeGroupData := groupsuffix.ConciergeAggregatedGroups(apiGroupSuffix)
|
|
||||||
|
|
||||||
addToSchemeAtNewGroup(scheme, loginv1alpha1.GroupName, loginConciergeGroupData.Group, loginv1alpha1.AddToScheme, loginapi.AddToScheme)
|
|
||||||
addToSchemeAtNewGroup(scheme, identityv1alpha1.GroupName, identityConciergeGroupData.Group, identityv1alpha1.AddToScheme, identityapi.AddToScheme)
|
|
||||||
|
|
||||||
// manually register conversions and defaulting into the correct scheme since we cannot directly call AddToScheme
|
|
||||||
schemeBuilder := runtime.NewSchemeBuilder(
|
|
||||||
loginv1alpha1.RegisterConversions,
|
|
||||||
loginv1alpha1.RegisterDefaults,
|
|
||||||
identityv1alpha1.RegisterConversions,
|
|
||||||
identityv1alpha1.RegisterDefaults,
|
|
||||||
)
|
|
||||||
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
|
||||||
|
|
||||||
// we do not want to return errors from the scheme and instead would prefer to defer
|
|
||||||
// to the REST storage layer for consistency. The simplest way to do this is to force
|
|
||||||
// a cache miss from the authenticator cache. Kube API groups are validated via the
|
|
||||||
// IsDNS1123Subdomain func thus we can easily create a group that is guaranteed never
|
|
||||||
// to be in the authenticator cache. Add a timestamp just to be extra sure.
|
|
||||||
const authenticatorCacheMissPrefix = "_INVALID_API_GROUP_"
|
|
||||||
authenticatorCacheMiss := authenticatorCacheMissPrefix + time.Now().UTC().String()
|
|
||||||
|
|
||||||
// we do not have any defaulting functions for *loginv1alpha1.TokenCredentialRequest
|
|
||||||
// today, but we may have some in the future. Calling AddTypeDefaultingFunc overwrites
|
|
||||||
// any previously registered defaulting function. Thus to make sure that we catch
|
|
||||||
// a situation where we add a defaulting func, we attempt to call it here with a nil
|
|
||||||
// *loginv1alpha1.TokenCredentialRequest. This will do nothing when there is no
|
|
||||||
// defaulting func registered, but it will almost certainly panic if one is added.
|
|
||||||
scheme.Default((*loginv1alpha1.TokenCredentialRequest)(nil))
|
|
||||||
|
|
||||||
// on incoming requests, restore the authenticator API group to the standard group
|
|
||||||
// note that we are responsible for duplicating this logic for every external API version
|
|
||||||
scheme.AddTypeDefaultingFunc(&loginv1alpha1.TokenCredentialRequest{}, func(obj interface{}) {
|
|
||||||
credentialRequest := obj.(*loginv1alpha1.TokenCredentialRequest)
|
|
||||||
|
|
||||||
if credentialRequest.Spec.Authenticator.APIGroup == nil {
|
|
||||||
// force a cache miss because this is an invalid request
|
|
||||||
plog.Debug("invalid token credential request, nil group", "authenticator", credentialRequest.Spec.Authenticator)
|
|
||||||
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
restoredGroup, ok := groupsuffix.Unreplace(*credentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix)
|
|
||||||
if !ok {
|
|
||||||
// force a cache miss because this is an invalid request
|
|
||||||
plog.Debug("invalid token credential request, wrong group", "authenticator", credentialRequest.Spec.Authenticator)
|
|
||||||
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
credentialRequest.Spec.Authenticator.APIGroup = &restoredGroup
|
|
||||||
})
|
|
||||||
|
|
||||||
return scheme, schema.GroupVersion(loginConciergeGroupData), schema.GroupVersion(identityConciergeGroupData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addToSchemeAtNewGroup(scheme *runtime.Scheme, oldGroup, newGroup string, funcs ...func(*runtime.Scheme) error) {
|
|
||||||
// we need a temporary place to register our types to avoid double registering them
|
|
||||||
tmpScheme := runtime.NewScheme()
|
|
||||||
schemeBuilder := runtime.NewSchemeBuilder(funcs...)
|
|
||||||
utilruntime.Must(schemeBuilder.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 != oldGroup {
|
|
||||||
panic(fmt.Errorf("tmp scheme has type not in the old aggregated API group %s: %s", oldGroup, gvk)) // programmer error
|
|
||||||
}
|
|
||||||
|
|
||||||
obj, err := tmpScheme.New(gvk)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // programmer error, scheme internal code is broken
|
|
||||||
}
|
|
||||||
newGVK := schema.GroupVersionKind{
|
|
||||||
Group: newGroup,
|
|
||||||
Version: gvk.Version,
|
|
||||||
Kind: gvk.Kind,
|
|
||||||
}
|
|
||||||
|
|
||||||
// register the existing type but with the new group in the correct scheme
|
|
||||||
scheme.AddKnownTypeWithName(newGVK, obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -67,14 +67,34 @@ spec:
|
|||||||
description: Frontend describes how clients can connect using
|
description: Frontend describes how clients can connect using
|
||||||
this strategy.
|
this strategy.
|
||||||
properties:
|
properties:
|
||||||
|
impersonationProxyInfo:
|
||||||
|
description: ImpersonationProxyInfo describes the parameters
|
||||||
|
for the impersonation proxy on this Concierge. This field
|
||||||
|
is only set when Type is "ImpersonationProxy".
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
|
PEM CA bundle of the impersonation proxy.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
endpoint:
|
||||||
|
description: Endpoint is the HTTPS endpoint of the impersonation
|
||||||
|
proxy.
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^https://
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- certificateAuthorityData
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
tokenCredentialRequestInfo:
|
tokenCredentialRequestInfo:
|
||||||
description: TokenCredentialRequestAPIInfo describes the
|
description: TokenCredentialRequestAPIInfo describes the
|
||||||
parameters for the TokenCredentialRequest API on this
|
parameters for the TokenCredentialRequest API on this
|
||||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
properties:
|
properties:
|
||||||
certificateAuthorityData:
|
certificateAuthorityData:
|
||||||
description: CertificateAuthorityData is the Kubernetes
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
API server CA bundle.
|
Kubernetes API server CA bundle.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
server:
|
server:
|
||||||
@ -91,6 +111,7 @@ spec:
|
|||||||
can use with a strategy.
|
can use with a strategy.
|
||||||
enum:
|
enum:
|
||||||
- TokenCredentialRequestAPI
|
- TokenCredentialRequestAPI
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@ -106,8 +127,13 @@ spec:
|
|||||||
reason:
|
reason:
|
||||||
description: Reason for the current status.
|
description: Reason for the current status.
|
||||||
enum:
|
enum:
|
||||||
- FetchedKey
|
- Listening
|
||||||
|
- Pending
|
||||||
|
- Disabled
|
||||||
|
- ErrorDuringSetup
|
||||||
- CouldNotFetchKey
|
- CouldNotFetchKey
|
||||||
|
- CouldNotGetClusterInfo
|
||||||
|
- FetchedKey
|
||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
description: Status of the attempted integration strategy.
|
description: Status of the attempted integration strategy.
|
||||||
@ -119,6 +145,7 @@ spec:
|
|||||||
description: Type of integration attempted.
|
description: Type of integration attempted.
|
||||||
enum:
|
enum:
|
||||||
- KubeClusterSigningCertificate
|
- KubeClusterSigningCertificate
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- lastUpdateTime
|
- lastUpdateTime
|
||||||
|
@ -42,6 +42,11 @@ data:
|
|||||||
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
|
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
|
||||||
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)
|
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)
|
||||||
apiService: (@= defaultResourceNameWithSuffix("api") @)
|
apiService: (@= defaultResourceNameWithSuffix("api") @)
|
||||||
|
impersonationConfigMap: (@= defaultResourceNameWithSuffix("impersonation-proxy-config") @)
|
||||||
|
impersonationLoadBalancerService: (@= defaultResourceNameWithSuffix("impersonation-proxy-load-balancer") @)
|
||||||
|
impersonationTLSCertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-tls-serving-certificate") @)
|
||||||
|
impersonationCACertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-ca-certificate") @)
|
||||||
|
impersonationSignerSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-signer-ca-certificate") @)
|
||||||
labels: (@= json.encode(labels()).rstrip() @)
|
labels: (@= json.encode(labels()).rstrip() @)
|
||||||
kubeCertAgent:
|
kubeCertAgent:
|
||||||
namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @)
|
namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @)
|
||||||
@ -189,6 +194,20 @@ spec:
|
|||||||
port: 443
|
port: 443
|
||||||
targetPort: 8443
|
targetPort: 8443
|
||||||
---
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: #@ defaultResourceNameWithSuffix("proxy")
|
||||||
|
namespace: #@ namespace()
|
||||||
|
labels: #@ labels()
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector: #@ defaultLabel()
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 443
|
||||||
|
targetPort: 8444
|
||||||
|
---
|
||||||
apiVersion: apiregistration.k8s.io/v1
|
apiVersion: apiregistration.k8s.io/v1
|
||||||
kind: APIService
|
kind: APIService
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -31,6 +31,12 @@ rules:
|
|||||||
resources: [ securitycontextconstraints ]
|
resources: [ securitycontextconstraints ]
|
||||||
verbs: [ use ]
|
verbs: [ use ]
|
||||||
resourceNames: [ nonroot ]
|
resourceNames: [ nonroot ]
|
||||||
|
- apiGroups: [ "" ]
|
||||||
|
resources: [ "users", "groups" ]
|
||||||
|
verbs: [ "impersonate" ]
|
||||||
|
- apiGroups: [ "" ]
|
||||||
|
resources: [ nodes ]
|
||||||
|
verbs: [ list ]
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
|
- #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
|
||||||
resources: [ credentialissuers ]
|
resources: [ credentialissuers ]
|
||||||
@ -69,7 +75,7 @@ metadata:
|
|||||||
rules:
|
rules:
|
||||||
- apiGroups: [ "" ]
|
- apiGroups: [ "" ]
|
||||||
resources: [ services ]
|
resources: [ services ]
|
||||||
verbs: [ create, get, list, patch, update, watch ]
|
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||||
- apiGroups: [ "" ]
|
- apiGroups: [ "" ]
|
||||||
resources: [ secrets ]
|
resources: [ secrets ]
|
||||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||||
@ -81,9 +87,12 @@ rules:
|
|||||||
- apiGroups: [ "" ]
|
- apiGroups: [ "" ]
|
||||||
resources: [ pods/exec ]
|
resources: [ pods/exec ]
|
||||||
verbs: [ create ]
|
verbs: [ create ]
|
||||||
- apiGroups: [apps]
|
- apiGroups: [ apps ]
|
||||||
resources: [replicasets,deployments]
|
resources: [ replicasets,deployments ]
|
||||||
verbs: [get]
|
verbs: [ get ]
|
||||||
|
- apiGroups: [ "" ]
|
||||||
|
resources: [ configmaps ]
|
||||||
|
verbs: [ list, get, watch ]
|
||||||
---
|
---
|
||||||
kind: RoleBinding
|
kind: RoleBinding
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
21
generated/1.17/README.adoc
generated
21
generated/1.17/README.adoc
generated
@ -251,6 +251,7 @@ Describes the configuration status of a Pinniped credential issuer.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
||||||
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
|
| *`impersonationProxyInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyinfo[$$ImpersonationProxyInfo$$]__ | ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. This field is only set when Type is "ImpersonationProxy".
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
@ -314,6 +315,24 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyinfo"]
|
||||||
|
==== ImpersonationProxyInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`endpoint`* __string__ | Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
||||||
==== TokenCredentialRequestAPIInfo
|
==== TokenCredentialRequestAPIInfo
|
||||||
|
|
||||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
||||||
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the Kubernetes API server CA bundle.
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||||
type StrategyType string
|
type StrategyType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||||
type FrontendType string
|
type FrontendType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=Success;Error
|
// +kubebuilder:validation:Enum=Success;Error
|
||||||
type StrategyStatus string
|
type StrategyStatus string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||||
type StrategyReason string
|
type StrategyReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||||
|
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||||
|
|
||||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||||
|
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||||
|
|
||||||
SuccessStrategyStatus = StrategyStatus("Success")
|
SuccessStrategyStatus = StrategyStatus("Success")
|
||||||
ErrorStrategyStatus = StrategyStatus("Error")
|
ErrorStrategyStatus = StrategyStatus("Error")
|
||||||
|
|
||||||
|
ListeningStrategyReason = StrategyReason("Listening")
|
||||||
|
PendingStrategyReason = StrategyReason("Pending")
|
||||||
|
DisabledStrategyReason = StrategyReason("Disabled")
|
||||||
|
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
|||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
// This field is only set when Type is "ImpersonationProxy".
|
||||||
|
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
|
||||||
// CertificateAuthorityData is the Kubernetes API server CA bundle.
|
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
type ImpersonationProxyInfo struct {
|
||||||
|
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
|
||||||
|
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
|||||||
*out = new(TokenCredentialRequestAPIInfo)
|
*out = new(TokenCredentialRequestAPIInfo)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ImpersonationProxyInfo != nil {
|
||||||
|
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||||
|
*out = new(ImpersonationProxyInfo)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopyInto(out *ImpersonationProxyInfo) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyInfo.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImpersonationProxyInfo)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -67,14 +67,34 @@ spec:
|
|||||||
description: Frontend describes how clients can connect using
|
description: Frontend describes how clients can connect using
|
||||||
this strategy.
|
this strategy.
|
||||||
properties:
|
properties:
|
||||||
|
impersonationProxyInfo:
|
||||||
|
description: ImpersonationProxyInfo describes the parameters
|
||||||
|
for the impersonation proxy on this Concierge. This field
|
||||||
|
is only set when Type is "ImpersonationProxy".
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
|
PEM CA bundle of the impersonation proxy.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
endpoint:
|
||||||
|
description: Endpoint is the HTTPS endpoint of the impersonation
|
||||||
|
proxy.
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^https://
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- certificateAuthorityData
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
tokenCredentialRequestInfo:
|
tokenCredentialRequestInfo:
|
||||||
description: TokenCredentialRequestAPIInfo describes the
|
description: TokenCredentialRequestAPIInfo describes the
|
||||||
parameters for the TokenCredentialRequest API on this
|
parameters for the TokenCredentialRequest API on this
|
||||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
properties:
|
properties:
|
||||||
certificateAuthorityData:
|
certificateAuthorityData:
|
||||||
description: CertificateAuthorityData is the Kubernetes
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
API server CA bundle.
|
Kubernetes API server CA bundle.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
server:
|
server:
|
||||||
@ -91,6 +111,7 @@ spec:
|
|||||||
can use with a strategy.
|
can use with a strategy.
|
||||||
enum:
|
enum:
|
||||||
- TokenCredentialRequestAPI
|
- TokenCredentialRequestAPI
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@ -106,8 +127,13 @@ spec:
|
|||||||
reason:
|
reason:
|
||||||
description: Reason for the current status.
|
description: Reason for the current status.
|
||||||
enum:
|
enum:
|
||||||
- FetchedKey
|
- Listening
|
||||||
|
- Pending
|
||||||
|
- Disabled
|
||||||
|
- ErrorDuringSetup
|
||||||
- CouldNotFetchKey
|
- CouldNotFetchKey
|
||||||
|
- CouldNotGetClusterInfo
|
||||||
|
- FetchedKey
|
||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
description: Status of the attempted integration strategy.
|
description: Status of the attempted integration strategy.
|
||||||
@ -119,6 +145,7 @@ spec:
|
|||||||
description: Type of integration attempted.
|
description: Type of integration attempted.
|
||||||
enum:
|
enum:
|
||||||
- KubeClusterSigningCertificate
|
- KubeClusterSigningCertificate
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- lastUpdateTime
|
- lastUpdateTime
|
||||||
|
21
generated/1.18/README.adoc
generated
21
generated/1.18/README.adoc
generated
@ -251,6 +251,7 @@ Describes the configuration status of a Pinniped credential issuer.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
||||||
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
|
| *`impersonationProxyInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyinfo[$$ImpersonationProxyInfo$$]__ | ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. This field is only set when Type is "ImpersonationProxy".
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
@ -314,6 +315,24 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyinfo"]
|
||||||
|
==== ImpersonationProxyInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`endpoint`* __string__ | Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
||||||
==== TokenCredentialRequestAPIInfo
|
==== TokenCredentialRequestAPIInfo
|
||||||
|
|
||||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
||||||
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the Kubernetes API server CA bundle.
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||||
type StrategyType string
|
type StrategyType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||||
type FrontendType string
|
type FrontendType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=Success;Error
|
// +kubebuilder:validation:Enum=Success;Error
|
||||||
type StrategyStatus string
|
type StrategyStatus string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||||
type StrategyReason string
|
type StrategyReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||||
|
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||||
|
|
||||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||||
|
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||||
|
|
||||||
SuccessStrategyStatus = StrategyStatus("Success")
|
SuccessStrategyStatus = StrategyStatus("Success")
|
||||||
ErrorStrategyStatus = StrategyStatus("Error")
|
ErrorStrategyStatus = StrategyStatus("Error")
|
||||||
|
|
||||||
|
ListeningStrategyReason = StrategyReason("Listening")
|
||||||
|
PendingStrategyReason = StrategyReason("Pending")
|
||||||
|
DisabledStrategyReason = StrategyReason("Disabled")
|
||||||
|
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
|||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
// This field is only set when Type is "ImpersonationProxy".
|
||||||
|
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
|
||||||
// CertificateAuthorityData is the Kubernetes API server CA bundle.
|
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
type ImpersonationProxyInfo struct {
|
||||||
|
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
|
||||||
|
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
|||||||
*out = new(TokenCredentialRequestAPIInfo)
|
*out = new(TokenCredentialRequestAPIInfo)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ImpersonationProxyInfo != nil {
|
||||||
|
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||||
|
*out = new(ImpersonationProxyInfo)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopyInto(out *ImpersonationProxyInfo) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyInfo.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImpersonationProxyInfo)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -67,14 +67,34 @@ spec:
|
|||||||
description: Frontend describes how clients can connect using
|
description: Frontend describes how clients can connect using
|
||||||
this strategy.
|
this strategy.
|
||||||
properties:
|
properties:
|
||||||
|
impersonationProxyInfo:
|
||||||
|
description: ImpersonationProxyInfo describes the parameters
|
||||||
|
for the impersonation proxy on this Concierge. This field
|
||||||
|
is only set when Type is "ImpersonationProxy".
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
|
PEM CA bundle of the impersonation proxy.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
endpoint:
|
||||||
|
description: Endpoint is the HTTPS endpoint of the impersonation
|
||||||
|
proxy.
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^https://
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- certificateAuthorityData
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
tokenCredentialRequestInfo:
|
tokenCredentialRequestInfo:
|
||||||
description: TokenCredentialRequestAPIInfo describes the
|
description: TokenCredentialRequestAPIInfo describes the
|
||||||
parameters for the TokenCredentialRequest API on this
|
parameters for the TokenCredentialRequest API on this
|
||||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
properties:
|
properties:
|
||||||
certificateAuthorityData:
|
certificateAuthorityData:
|
||||||
description: CertificateAuthorityData is the Kubernetes
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
API server CA bundle.
|
Kubernetes API server CA bundle.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
server:
|
server:
|
||||||
@ -91,6 +111,7 @@ spec:
|
|||||||
can use with a strategy.
|
can use with a strategy.
|
||||||
enum:
|
enum:
|
||||||
- TokenCredentialRequestAPI
|
- TokenCredentialRequestAPI
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@ -106,8 +127,13 @@ spec:
|
|||||||
reason:
|
reason:
|
||||||
description: Reason for the current status.
|
description: Reason for the current status.
|
||||||
enum:
|
enum:
|
||||||
- FetchedKey
|
- Listening
|
||||||
|
- Pending
|
||||||
|
- Disabled
|
||||||
|
- ErrorDuringSetup
|
||||||
- CouldNotFetchKey
|
- CouldNotFetchKey
|
||||||
|
- CouldNotGetClusterInfo
|
||||||
|
- FetchedKey
|
||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
description: Status of the attempted integration strategy.
|
description: Status of the attempted integration strategy.
|
||||||
@ -119,6 +145,7 @@ spec:
|
|||||||
description: Type of integration attempted.
|
description: Type of integration attempted.
|
||||||
enum:
|
enum:
|
||||||
- KubeClusterSigningCertificate
|
- KubeClusterSigningCertificate
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- lastUpdateTime
|
- lastUpdateTime
|
||||||
|
21
generated/1.19/README.adoc
generated
21
generated/1.19/README.adoc
generated
@ -251,6 +251,7 @@ Describes the configuration status of a Pinniped credential issuer.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
||||||
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
|
| *`impersonationProxyInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyinfo[$$ImpersonationProxyInfo$$]__ | ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. This field is only set when Type is "ImpersonationProxy".
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
@ -314,6 +315,24 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyinfo"]
|
||||||
|
==== ImpersonationProxyInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`endpoint`* __string__ | Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
||||||
==== TokenCredentialRequestAPIInfo
|
==== TokenCredentialRequestAPIInfo
|
||||||
|
|
||||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
||||||
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the Kubernetes API server CA bundle.
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||||
type StrategyType string
|
type StrategyType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||||
type FrontendType string
|
type FrontendType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=Success;Error
|
// +kubebuilder:validation:Enum=Success;Error
|
||||||
type StrategyStatus string
|
type StrategyStatus string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||||
type StrategyReason string
|
type StrategyReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||||
|
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||||
|
|
||||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||||
|
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||||
|
|
||||||
SuccessStrategyStatus = StrategyStatus("Success")
|
SuccessStrategyStatus = StrategyStatus("Success")
|
||||||
ErrorStrategyStatus = StrategyStatus("Error")
|
ErrorStrategyStatus = StrategyStatus("Error")
|
||||||
|
|
||||||
|
ListeningStrategyReason = StrategyReason("Listening")
|
||||||
|
PendingStrategyReason = StrategyReason("Pending")
|
||||||
|
DisabledStrategyReason = StrategyReason("Disabled")
|
||||||
|
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
|||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
// This field is only set when Type is "ImpersonationProxy".
|
||||||
|
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
|
||||||
// CertificateAuthorityData is the Kubernetes API server CA bundle.
|
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
type ImpersonationProxyInfo struct {
|
||||||
|
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
|
||||||
|
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
|||||||
*out = new(TokenCredentialRequestAPIInfo)
|
*out = new(TokenCredentialRequestAPIInfo)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ImpersonationProxyInfo != nil {
|
||||||
|
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||||
|
*out = new(ImpersonationProxyInfo)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopyInto(out *ImpersonationProxyInfo) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyInfo.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImpersonationProxyInfo)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -67,14 +67,34 @@ spec:
|
|||||||
description: Frontend describes how clients can connect using
|
description: Frontend describes how clients can connect using
|
||||||
this strategy.
|
this strategy.
|
||||||
properties:
|
properties:
|
||||||
|
impersonationProxyInfo:
|
||||||
|
description: ImpersonationProxyInfo describes the parameters
|
||||||
|
for the impersonation proxy on this Concierge. This field
|
||||||
|
is only set when Type is "ImpersonationProxy".
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
|
PEM CA bundle of the impersonation proxy.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
endpoint:
|
||||||
|
description: Endpoint is the HTTPS endpoint of the impersonation
|
||||||
|
proxy.
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^https://
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- certificateAuthorityData
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
tokenCredentialRequestInfo:
|
tokenCredentialRequestInfo:
|
||||||
description: TokenCredentialRequestAPIInfo describes the
|
description: TokenCredentialRequestAPIInfo describes the
|
||||||
parameters for the TokenCredentialRequest API on this
|
parameters for the TokenCredentialRequest API on this
|
||||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
properties:
|
properties:
|
||||||
certificateAuthorityData:
|
certificateAuthorityData:
|
||||||
description: CertificateAuthorityData is the Kubernetes
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
API server CA bundle.
|
Kubernetes API server CA bundle.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
server:
|
server:
|
||||||
@ -91,6 +111,7 @@ spec:
|
|||||||
can use with a strategy.
|
can use with a strategy.
|
||||||
enum:
|
enum:
|
||||||
- TokenCredentialRequestAPI
|
- TokenCredentialRequestAPI
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@ -106,8 +127,13 @@ spec:
|
|||||||
reason:
|
reason:
|
||||||
description: Reason for the current status.
|
description: Reason for the current status.
|
||||||
enum:
|
enum:
|
||||||
- FetchedKey
|
- Listening
|
||||||
|
- Pending
|
||||||
|
- Disabled
|
||||||
|
- ErrorDuringSetup
|
||||||
- CouldNotFetchKey
|
- CouldNotFetchKey
|
||||||
|
- CouldNotGetClusterInfo
|
||||||
|
- FetchedKey
|
||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
description: Status of the attempted integration strategy.
|
description: Status of the attempted integration strategy.
|
||||||
@ -119,6 +145,7 @@ spec:
|
|||||||
description: Type of integration attempted.
|
description: Type of integration attempted.
|
||||||
enum:
|
enum:
|
||||||
- KubeClusterSigningCertificate
|
- KubeClusterSigningCertificate
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- lastUpdateTime
|
- lastUpdateTime
|
||||||
|
21
generated/1.20/README.adoc
generated
21
generated/1.20/README.adoc
generated
@ -251,6 +251,7 @@ Describes the configuration status of a Pinniped credential issuer.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
||||||
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
|
| *`impersonationProxyInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyinfo[$$ImpersonationProxyInfo$$]__ | ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. This field is only set when Type is "ImpersonationProxy".
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
@ -314,6 +315,24 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyinfo"]
|
||||||
|
==== ImpersonationProxyInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`endpoint`* __string__ | Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
||||||
==== TokenCredentialRequestAPIInfo
|
==== TokenCredentialRequestAPIInfo
|
||||||
|
|
||||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
||||||
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the Kubernetes API server CA bundle.
|
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||||
type StrategyType string
|
type StrategyType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||||
type FrontendType string
|
type FrontendType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=Success;Error
|
// +kubebuilder:validation:Enum=Success;Error
|
||||||
type StrategyStatus string
|
type StrategyStatus string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||||
type StrategyReason string
|
type StrategyReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||||
|
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||||
|
|
||||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||||
|
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||||
|
|
||||||
SuccessStrategyStatus = StrategyStatus("Success")
|
SuccessStrategyStatus = StrategyStatus("Success")
|
||||||
ErrorStrategyStatus = StrategyStatus("Error")
|
ErrorStrategyStatus = StrategyStatus("Error")
|
||||||
|
|
||||||
|
ListeningStrategyReason = StrategyReason("Listening")
|
||||||
|
PendingStrategyReason = StrategyReason("Pending")
|
||||||
|
DisabledStrategyReason = StrategyReason("Disabled")
|
||||||
|
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
|||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
// This field is only set when Type is "ImpersonationProxy".
|
||||||
|
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
|
||||||
// CertificateAuthorityData is the Kubernetes API server CA bundle.
|
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
type ImpersonationProxyInfo struct {
|
||||||
|
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
|
||||||
|
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
|||||||
*out = new(TokenCredentialRequestAPIInfo)
|
*out = new(TokenCredentialRequestAPIInfo)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ImpersonationProxyInfo != nil {
|
||||||
|
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||||
|
*out = new(ImpersonationProxyInfo)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopyInto(out *ImpersonationProxyInfo) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyInfo.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImpersonationProxyInfo)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -67,14 +67,34 @@ spec:
|
|||||||
description: Frontend describes how clients can connect using
|
description: Frontend describes how clients can connect using
|
||||||
this strategy.
|
this strategy.
|
||||||
properties:
|
properties:
|
||||||
|
impersonationProxyInfo:
|
||||||
|
description: ImpersonationProxyInfo describes the parameters
|
||||||
|
for the impersonation proxy on this Concierge. This field
|
||||||
|
is only set when Type is "ImpersonationProxy".
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
|
PEM CA bundle of the impersonation proxy.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
endpoint:
|
||||||
|
description: Endpoint is the HTTPS endpoint of the impersonation
|
||||||
|
proxy.
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^https://
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- certificateAuthorityData
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
tokenCredentialRequestInfo:
|
tokenCredentialRequestInfo:
|
||||||
description: TokenCredentialRequestAPIInfo describes the
|
description: TokenCredentialRequestAPIInfo describes the
|
||||||
parameters for the TokenCredentialRequest API on this
|
parameters for the TokenCredentialRequest API on this
|
||||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
properties:
|
properties:
|
||||||
certificateAuthorityData:
|
certificateAuthorityData:
|
||||||
description: CertificateAuthorityData is the Kubernetes
|
description: CertificateAuthorityData is the base64-encoded
|
||||||
API server CA bundle.
|
Kubernetes API server CA bundle.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
server:
|
server:
|
||||||
@ -91,6 +111,7 @@ spec:
|
|||||||
can use with a strategy.
|
can use with a strategy.
|
||||||
enum:
|
enum:
|
||||||
- TokenCredentialRequestAPI
|
- TokenCredentialRequestAPI
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@ -106,8 +127,13 @@ spec:
|
|||||||
reason:
|
reason:
|
||||||
description: Reason for the current status.
|
description: Reason for the current status.
|
||||||
enum:
|
enum:
|
||||||
- FetchedKey
|
- Listening
|
||||||
|
- Pending
|
||||||
|
- Disabled
|
||||||
|
- ErrorDuringSetup
|
||||||
- CouldNotFetchKey
|
- CouldNotFetchKey
|
||||||
|
- CouldNotGetClusterInfo
|
||||||
|
- FetchedKey
|
||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
description: Status of the attempted integration strategy.
|
description: Status of the attempted integration strategy.
|
||||||
@ -119,6 +145,7 @@ spec:
|
|||||||
description: Type of integration attempted.
|
description: Type of integration attempted.
|
||||||
enum:
|
enum:
|
||||||
- KubeClusterSigningCertificate
|
- KubeClusterSigningCertificate
|
||||||
|
- ImpersonationProxy
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- lastUpdateTime
|
- lastUpdateTime
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||||
type StrategyType string
|
type StrategyType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||||
type FrontendType string
|
type FrontendType string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=Success;Error
|
// +kubebuilder:validation:Enum=Success;Error
|
||||||
type StrategyStatus string
|
type StrategyStatus string
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||||
type StrategyReason string
|
type StrategyReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||||
|
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||||
|
|
||||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||||
|
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||||
|
|
||||||
SuccessStrategyStatus = StrategyStatus("Success")
|
SuccessStrategyStatus = StrategyStatus("Success")
|
||||||
ErrorStrategyStatus = StrategyStatus("Error")
|
ErrorStrategyStatus = StrategyStatus("Error")
|
||||||
|
|
||||||
|
ListeningStrategyReason = StrategyReason("Listening")
|
||||||
|
PendingStrategyReason = StrategyReason("Pending")
|
||||||
|
DisabledStrategyReason = StrategyReason("Disabled")
|
||||||
|
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
|||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
// This field is only set when Type is "ImpersonationProxy".
|
||||||
|
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
|
||||||
// CertificateAuthorityData is the Kubernetes API server CA bundle.
|
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||||
|
type ImpersonationProxyInfo struct {
|
||||||
|
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
|
||||||
|
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
|||||||
*out = new(TokenCredentialRequestAPIInfo)
|
*out = new(TokenCredentialRequestAPIInfo)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ImpersonationProxyInfo != nil {
|
||||||
|
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||||
|
*out = new(ImpersonationProxyInfo)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopyInto(out *ImpersonationProxyInfo) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyInfo.
|
||||||
|
func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImpersonationProxyInfo)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
2
go.mod
2
go.mod
@ -15,6 +15,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.5.5
|
github.com/google/go-cmp v0.5.5
|
||||||
github.com/google/gofuzz v1.2.0
|
github.com/google/gofuzz v1.2.0
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/oleiade/reflections v1.0.1 // indirect
|
github.com/oleiade/reflections v1.0.1 // indirect
|
||||||
github.com/onsi/ginkgo v1.13.0 // indirect
|
github.com/onsi/ginkgo v1.13.0 // indirect
|
||||||
github.com/ory/fosite v0.39.0
|
github.com/ory/fosite v0.39.0
|
||||||
@ -26,6 +27,7 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620
|
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect
|
||||||
|
3
go.sum
3
go.sum
@ -1181,8 +1181,9 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -116,6 +116,22 @@ check_dependency htpasswd "Please install htpasswd. Should be pre-installed on M
|
|||||||
check_dependency openssl "Please install openssl. Should be pre-installed on MacOS."
|
check_dependency openssl "Please install openssl. Should be pre-installed on MacOS."
|
||||||
check_dependency chromedriver "Please install chromedriver. e.g. 'brew install chromedriver' for MacOS"
|
check_dependency chromedriver "Please install chromedriver. e.g. 'brew install chromedriver' for MacOS"
|
||||||
|
|
||||||
|
# Check that Chrome and chromedriver versions match. If chromedriver falls a couple versions behind
|
||||||
|
# then usually tests start to fail with strange error messages.
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
chrome_version=$(/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version | cut -d ' ' -f3 | cut -d '.' -f1)
|
||||||
|
else
|
||||||
|
chrome_version=$(google-chrome --version | cut -d ' ' -f3 | cut -d '.' -f1)
|
||||||
|
fi
|
||||||
|
chromedriver_version=$(chromedriver --version | cut -d ' ' -f2 | cut -d '.' -f1)
|
||||||
|
if [[ "$chrome_version" != "$chromedriver_version" ]]; then
|
||||||
|
log_error "It appears that you are using Chrome $chrome_version with chromedriver $chromedriver_version."
|
||||||
|
log_error "Please use the same version of chromedriver as Chrome."
|
||||||
|
log_error "If you are using the latest version of Chrome, then you can upgrade"
|
||||||
|
log_error "to the latest chromedriver, e.g. 'brew upgrade chromedriver' on MacOS."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Require kubectl >= 1.18.x
|
# Require kubectl >= 1.18.x
|
||||||
if [ "$(kubectl version --client=true --short | cut -d '.' -f 2)" -lt 18 ]; then
|
if [ "$(kubectl version --client=true --short | cut -d '.' -f 2)" -lt 18 ]; then
|
||||||
log_error "kubectl >= 1.18.x is required, you have $(kubectl version --client=true --short | cut -d ':' -f2)"
|
log_error "kubectl >= 1.18.x is required, you have $(kubectl version --client=true --short | cut -d ':' -f2)"
|
||||||
@ -292,7 +308,7 @@ kind_capabilities_file="$pinniped_path/test/cluster_capabilities/kind.yaml"
|
|||||||
pinniped_cluster_capability_file_content=$(cat "$kind_capabilities_file")
|
pinniped_cluster_capability_file_content=$(cat "$kind_capabilities_file")
|
||||||
|
|
||||||
cat <<EOF >/tmp/integration-test-env
|
cat <<EOF >/tmp/integration-test-env
|
||||||
# The following env vars should be set before running 'go test -v -count 1 ./test/integration'
|
# The following env vars should be set before running 'go test -v -count 1 -timeout 0 ./test/integration'
|
||||||
export PINNIPED_TEST_CONCIERGE_NAMESPACE=${concierge_namespace}
|
export PINNIPED_TEST_CONCIERGE_NAMESPACE=${concierge_namespace}
|
||||||
export PINNIPED_TEST_CONCIERGE_APP_NAME=${concierge_app_name}
|
export PINNIPED_TEST_CONCIERGE_APP_NAME=${concierge_app_name}
|
||||||
export PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS='${concierge_custom_labels}'
|
export PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS='${concierge_custom_labels}'
|
||||||
@ -341,7 +357,7 @@ goland_vars=$(grep -v '^#' /tmp/integration-test-env | grep -E '^export .+=' | s
|
|||||||
log_note
|
log_note
|
||||||
log_note "🚀 Ready to run integration tests! For example..."
|
log_note "🚀 Ready to run integration tests! For example..."
|
||||||
log_note " cd $pinniped_path"
|
log_note " cd $pinniped_path"
|
||||||
log_note ' source /tmp/integration-test-env && go test -v -race -count 1 ./test/integration'
|
log_note ' source /tmp/integration-test-env && go test -v -race -count 1 -timeout 0 ./test/integration'
|
||||||
log_note
|
log_note
|
||||||
log_note 'Want to run integration tests in GoLand? Copy/paste this "Environment" value for GoLand run configurations:'
|
log_note 'Want to run integration tests in GoLand? Copy/paste this "Environment" value for GoLand run configurations:'
|
||||||
log_note " ${goland_vars}PINNIPED_TEST_CLUSTER_CAPABILITY_FILE=${kind_capabilities_file}"
|
log_note " ${goland_vars}PINNIPED_TEST_CLUSTER_CAPABILITY_FILE=${kind_capabilities_file}"
|
||||||
|
114
hack/prepare-impersonator-on-kind.sh
Executable file
114
hack/prepare-impersonator-on-kind.sh
Executable file
@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# A script to perform the setup required to manually test using the impersonation proxy on a kind cluster.
|
||||||
|
# Assumes that you installed the apps already using hack/prepare-for-integration-tests.sh.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# The name of the namespace in which the concierge is installed.
|
||||||
|
CONCIERGE_NAMESPACE=concierge
|
||||||
|
# The name of the concierge app's Deployment.
|
||||||
|
CONCIERGE_DEPLOYMENT=pinniped-concierge
|
||||||
|
# The namespace in which the local-user-authenticator app is installed.
|
||||||
|
LOCAL_USER_AUTHENTICATOR_NAMESPACE=local-user-authenticator
|
||||||
|
# The port on which the impersonation proxy runs in the concierge pods.
|
||||||
|
IMPERSONATION_PROXY_PORT=8444
|
||||||
|
# The port that we will use to access the impersonator from outside the cluster via `kubectl port-forward`.
|
||||||
|
LOCAL_PORT=8777
|
||||||
|
LOCAL_HOST="127.0.0.1:${LOCAL_PORT}"
|
||||||
|
|
||||||
|
# Change working directory to the top of the repo.
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# Build the CLI for use later in the script.
|
||||||
|
go build ./cmd/pinniped
|
||||||
|
|
||||||
|
# Create a test user and password.
|
||||||
|
if ! kubectl get secret pinny-the-seal --namespace $LOCAL_USER_AUTHENTICATOR_NAMESPACE; then
|
||||||
|
kubectl create secret generic pinny-the-seal --namespace $LOCAL_USER_AUTHENTICATOR_NAMESPACE \
|
||||||
|
--from-literal=groups=group1,group2 \
|
||||||
|
--from-literal=passwordHash="$(htpasswd -nbBC 10 x password123 | sed -e "s/^x://")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the CA of the local-user-authenticator.
|
||||||
|
LOCAL_USER_AUTHENTICATOR_CA=$(kubectl get secret local-user-authenticator-tls-serving-certificate \
|
||||||
|
--namespace $LOCAL_USER_AUTHENTICATOR_NAMESPACE \
|
||||||
|
-o jsonpath=\{.data.caCertificate\})
|
||||||
|
|
||||||
|
# Create a WebhookAuthenticator which points at the local-user-authenticator.
|
||||||
|
cat <<EOF | kubectl apply -f -
|
||||||
|
apiVersion: authentication.concierge.pinniped.dev/v1alpha1
|
||||||
|
kind: WebhookAuthenticator
|
||||||
|
metadata:
|
||||||
|
name: local-user-authenticator
|
||||||
|
spec:
|
||||||
|
endpoint: https://local-user-authenticator.local-user-authenticator.svc/authenticate
|
||||||
|
tls:
|
||||||
|
certificateAuthorityData: $LOCAL_USER_AUTHENTICATOR_CA
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create an RBAC rule to allow the test user to do most things.
|
||||||
|
cat <<EOF | kubectl apply -f -
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: pinny-the-seal-can-edit
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: edit
|
||||||
|
subjects:
|
||||||
|
- kind: User
|
||||||
|
name: pinny-the-seal
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create a configmap to enable the impersonation proxy and set the endpoint to match the
|
||||||
|
# host and port that we will use the access the impersonation proxy (via the port-forwarded port).
|
||||||
|
cat <<EOF | kubectl apply -f -
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: pinniped-concierge-impersonation-proxy-config
|
||||||
|
namespace: $CONCIERGE_NAMESPACE
|
||||||
|
data:
|
||||||
|
config.yaml: |
|
||||||
|
endpoint: ${LOCAL_HOST}
|
||||||
|
mode: enabled
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Wait for the CredentialIssuer's impersonator status to update to be successful.
|
||||||
|
while [[ -z "$(kubectl get credentialissuer pinniped-concierge-config -o json |
|
||||||
|
jq '.status.strategies[] | select((.type=="ImpersonationProxy") and (.status=="Success"))')" ]]; do
|
||||||
|
echo "Waiting for a successful ImpersonationProxy strategy on CredentialIssuer..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Impersonator is available on https://${LOCAL_HOST}"
|
||||||
|
|
||||||
|
# Make the impersonation proxy's port from the inside the cluster available locally.
|
||||||
|
kubectl port-forward -n $CONCIERGE_NAMESPACE deployment/$CONCIERGE_DEPLOYMENT ${LOCAL_PORT}:${IMPERSONATION_PROXY_PORT} &
|
||||||
|
port_forward_pid=$!
|
||||||
|
|
||||||
|
# Kill the kubectl port-forward command whenever the script is control-c cancelled or otherwise ends.
|
||||||
|
function cleanup() {
|
||||||
|
echo
|
||||||
|
echo "Cleaning up cluster resources..."
|
||||||
|
kubectl delete secret -n $LOCAL_USER_AUTHENTICATOR_NAMESPACE pinny-the-seal
|
||||||
|
kubectl delete configmap -n $CONCIERGE_NAMESPACE pinniped-concierge-impersonation-proxy-config
|
||||||
|
kubectl delete clusterrolebinding pinny-the-seal-can-edit
|
||||||
|
kubectl delete webhookauthenticator local-user-authenticator
|
||||||
|
echo "Stopping kubectl port-forward and exiting..."
|
||||||
|
# It may have already shut down, so ignore errors.
|
||||||
|
kill -9 $port_forward_pid &> /dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Get a working kubeconfig that will send requests through the impersonation proxy.
|
||||||
|
./pinniped get kubeconfig \
|
||||||
|
--static-token "pinny-the-seal:password123" \
|
||||||
|
--concierge-mode ImpersonationProxy >/tmp/kubeconfig
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo 'Ready. In another tab, use "kubectl --kubeconfig /tmp/kubeconfig <cmd>" to make requests through the impersonation proxy.'
|
||||||
|
echo "When done, cancel with ctrl-C to clean up."
|
||||||
|
wait $port_forward_pid
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package certauthority implements a simple x509 certificate authority suitable for use in an aggregated API service.
|
// Package certauthority implements a simple x509 certificate authority suitable for use in an aggregated API service.
|
||||||
@ -19,6 +19,8 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/constable"
|
||||||
)
|
)
|
||||||
|
|
||||||
// certBackdate is the amount of time before time.Now() that will be used to set
|
// certBackdate is the amount of time before time.Now() that will be used to set
|
||||||
@ -44,12 +46,17 @@ type env struct {
|
|||||||
|
|
||||||
// CA holds the state for a simple x509 certificate authority suitable for use in an aggregated API service.
|
// CA holds the state for a simple x509 certificate authority suitable for use in an aggregated API service.
|
||||||
type CA struct {
|
type CA struct {
|
||||||
// caCert is the DER-encoded certificate for the current CA.
|
// caCertBytes is the DER-encoded certificate for the current CA.
|
||||||
caCertBytes []byte
|
caCertBytes []byte
|
||||||
|
|
||||||
// signer is the private key for the current CA.
|
// signer is the private key for the current CA.
|
||||||
signer crypto.Signer
|
signer crypto.Signer
|
||||||
|
|
||||||
|
// privateKey is the same private key represented by signer, but in a format which allows export.
|
||||||
|
// It is only set by New, not by Load, since Load can handle various types of PrivateKey but New
|
||||||
|
// only needs to create keys of type ecdsa.PrivateKey.
|
||||||
|
privateKey *ecdsa.PrivateKey
|
||||||
|
|
||||||
// env is our reference to the outside world (clocks and random number generation).
|
// env is our reference to the outside world (clocks and random number generation).
|
||||||
env env
|
env env
|
||||||
}
|
}
|
||||||
@ -66,7 +73,7 @@ func secureEnv() env {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ErrInvalidCACertificate is returned when the contents of the loaded CA certificate do not meet our assumptions.
|
// ErrInvalidCACertificate is returned when the contents of the loaded CA certificate do not meet our assumptions.
|
||||||
var ErrInvalidCACertificate = fmt.Errorf("invalid CA certificate")
|
const ErrInvalidCACertificate = constable.Error("invalid CA certificate")
|
||||||
|
|
||||||
// Load a certificate authority from an existing certificate and private key (in PEM format).
|
// Load a certificate authority from an existing certificate and private key (in PEM format).
|
||||||
func Load(certPEM string, keyPEM string) (*CA, error) {
|
func Load(certPEM string, keyPEM string) (*CA, error) {
|
||||||
@ -77,6 +84,13 @@ func Load(certPEM string, keyPEM string) (*CA, error) {
|
|||||||
if certCount := len(cert.Certificate); certCount != 1 {
|
if certCount := len(cert.Certificate); certCount != 1 {
|
||||||
return nil, fmt.Errorf("%w: expected a single certificate, found %d certificates", ErrInvalidCACertificate, certCount)
|
return nil, fmt.Errorf("%w: expected a single certificate, found %d certificates", ErrInvalidCACertificate, certCount)
|
||||||
}
|
}
|
||||||
|
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse key pair as x509 cert: %w", err)
|
||||||
|
}
|
||||||
|
if !x509Cert.IsCA {
|
||||||
|
return nil, fmt.Errorf("%w: passed in key pair is not a CA", ErrInvalidCACertificate)
|
||||||
|
}
|
||||||
return &CA{
|
return &CA{
|
||||||
caCertBytes: cert.Certificate[0],
|
caCertBytes: cert.Certificate[0],
|
||||||
signer: cert.PrivateKey.(crypto.Signer),
|
signer: cert.PrivateKey.(crypto.Signer),
|
||||||
@ -84,13 +98,13 @@ func Load(certPEM string, keyPEM string) (*CA, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New generates a fresh certificate authority with the given subject and ttl.
|
// New generates a fresh certificate authority with the given Common Name and TTL.
|
||||||
func New(subject pkix.Name, ttl time.Duration) (*CA, error) {
|
func New(commonName string, ttl time.Duration) (*CA, error) {
|
||||||
return newInternal(subject, ttl, secureEnv())
|
return newInternal(commonName, ttl, secureEnv())
|
||||||
}
|
}
|
||||||
|
|
||||||
// newInternal is the internal guts of New, broken out for easier testing.
|
// newInternal is the internal guts of New, broken out for easier testing.
|
||||||
func newInternal(subject pkix.Name, ttl time.Duration, env env) (*CA, error) {
|
func newInternal(commonName string, ttl time.Duration, env env) (*CA, error) {
|
||||||
ca := CA{env: env}
|
ca := CA{env: env}
|
||||||
// Generate a random serial for the CA
|
// Generate a random serial for the CA
|
||||||
serialNumber, err := randomSerial(env.serialRNG)
|
serialNumber, err := randomSerial(env.serialRNG)
|
||||||
@ -99,11 +113,11 @@ func newInternal(subject pkix.Name, ttl time.Duration, env env) (*CA, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new P256 keypair.
|
// Generate a new P256 keypair.
|
||||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), env.keygenRNG)
|
ca.privateKey, err = ecdsa.GenerateKey(elliptic.P256(), env.keygenRNG)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not generate CA private key: %w", err)
|
return nil, fmt.Errorf("could not generate CA private key: %w", err)
|
||||||
}
|
}
|
||||||
ca.signer = privateKey
|
ca.signer = ca.privateKey
|
||||||
|
|
||||||
// Make a CA certificate valid for some ttl and backdated by some amount.
|
// Make a CA certificate valid for some ttl and backdated by some amount.
|
||||||
now := env.clock()
|
now := env.clock()
|
||||||
@ -113,7 +127,7 @@ func newInternal(subject pkix.Name, ttl time.Duration, env env) (*CA, error) {
|
|||||||
// Create CA cert template
|
// Create CA cert template
|
||||||
caTemplate := x509.Certificate{
|
caTemplate := x509.Certificate{
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: serialNumber,
|
||||||
Subject: subject,
|
Subject: pkix.Name{CommonName: commonName},
|
||||||
NotBefore: notBefore,
|
NotBefore: notBefore,
|
||||||
NotAfter: notAfter,
|
NotAfter: notAfter,
|
||||||
IsCA: true,
|
IsCA: true,
|
||||||
@ -123,7 +137,7 @@ func newInternal(subject pkix.Name, ttl time.Duration, env env) (*CA, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Self-sign the CA to get the DER certificate.
|
// Self-sign the CA to get the DER certificate.
|
||||||
caCertBytes, err := x509.CreateCertificate(env.signingRNG, &caTemplate, &caTemplate, &privateKey.PublicKey, privateKey)
|
caCertBytes, err := x509.CreateCertificate(env.signingRNG, &caTemplate, &caTemplate, &ca.privateKey.PublicKey, ca.privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not issue CA certificate: %w", err)
|
return nil, fmt.Errorf("could not issue CA certificate: %w", err)
|
||||||
}
|
}
|
||||||
@ -136,6 +150,18 @@ func (c *CA) Bundle() []byte {
|
|||||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.caCertBytes})
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.caCertBytes})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrivateKeyToPEM returns the current CA private key in PEM format, if this CA was constructed by New.
|
||||||
|
func (c *CA) PrivateKeyToPEM() ([]byte, error) {
|
||||||
|
if c.privateKey == nil {
|
||||||
|
return nil, fmt.Errorf("no private key data (did you try to use this after Load?)")
|
||||||
|
}
|
||||||
|
derKey, err := x509.MarshalECPrivateKey(c.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: derKey}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Pool returns the current CA signing bundle as a *x509.CertPool.
|
// Pool returns the current CA signing bundle as a *x509.CertPool.
|
||||||
func (c *CA) Pool() *x509.CertPool {
|
func (c *CA) Pool() *x509.CertPool {
|
||||||
pool := x509.NewCertPool()
|
pool := x509.NewCertPool()
|
||||||
@ -143,8 +169,31 @@ func (c *CA) Pool() *x509.CertPool {
|
|||||||
return pool
|
return pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue a new server certificate for the given identity and duration.
|
// IssueClientCert issues a new client certificate with username and groups included in the Kube-style
|
||||||
func (c *CA) Issue(subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.Duration) (*tls.Certificate, error) {
|
// certificate subject for the given identity and duration.
|
||||||
|
func (c *CA) IssueClientCert(username string, groups []string, ttl time.Duration) (*tls.Certificate, error) {
|
||||||
|
return c.issueCert(x509.ExtKeyUsageClientAuth, pkix.Name{CommonName: username, Organization: groups}, nil, nil, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueServerCert issues a new server certificate for the given identity and duration.
|
||||||
|
// The dnsNames and ips are each optional, but at least one of them should be specified.
|
||||||
|
func (c *CA) IssueServerCert(dnsNames []string, ips []net.IP, ttl time.Duration) (*tls.Certificate, error) {
|
||||||
|
return c.issueCert(x509.ExtKeyUsageServerAuth, pkix.Name{}, dnsNames, ips, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to IssueClientCert, but returning the new cert as a pair of PEM-formatted byte slices
|
||||||
|
// for the certificate and private key.
|
||||||
|
func (c *CA) IssueClientCertPEM(username string, groups []string, ttl time.Duration) ([]byte, []byte, error) {
|
||||||
|
return toPEM(c.IssueClientCert(username, groups, ttl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to IssueServerCert, but returning the new cert as a pair of PEM-formatted byte slices
|
||||||
|
// for the certificate and private key.
|
||||||
|
func (c *CA) IssueServerCertPEM(dnsNames []string, ips []net.IP, ttl time.Duration) ([]byte, []byte, error) {
|
||||||
|
return toPEM(c.IssueServerCert(dnsNames, ips, ttl))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CA) issueCert(extKeyUsage x509.ExtKeyUsage, subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.Duration) (*tls.Certificate, error) {
|
||||||
// Choose a random 128 bit serial number.
|
// Choose a random 128 bit serial number.
|
||||||
serialNumber, err := randomSerial(c.env.serialRNG)
|
serialNumber, err := randomSerial(c.env.serialRNG)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -174,8 +223,7 @@ func (c *CA) Issue(subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.
|
|||||||
Subject: subject,
|
Subject: subject,
|
||||||
NotBefore: notBefore,
|
NotBefore: notBefore,
|
||||||
NotAfter: notAfter,
|
NotAfter: notAfter,
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
ExtKeyUsage: []x509.ExtKeyUsage{extKeyUsage},
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
IsCA: false,
|
IsCA: false,
|
||||||
DNSNames: dnsNames,
|
DNSNames: dnsNames,
|
||||||
@ -200,14 +248,8 @@ func (c *CA) Issue(subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssuePEM issues a new server certificate for the given identity and duration, returning it as a pair of
|
|
||||||
// PEM-formatted byte slices for the certificate and private key.
|
|
||||||
func (c *CA) IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error) {
|
|
||||||
return toPEM(c.Issue(subject, dnsNames, nil, ttl))
|
|
||||||
}
|
|
||||||
|
|
||||||
func toPEM(cert *tls.Certificate, err error) ([]byte, []byte, error) {
|
func toPEM(cert *tls.Certificate, err error) ([]byte, []byte, error) {
|
||||||
// If the wrapped Issue() returned an error, pass it back.
|
// If the wrapped IssueServerCert() returned an error, pass it back.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, 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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package certauthority
|
package certauthority
|
||||||
@ -7,7 +7,6 @@ import (
|
|||||||
"crypto"
|
"crypto"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -17,6 +16,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadFromFiles(t *testing.T, certPath string, keyPath string) (*CA, error) {
|
func loadFromFiles(t *testing.T, certPath string, keyPath string) (*CA, error) {
|
||||||
@ -80,22 +81,25 @@ func TestLoad(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, ca.caCertBytes)
|
require.NotEmpty(t, ca.caCertBytes)
|
||||||
require.NotNil(t, ca.signer)
|
require.NotNil(t, ca.signer)
|
||||||
|
require.Nil(t, ca.privateKey) // this struct field is only used for CA's created by New()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
got, err := New(pkix.Name{CommonName: "Test CA"}, time.Minute)
|
ca, err := New("Test CA", time.Minute)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, got)
|
require.NotNil(t, ca)
|
||||||
|
|
||||||
// Make sure the CA certificate looks roughly like what we expect.
|
// Make sure the CA certificate looks roughly like what we expect.
|
||||||
caCert, err := x509.ParseCertificate(got.caCertBytes)
|
caCert, err := x509.ParseCertificate(ca.caCertBytes)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "Test CA", caCert.Subject.CommonName)
|
require.Equal(t, "Test CA", caCert.Subject.CommonName)
|
||||||
require.WithinDuration(t, now.Add(-10*time.Second), caCert.NotBefore, 10*time.Second)
|
require.WithinDuration(t, now.Add(-10*time.Second), caCert.NotBefore, 10*time.Second)
|
||||||
require.WithinDuration(t, now.Add(time.Minute), caCert.NotAfter, 10*time.Second)
|
require.WithinDuration(t, now.Add(time.Minute), caCert.NotAfter, 10*time.Second)
|
||||||
|
|
||||||
|
require.NotNil(t, ca.privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewInternal(t *testing.T) {
|
func TestNewInternal(t *testing.T) {
|
||||||
@ -155,7 +159,7 @@ func TestNewInternal(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := newInternal(pkix.Name{CommonName: "Test CA"}, tt.ttl, tt.env)
|
got, err := newInternal("Test CA", tt.ttl, tt.env)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.EqualError(t, err, tt.wantErr)
|
require.EqualError(t, err, tt.wantErr)
|
||||||
require.Nil(t, got)
|
require.Nil(t, got)
|
||||||
@ -175,21 +179,34 @@ func TestNewInternal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBundle(t *testing.T) {
|
func TestBundle(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
ca := CA{caCertBytes: []byte{1, 2, 3, 4, 5, 6, 7, 8}}
|
||||||
ca := CA{caCertBytes: []byte{1, 2, 3, 4, 5, 6, 7, 8}}
|
certPEM := ca.Bundle()
|
||||||
got := ca.Bundle()
|
require.Equal(t, "-----BEGIN CERTIFICATE-----\nAQIDBAUGBwg=\n-----END CERTIFICATE-----\n", string(certPEM))
|
||||||
require.Equal(t, "-----BEGIN CERTIFICATE-----\nAQIDBAUGBwg=\n-----END CERTIFICATE-----\n", string(got))
|
}
|
||||||
})
|
|
||||||
|
func TestPrivateKeyToPEM(t *testing.T) {
|
||||||
|
ca, err := New("Test CA", time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
keyPEM, err := ca.PrivateKeyToPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Regexp(t, "(?s)-----BEGIN EC "+"PRIVATE KEY-----\n.*\n-----END EC PRIVATE KEY-----", string(keyPEM))
|
||||||
|
certPEM := ca.Bundle()
|
||||||
|
// Check that the public and private keys work together.
|
||||||
|
_, err = tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reloaded, err := Load(string(certPEM), string(keyPEM))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = reloaded.PrivateKeyToPEM()
|
||||||
|
require.EqualError(t, err, "no private key data (did you try to use this after Load?)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPool(t *testing.T) {
|
func TestPool(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
ca, err := New("test", 1*time.Hour)
|
||||||
ca, err := New(pkix.Name{CommonName: "test"}, 1*time.Hour)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
got := ca.Pool()
|
pool := ca.Pool()
|
||||||
require.Len(t, got.Subjects(), 1)
|
require.Len(t, pool.Subjects(), 1)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type errSigner struct {
|
type errSigner struct {
|
||||||
@ -204,6 +221,8 @@ func (e *errSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIssue(t *testing.T) {
|
func TestIssue(t *testing.T) {
|
||||||
|
const numRandBytes = 64 * 2 // each call to issue a cert will consume 64 bytes from the reader
|
||||||
|
|
||||||
now := time.Date(2020, 7, 10, 12, 41, 12, 1234, time.UTC)
|
now := time.Date(2020, 7, 10, 12, 41, 12, 1234, time.UTC)
|
||||||
|
|
||||||
realCA, err := loadFromFiles(t, "./testdata/test.crt", "./testdata/test.key")
|
realCA, err := loadFromFiles(t, "./testdata/test.crt", "./testdata/test.key")
|
||||||
@ -227,7 +246,7 @@ func TestIssue(t *testing.T) {
|
|||||||
name: "failed to generate keypair",
|
name: "failed to generate keypair",
|
||||||
ca: CA{
|
ca: CA{
|
||||||
env: env{
|
env: env{
|
||||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
keygenRNG: strings.NewReader(""),
|
keygenRNG: strings.NewReader(""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -237,8 +256,8 @@ func TestIssue(t *testing.T) {
|
|||||||
name: "invalid CA certificate",
|
name: "invalid CA certificate",
|
||||||
ca: CA{
|
ca: CA{
|
||||||
env: env{
|
env: env{
|
||||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
clock: func() time.Time { return now },
|
clock: func() time.Time { return now },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -248,8 +267,8 @@ func TestIssue(t *testing.T) {
|
|||||||
name: "signing error",
|
name: "signing error",
|
||||||
ca: CA{
|
ca: CA{
|
||||||
env: env{
|
env: env{
|
||||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
clock: func() time.Time { return now },
|
clock: func() time.Time { return now },
|
||||||
},
|
},
|
||||||
caCertBytes: realCA.caCertBytes,
|
caCertBytes: realCA.caCertBytes,
|
||||||
@ -261,11 +280,11 @@ func TestIssue(t *testing.T) {
|
|||||||
wantErr: "could not sign certificate: some signer error",
|
wantErr: "could not sign certificate: some signer error",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "success",
|
name: "parse certificate error",
|
||||||
ca: CA{
|
ca: CA{
|
||||||
env: env{
|
env: env{
|
||||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
clock: func() time.Time { return now },
|
clock: func() time.Time { return now },
|
||||||
parseCert: func(_ []byte) (*x509.Certificate, error) {
|
parseCert: func(_ []byte) (*x509.Certificate, error) {
|
||||||
return nil, fmt.Errorf("some parse certificate error")
|
return nil, fmt.Errorf("some parse certificate error")
|
||||||
@ -280,8 +299,8 @@ func TestIssue(t *testing.T) {
|
|||||||
name: "success",
|
name: "success",
|
||||||
ca: CA{
|
ca: CA{
|
||||||
env: env{
|
env: env{
|
||||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||||
clock: func() time.Time { return now },
|
clock: func() time.Time { return now },
|
||||||
parseCert: x509.ParseCertificate,
|
parseCert: x509.ParseCertificate,
|
||||||
},
|
},
|
||||||
@ -293,28 +312,26 @@ func TestIssue(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := tt.ca.Issue(pkix.Name{CommonName: "Test Server"}, []string{"example.com"}, []net.IP{net.IPv4(1, 2, 3, 4)}, 10*time.Minute)
|
got, err := tt.ca.IssueServerCert([]string{"example.com"}, []net.IP{net.IPv4(1, 2, 3, 4)}, 10*time.Minute)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.EqualError(t, err, tt.wantErr)
|
require.EqualError(t, err, tt.wantErr)
|
||||||
require.Nil(t, got)
|
require.Nil(t, got)
|
||||||
return
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
}
|
||||||
|
got, err = tt.ca.IssueClientCert("test-user", []string{"group1", "group2"}, 10*time.Minute)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
require.Nil(t, got)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, got)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssuePEM(t *testing.T) {
|
|
||||||
realCA, err := loadFromFiles(t, "./testdata/test.crt", "./testdata/test.key")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
certPEM, keyPEM, err := realCA.IssuePEM(pkix.Name{CommonName: "Test Server"}, []string{"example.com"}, 10*time.Minute)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, certPEM)
|
|
||||||
require.NotEmpty(t, keyPEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToPEM(t *testing.T) {
|
func TestToPEM(t *testing.T) {
|
||||||
realCert, err := tls.LoadX509KeyPair("./testdata/test.crt", "./testdata/test.key")
|
realCert, err := tls.LoadX509KeyPair("./testdata/test.crt", "./testdata/test.key")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -342,3 +359,90 @@ func TestToPEM(t *testing.T) {
|
|||||||
require.NotEmpty(t, keyPEM)
|
require.NotEmpty(t, keyPEM)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssueMethods(t *testing.T) {
|
||||||
|
// One CA can be used to issue both kinds of certs.
|
||||||
|
ca, err := New("Test CA", time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ttl := 121 * time.Hour
|
||||||
|
|
||||||
|
t.Run("client certs", func(t *testing.T) {
|
||||||
|
user := "test-username"
|
||||||
|
groups := []string{"group1", "group2"}
|
||||||
|
|
||||||
|
clientCert, err := ca.IssueClientCert(user, groups, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
certPEM, keyPEM, err := ToPEM(clientCert)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateClientCert(t, ca.Bundle(), certPEM, keyPEM, user, groups, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueClientCertPEM(user, groups, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateClientCert(t, ca.Bundle(), certPEM, keyPEM, user, groups, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueClientCertPEM(user, nil, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateClientCert(t, ca.Bundle(), certPEM, keyPEM, user, nil, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueClientCertPEM(user, []string{}, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateClientCert(t, ca.Bundle(), certPEM, keyPEM, user, nil, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueClientCertPEM("", []string{}, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateClientCert(t, ca.Bundle(), certPEM, keyPEM, "", nil, ttl)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("server certs", func(t *testing.T) {
|
||||||
|
dnsNames := []string{"example.com", "pinniped.dev"}
|
||||||
|
ips := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.2.3.4")}
|
||||||
|
|
||||||
|
serverCert, err := ca.IssueServerCert(dnsNames, ips, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
certPEM, keyPEM, err := ToPEM(serverCert)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateServerCert(t, ca.Bundle(), certPEM, keyPEM, dnsNames, ips, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueServerCertPEM(dnsNames, ips, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateServerCert(t, ca.Bundle(), certPEM, keyPEM, dnsNames, ips, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueServerCertPEM(nil, ips, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateServerCert(t, ca.Bundle(), certPEM, keyPEM, nil, ips, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueServerCertPEM(dnsNames, nil, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateServerCert(t, ca.Bundle(), certPEM, keyPEM, dnsNames, nil, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueServerCertPEM([]string{}, ips, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateServerCert(t, ca.Bundle(), certPEM, keyPEM, nil, ips, ttl)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err = ca.IssueServerCertPEM(dnsNames, []net.IP{}, ttl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
validateServerCert(t, ca.Bundle(), certPEM, keyPEM, dnsNames, nil, ttl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateClientCert(t *testing.T, caBundle []byte, certPEM []byte, keyPEM []byte, expectedUser string, expectedGroups []string, expectedTTL time.Duration) {
|
||||||
|
const fudgeFactor = 10 * time.Second
|
||||||
|
v := testutil.ValidateClientCertificate(t, string(caBundle), string(certPEM))
|
||||||
|
v.RequireLifetime(time.Now(), time.Now().Add(expectedTTL), certBackdate+fudgeFactor)
|
||||||
|
v.RequireMatchesPrivateKey(string(keyPEM))
|
||||||
|
v.RequireCommonName(expectedUser)
|
||||||
|
v.RequireOrganizations(expectedGroups)
|
||||||
|
v.RequireEmptyDNSNames()
|
||||||
|
v.RequireEmptyIPs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateServerCert(t *testing.T, caBundle []byte, certPEM []byte, keyPEM []byte, expectedDNSNames []string, expectedIPs []net.IP, expectedTTL time.Duration) {
|
||||||
|
const fudgeFactor = 10 * time.Second
|
||||||
|
v := testutil.ValidateServerCertificate(t, string(caBundle), string(certPEM))
|
||||||
|
v.RequireLifetime(time.Now(), time.Now().Add(expectedTTL), certBackdate+fudgeFactor)
|
||||||
|
v.RequireMatchesPrivateKey(string(keyPEM))
|
||||||
|
v.RequireCommonName("")
|
||||||
|
v.RequireDNSNames(expectedDNSNames)
|
||||||
|
v.RequireIPs(expectedIPs)
|
||||||
|
}
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package dynamiccertauthority implements a x509 certificate authority capable of issuing
|
// Package dynamiccertauthority implements a x509 certificate authority capable of issuing
|
||||||
@ -6,34 +6,42 @@
|
|||||||
package dynamiccertauthority
|
package dynamiccertauthority
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509/pkix"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/issuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CA is a type capable of issuing certificates.
|
// ca is a type capable of issuing certificates.
|
||||||
type CA struct {
|
type ca struct {
|
||||||
provider dynamiccert.Provider
|
provider dynamiccertificates.CertKeyContentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new CA, ready to issue certs whenever the provided provider has a keypair to
|
// New creates a ClientCertIssuer, ready to issue certs whenever
|
||||||
// provide.
|
// the given CertKeyContentProvider has a keypair to provide.
|
||||||
func New(provider dynamiccert.Provider) *CA {
|
func New(provider dynamiccertificates.CertKeyContentProvider) issuer.ClientCertIssuer {
|
||||||
return &CA{
|
return &ca{
|
||||||
provider: provider,
|
provider: provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssuePEM issues a new server certificate for the given identity and duration, returning it as a
|
func (c *ca) Name() string {
|
||||||
|
return c.provider.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueClientCertPEM issues a new client certificate for the given identity and duration, returning it as a
|
||||||
// pair of PEM-formatted byte slices for the certificate and private key.
|
// pair of PEM-formatted byte slices for the certificate and private key.
|
||||||
func (c *CA) IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error) {
|
func (c *ca) IssueClientCertPEM(username string, groups []string, ttl time.Duration) ([]byte, []byte, error) {
|
||||||
caCrtPEM, caKeyPEM := c.provider.CurrentCertKeyContent()
|
caCrtPEM, caKeyPEM := c.provider.CurrentCertKeyContent()
|
||||||
|
// in the future we could split dynamiccert.Private into two interfaces (Private and PrivateRead)
|
||||||
|
// and have this code take PrivateRead as input. We would then add ourselves as a listener to
|
||||||
|
// the PrivateRead. This would allow us to only reload the CA contents when they actually change.
|
||||||
ca, err := certauthority.Load(string(caCrtPEM), string(caKeyPEM))
|
ca, err := certauthority.Load(string(caCrtPEM), string(caKeyPEM))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ca.IssuePEM(subject, dnsNames, ttl)
|
return ca.IssueClientCertPEM(username, groups, ttl)
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package dynamiccertauthority
|
package dynamiccertauthority
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509/pkix"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
|
"go.pinniped.dev/internal/issuer"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCAIssuePEM(t *testing.T) {
|
func TestCAIssuePEM(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
provider := dynamiccert.New()
|
provider := dynamiccert.NewCA(t.Name())
|
||||||
ca := New(provider)
|
ca := New(provider)
|
||||||
|
|
||||||
goodCACrtPEM0, goodCAKeyPEM0, err := testutil.CreateCertificate(
|
goodCACrtPEM0, goodCAKeyPEM0, err := testutil.CreateCertificate(
|
||||||
@ -44,12 +44,12 @@ func TestCAIssuePEM(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "only cert",
|
name: "only cert",
|
||||||
caCrtPEM: goodCACrtPEM0,
|
caCrtPEM: goodCACrtPEM0,
|
||||||
wantError: "could not load CA: tls: failed to find any PEM data in key input",
|
wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in key input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only key",
|
name: "only key",
|
||||||
caKeyPEM: goodCAKeyPEM0,
|
caKeyPEM: goodCAKeyPEM0,
|
||||||
wantError: "could not load CA: tls: failed to find any PEM data in certificate input",
|
wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "new cert+key",
|
name: "new cert+key",
|
||||||
@ -68,19 +68,19 @@ func TestCAIssuePEM(t *testing.T) {
|
|||||||
name: "bad cert",
|
name: "bad cert",
|
||||||
caCrtPEM: []byte("this is not a cert"),
|
caCrtPEM: []byte("this is not a cert"),
|
||||||
caKeyPEM: goodCAKeyPEM0,
|
caKeyPEM: goodCAKeyPEM0,
|
||||||
wantError: "could not load CA: tls: failed to find any PEM data in certificate input",
|
wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad key",
|
name: "bad key",
|
||||||
caCrtPEM: goodCACrtPEM0,
|
caCrtPEM: goodCACrtPEM0,
|
||||||
caKeyPEM: []byte("this is not a key"),
|
caKeyPEM: []byte("this is not a key"),
|
||||||
wantError: "could not load CA: tls: failed to find any PEM data in key input",
|
wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in key input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mismatch cert+key",
|
name: "mismatch cert+key",
|
||||||
caCrtPEM: goodCACrtPEM0,
|
caCrtPEM: goodCACrtPEM0,
|
||||||
caKeyPEM: goodCAKeyPEM1,
|
caKeyPEM: goodCAKeyPEM1,
|
||||||
wantError: "could not load CA: tls: private key does not match public key",
|
wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: private key does not match public key",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good cert+key again",
|
name: "good cert+key again",
|
||||||
@ -94,17 +94,7 @@ func TestCAIssuePEM(t *testing.T) {
|
|||||||
// Can't run these steps in parallel, because each one depends on the previous steps being
|
// Can't run these steps in parallel, because each one depends on the previous steps being
|
||||||
// run.
|
// run.
|
||||||
|
|
||||||
if step.caCrtPEM != nil || step.caKeyPEM != nil {
|
crtPEM, keyPEM, err := issuePEM(provider, ca, step.caCrtPEM, step.caKeyPEM)
|
||||||
provider.Set(step.caCrtPEM, step.caKeyPEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
crtPEM, keyPEM, err := ca.IssuePEM(
|
|
||||||
pkix.Name{
|
|
||||||
CommonName: "some-common-name",
|
|
||||||
},
|
|
||||||
[]string{"some-dns-name", "some-other-dns-name"},
|
|
||||||
time.Hour*24,
|
|
||||||
)
|
|
||||||
|
|
||||||
if step.wantError != "" {
|
if step.wantError != "" {
|
||||||
require.EqualError(t, err, step.wantError)
|
require.EqualError(t, err, step.wantError)
|
||||||
@ -116,13 +106,24 @@ func TestCAIssuePEM(t *testing.T) {
|
|||||||
require.NotEmpty(t, keyPEM)
|
require.NotEmpty(t, keyPEM)
|
||||||
|
|
||||||
caCrtPEM, _ := provider.CurrentCertKeyContent()
|
caCrtPEM, _ := provider.CurrentCertKeyContent()
|
||||||
crtAssertions := testutil.ValidateCertificate(t, string(caCrtPEM), string(crtPEM))
|
crtAssertions := testutil.ValidateClientCertificate(t, string(caCrtPEM), string(crtPEM))
|
||||||
crtAssertions.RequireCommonName("some-common-name")
|
crtAssertions.RequireCommonName("some-username")
|
||||||
crtAssertions.RequireDNSName("some-dns-name")
|
crtAssertions.RequireOrganizations([]string{"some-group1", "some-group2"})
|
||||||
crtAssertions.RequireDNSName("some-other-dns-name")
|
|
||||||
crtAssertions.RequireLifetime(time.Now(), time.Now().Add(time.Hour*24), time.Minute*10)
|
crtAssertions.RequireLifetime(time.Now(), time.Now().Add(time.Hour*24), time.Minute*10)
|
||||||
crtAssertions.RequireMatchesPrivateKey(string(keyPEM))
|
crtAssertions.RequireMatchesPrivateKey(string(keyPEM))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func issuePEM(provider dynamiccert.Provider, ca issuer.ClientCertIssuer, caCrt, caKey []byte) ([]byte, []byte, error) {
|
||||||
|
// if setting fails, look at that error
|
||||||
|
if caCrt != nil || caKey != nil {
|
||||||
|
if err := provider.SetCertKeyContent(caCrt, caKey); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise check to see if their is an issuing error
|
||||||
|
return ca.IssueClientCertPEM("some-username", []string{"some-group1", "some-group2"}, time.Hour*24)
|
||||||
|
}
|
||||||
|
63
internal/clusterhost/clusterhost.go
Normal file
63
internal/clusterhost/clusterhost.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package clusterhost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
labelNodeRolePrefix = "node-role.kubernetes.io/"
|
||||||
|
nodeLabelRole = "kubernetes.io/node-role"
|
||||||
|
controlPlaneNodeRole = "control-plane"
|
||||||
|
// this role was deprecated by kubernetes 1.20.
|
||||||
|
masterNodeRole = "master"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterHost struct {
|
||||||
|
client kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(client kubernetes.Interface) *ClusterHost {
|
||||||
|
return &ClusterHost{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterHost) HasControlPlaneNodes(ctx context.Context) (bool, error) {
|
||||||
|
nodes, err := c.client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error fetching nodes: %v", err)
|
||||||
|
}
|
||||||
|
if len(nodes.Items) == 0 {
|
||||||
|
return false, fmt.Errorf("no nodes found")
|
||||||
|
}
|
||||||
|
for _, node := range nodes.Items {
|
||||||
|
for k, v := range node.Labels {
|
||||||
|
if isControlPlaneNodeRole(k, v) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isControlPlaneNodeRole(k string, v string) bool {
|
||||||
|
if k == labelNodeRolePrefix+controlPlaneNodeRole {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if k == labelNodeRolePrefix+masterNodeRole {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if k == nodeLabelRole && v == controlPlaneNodeRole {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if k == nodeLabelRole && v == masterNodeRole {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
169
internal/clusterhost/clusterhost_test.go
Normal file
169
internal/clusterhost/clusterhost_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package clusterhost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||||
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHasControlPlaneNodes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nodes []*v1.Node
|
||||||
|
listNodesErr error
|
||||||
|
wantErr error
|
||||||
|
wantReturnValue bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Fetching nodes returns an error",
|
||||||
|
listNodesErr: errors.New("couldn't get nodes"),
|
||||||
|
wantErr: errors.New("error fetching nodes: couldn't get nodes"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fetching nodes returns an empty array",
|
||||||
|
nodes: []*v1.Node{},
|
||||||
|
wantErr: errors.New("no nodes found"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nodes found, but not control plane nodes",
|
||||||
|
nodes: []*v1.Node{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-1",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"not-control-plane-label": "some-value",
|
||||||
|
"kubernetes.io/node-role": "worker",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-2",
|
||||||
|
Labels: map[string]string{"node-role.kubernetes.io/worker": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReturnValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nodes found, including a control-plane role in node-role.kubernetes.io/<role> format",
|
||||||
|
nodes: []*v1.Node{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-1",
|
||||||
|
Labels: map[string]string{"unrelated-label": "some-value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-2",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"some-other-label": "some-value",
|
||||||
|
"node-role.kubernetes.io/control-plane": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReturnValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nodes found, including a master role in node-role.kubernetes.io/<role> format",
|
||||||
|
nodes: []*v1.Node{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-1",
|
||||||
|
Labels: map[string]string{"unrelated-label": "some-value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-2",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"some-other-label": "some-value",
|
||||||
|
"node-role.kubernetes.io/master": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReturnValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nodes found, including a control-plane role in kubernetes.io/node-role=<role> format",
|
||||||
|
nodes: []*v1.Node{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-1",
|
||||||
|
Labels: map[string]string{"unrelated-label": "some-value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-2",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"some-other-label": "some-value",
|
||||||
|
"kubernetes.io/node-role": "control-plane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReturnValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nodes found, including a master role in kubernetes.io/node-role=<role> format",
|
||||||
|
nodes: []*v1.Node{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-1",
|
||||||
|
Labels: map[string]string{"unrelated-label": "some-value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node-2",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"some-other-label": "some-value",
|
||||||
|
"kubernetes.io/node-role": "master",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReturnValue: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
test := tt
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
kubeClient := kubernetesfake.NewSimpleClientset()
|
||||||
|
if test.listNodesErr != nil {
|
||||||
|
listNodesErr := test.listNodesErr
|
||||||
|
kubeClient.PrependReactor(
|
||||||
|
"list",
|
||||||
|
"nodes",
|
||||||
|
func(_ coretesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, listNodesErr
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for _, node := range test.nodes {
|
||||||
|
err := kubeClient.Tracker().Add(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
clusterHost := New(kubeClient)
|
||||||
|
hasControlPlaneNodes, err := clusterHost.HasControlPlaneNodes(context.Background())
|
||||||
|
require.Equal(t, test.wantErr, err)
|
||||||
|
require.Equal(t, test.wantReturnValue, hasControlPlaneNodes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import (
|
|||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
"k8s.io/client-go/pkg/version"
|
"k8s.io/client-go/pkg/version"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/issuer"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
"go.pinniped.dev/internal/registry/credentialrequest"
|
"go.pinniped.dev/internal/registry/credentialrequest"
|
||||||
"go.pinniped.dev/internal/registry/whoamirequest"
|
"go.pinniped.dev/internal/registry/whoamirequest"
|
||||||
@ -27,7 +28,7 @@ type Config struct {
|
|||||||
|
|
||||||
type ExtraConfig struct {
|
type ExtraConfig struct {
|
||||||
Authenticator credentialrequest.TokenCredentialRequestAuthenticator
|
Authenticator credentialrequest.TokenCredentialRequestAuthenticator
|
||||||
Issuer credentialrequest.CertIssuer
|
Issuer issuer.ClientCertIssuer
|
||||||
StartControllersPostStartHook func(ctx context.Context)
|
StartControllersPostStartHook func(ctx context.Context)
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
NegotiatedSerializer runtime.NegotiatedSerializer
|
NegotiatedSerializer runtime.NegotiatedSerializer
|
||||||
|
65
internal/concierge/impersonator/config.go
Normal file
65
internal/concierge/impersonator/config.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package impersonator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Explicitly enable the impersonation proxy.
|
||||||
|
ModeEnabled Mode = "enabled"
|
||||||
|
|
||||||
|
// Explicitly disable the impersonation proxy.
|
||||||
|
ModeDisabled Mode = "disabled"
|
||||||
|
|
||||||
|
// Allow the proxy to decide if it should be enabled or disabled based upon the cluster in which it is running.
|
||||||
|
ModeAuto Mode = "auto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigMapDataKey = "config.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Enable or disable the impersonation proxy. Optional. Defaults to ModeAuto.
|
||||||
|
Mode Mode `json:"mode,omitempty"`
|
||||||
|
|
||||||
|
// Used when creating TLS certificates and for clients to discover the endpoint. Optional. When not specified, if the
|
||||||
|
// impersonation proxy is started, then it will automatically create a LoadBalancer Service and use its ingress as the
|
||||||
|
// endpoint.
|
||||||
|
//
|
||||||
|
// When specified, it may be a hostname or IP address, optionally with a port number, of the impersonation proxy
|
||||||
|
// for clients to use from outside the cluster. E.g. myhost.mycompany.com:8443. Clients should assume that they should
|
||||||
|
// connect via HTTPS to this service.
|
||||||
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) HasEndpoint() bool {
|
||||||
|
return c.Endpoint != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{Mode: ModeAuto}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFromConfigMap(configMap *v1.ConfigMap) (*Config, error) {
|
||||||
|
stringConfig, ok := configMap.Data[ConfigMapDataKey]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(`ConfigMap is missing expected key "%s"`, ConfigMapDataKey)
|
||||||
|
}
|
||||||
|
config := NewConfig()
|
||||||
|
if err := yaml.Unmarshal([]byte(stringConfig), config); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode yaml: %w", err)
|
||||||
|
}
|
||||||
|
if config.Mode != ModeAuto && config.Mode != ModeEnabled && config.Mode != ModeDisabled {
|
||||||
|
return nil, fmt.Errorf(`illegal value for "mode": %s`, config.Mode)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
155
internal/concierge/impersonator/config_test.go
Normal file
155
internal/concierge/impersonator/config_test.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package impersonator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/here"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewConfig(t *testing.T) {
|
||||||
|
// It defaults the mode.
|
||||||
|
require.Equal(t, &Config{Mode: ModeAuto}, NewConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasEndpoint(t *testing.T) {
|
||||||
|
configWithoutEndpoint := Config{}
|
||||||
|
configWithEndpoint := Config{Endpoint: "something"}
|
||||||
|
require.False(t, configWithoutEndpoint.HasEndpoint())
|
||||||
|
require.True(t, configWithEndpoint.HasEndpoint())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFromConfigMap(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configMap *v1.ConfigMap
|
||||||
|
wantConfig *Config
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fully configured, valid config",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": here.Doc(`
|
||||||
|
mode: enabled
|
||||||
|
endpoint: proxy.example.com:8443
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantConfig: &Config{
|
||||||
|
Mode: "enabled",
|
||||||
|
Endpoint: "proxy.example.com:8443",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty, valid config",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantConfig: &Config{
|
||||||
|
Mode: "auto",
|
||||||
|
Endpoint: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid config with mode enabled",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": "mode: enabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantConfig: &Config{
|
||||||
|
Mode: "enabled",
|
||||||
|
Endpoint: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid config with mode disabled",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": "mode: disabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantConfig: &Config{
|
||||||
|
Mode: "disabled",
|
||||||
|
Endpoint: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid config with mode auto",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": "mode: auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantConfig: &Config{
|
||||||
|
Mode: "auto",
|
||||||
|
Endpoint: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong key in configmap",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"wrong-key": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantError: `ConfigMap is missing expected key "config.yaml"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "illegal yaml in configmap",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": "this is not yaml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type impersonator.Config",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "illegal value for mode in configmap",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Data: map[string]string{
|
||||||
|
"config.yaml": "mode: unexpected-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantError: `illegal value for "mode": unexpected-value`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
test := tt
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
config, err := ConfigFromConfigMap(test.configMap)
|
||||||
|
require.Equal(t, test.wantConfig, config)
|
||||||
|
if test.wantError != "" {
|
||||||
|
require.EqualError(t, err, test.wantError)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
391
internal/concierge/impersonator/impersonator.go
Normal file
391
internal/concierge/impersonator/impersonator.go
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package impersonator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||||
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/filterlatency"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
|
"k8s.io/apiserver/pkg/server/filters"
|
||||||
|
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/transport"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/constable"
|
||||||
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
|
"go.pinniped.dev/internal/httputil/securityheader"
|
||||||
|
"go.pinniped.dev/internal/kubeclient"
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FactoryFunc is a function which can create an impersonator server.
|
||||||
|
// It returns a function which will start the impersonator server.
|
||||||
|
// That start function takes a stopCh which can be used to stop the server.
|
||||||
|
// Once a server has been stopped, don't start it again using the start function.
|
||||||
|
// Instead, call the factory function again to get a new start function.
|
||||||
|
type FactoryFunc func(
|
||||||
|
port int,
|
||||||
|
dynamicCertProvider dynamiccert.Private,
|
||||||
|
impersonationProxySignerCA dynamiccert.Public,
|
||||||
|
) (func(stopCh <-chan struct{}) error, error)
|
||||||
|
|
||||||
|
func New(
|
||||||
|
port int,
|
||||||
|
dynamicCertProvider dynamiccert.Private,
|
||||||
|
impersonationProxySignerCA dynamiccert.Public,
|
||||||
|
) (func(stopCh <-chan struct{}) error, error) {
|
||||||
|
return newInternal(port, dynamicCertProvider, impersonationProxySignerCA, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInternal( //nolint:funlen // yeah, it's kind of long.
|
||||||
|
port int,
|
||||||
|
dynamicCertProvider dynamiccert.Private,
|
||||||
|
impersonationProxySignerCA dynamiccert.Public,
|
||||||
|
clientOpts []kubeclient.Option, // for unit testing, should always be nil in production
|
||||||
|
recOpts func(*genericoptions.RecommendedOptions), // for unit testing, should always be nil in production
|
||||||
|
) (func(stopCh <-chan struct{}) error, error) {
|
||||||
|
var listener net.Listener
|
||||||
|
|
||||||
|
constructServer := func() (func(stopCh <-chan struct{}) error, error) {
|
||||||
|
// Bare minimum server side scheme to allow for status messages to be encoded.
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
|
||||||
|
// This is unused for now but it is a safe value that we could use in the future.
|
||||||
|
defaultEtcdPathPrefix := "/pinniped-impersonation-proxy-registry"
|
||||||
|
|
||||||
|
recommendedOptions := genericoptions.NewRecommendedOptions(
|
||||||
|
defaultEtcdPathPrefix,
|
||||||
|
codecs.LegacyCodec(),
|
||||||
|
)
|
||||||
|
recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet
|
||||||
|
recommendedOptions.SecureServing.ServerCert.GeneratedCert = dynamicCertProvider // serving certs (end user facing)
|
||||||
|
recommendedOptions.SecureServing.BindPort = port
|
||||||
|
|
||||||
|
// Wire up the impersonation proxy signer CA as another valid authenticator for client cert auth,
|
||||||
|
// along with the Kube API server's CA.
|
||||||
|
// Note: any changes to the the Authentication stack need to be kept in sync with any assumptions made
|
||||||
|
// by getTransportForUser, especially if we ever update the TCR API to start returning bearer tokens.
|
||||||
|
kubeClient, err := kubeclient.New(clientOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kubeClientCA, err := dynamiccertificates.NewDynamicCAFromConfigMapController(
|
||||||
|
"client-ca", metav1.NamespaceSystem, "extension-apiserver-authentication", "client-ca-file", kubeClient.Kubernetes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recommendedOptions.Authentication.ClientCert.ClientCA = "---irrelevant-but-needs-to-be-non-empty---" // drop when we pick up https://github.com/kubernetes/kubernetes/pull/100055
|
||||||
|
recommendedOptions.Authentication.ClientCert.CAContentProvider = dynamiccertificates.NewUnionCAContentProvider(
|
||||||
|
impersonationProxySignerCA, kubeClientCA,
|
||||||
|
)
|
||||||
|
|
||||||
|
if recOpts != nil {
|
||||||
|
recOpts(recommendedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
|
||||||
|
|
||||||
|
// Note that ApplyTo is going to create a network listener and bind to the requested port.
|
||||||
|
// It puts this listener into serverConfig.SecureServing.Listener.
|
||||||
|
err = recommendedOptions.ApplyTo(serverConfig)
|
||||||
|
if serverConfig.SecureServing != nil {
|
||||||
|
// Set the pointer from the outer function to allow the outer function to close the listener in case
|
||||||
|
// this function returns an error for any reason anywhere below here.
|
||||||
|
listener = serverConfig.SecureServing.Listener
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loopback authentication to this server does not really make sense since we just proxy everything to
|
||||||
|
// the Kube API server, thus we replace loopback connection config with one that does direct connections
|
||||||
|
// the Kube API server. Loopback config is mainly used by post start hooks, so this is mostly future proofing.
|
||||||
|
serverConfig.LoopbackClientConfig = rest.CopyConfig(kubeClient.ProtoConfig) // assume proto is safe (hooks can override)
|
||||||
|
// Remove the bearer token so our authorizer does not get stomped on by AuthorizeClientBearerToken.
|
||||||
|
// See sanity checks at the end of this function.
|
||||||
|
serverConfig.LoopbackClientConfig.BearerToken = ""
|
||||||
|
|
||||||
|
// match KAS exactly since our long running operations are just a proxy to it
|
||||||
|
// this must be kept in sync with github.com/kubernetes/kubernetes/cmd/kube-apiserver/app/server.go
|
||||||
|
// this is nothing to stress about - it has not changed since the beginning of Kube:
|
||||||
|
// v1.6 no-op move away from regex to request info https://github.com/kubernetes/kubernetes/pull/38119
|
||||||
|
// v1.1 added pods/attach to the list https://github.com/kubernetes/kubernetes/pull/13705
|
||||||
|
serverConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck(
|
||||||
|
sets.NewString("watch", "proxy"),
|
||||||
|
sets.NewString("attach", "exec", "proxy", "log", "portforward"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assume proto config is safe because transport level configs do not use rest.ContentConfig.
|
||||||
|
// Thus if we are interacting with actual APIs, they should be using pre-built clients.
|
||||||
|
impersonationProxyFunc, err := newImpersonationReverseProxyFunc(rest.CopyConfig(kubeClient.ProtoConfig))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultBuildHandlerChainFunc := serverConfig.BuildHandlerChainFunc
|
||||||
|
serverConfig.BuildHandlerChainFunc = func(_ http.Handler, c *genericapiserver.Config) http.Handler {
|
||||||
|
// We ignore the passed in handler because we never have any REST APIs to delegate to.
|
||||||
|
// This means we are ignoring the admission, discovery, REST storage, etc layers.
|
||||||
|
doNotDelegate := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})
|
||||||
|
|
||||||
|
// Impersonation proxy business logic with timing information.
|
||||||
|
impersonationProxyCompleted := filterlatency.TrackCompleted(doNotDelegate)
|
||||||
|
impersonationProxy := impersonationProxyFunc(c)
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer impersonationProxyCompleted.ServeHTTP(w, r)
|
||||||
|
impersonationProxy.ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
handler = filterlatency.TrackStarted(handler, "impersonationproxy")
|
||||||
|
|
||||||
|
// The standard Kube handler chain (authn, authz, impersonation, audit, etc).
|
||||||
|
// See the genericapiserver.DefaultBuildHandlerChain func for details.
|
||||||
|
handler = defaultBuildHandlerChainFunc(handler, c)
|
||||||
|
|
||||||
|
// Always set security headers so browsers do the right thing.
|
||||||
|
handler = securityheader.Wrap(handler)
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the delegating authorizer with one that only cares about impersonation.
|
||||||
|
// Empty string is disallowed because request info has had bugs in the past where it would leave it empty.
|
||||||
|
disallowedVerbs := sets.NewString("", "impersonate")
|
||||||
|
noImpersonationAuthorizer := &comparableAuthorizer{
|
||||||
|
AuthorizerFunc: func(a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
// Supporting impersonation is not hard, it would just require a bunch of testing
|
||||||
|
// and configuring the audit layer (to preserve the caller) which we can do later.
|
||||||
|
// We would also want to delete the incoming impersonation headers
|
||||||
|
// instead of overwriting the delegating authorizer, we would
|
||||||
|
// actually use it to make the impersonation authorization checks.
|
||||||
|
if disallowedVerbs.Has(a.GetVerb()) {
|
||||||
|
return authorizer.DecisionDeny, "impersonation is not allowed or invalid verb", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizer.DecisionAllow, "deferring authorization to kube API server", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Set our custom authorizer before calling Compete(), which will use it.
|
||||||
|
serverConfig.Authorization.Authorizer = noImpersonationAuthorizer
|
||||||
|
|
||||||
|
impersonationProxyServer, err := serverConfig.Complete().New("impersonation-proxy", genericapiserver.NewEmptyDelegate())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedRun := impersonationProxyServer.PrepareRun()
|
||||||
|
|
||||||
|
// Sanity check. Make sure that our custom authorizer is still in place and did not get changed or wrapped.
|
||||||
|
if preparedRun.Authorizer != noImpersonationAuthorizer {
|
||||||
|
return nil, constable.Error("invalid mutation of impersonation authorizer detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check. Assert that we have a functioning token file to use and no bearer token.
|
||||||
|
if len(preparedRun.LoopbackClientConfig.BearerToken) != 0 || len(preparedRun.LoopbackClientConfig.BearerTokenFile) == 0 {
|
||||||
|
return nil, constable.Error("invalid impersonator loopback rest config has wrong bearer token semantics")
|
||||||
|
}
|
||||||
|
|
||||||
|
return preparedRun.Run, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := constructServer()
|
||||||
|
// If there was any error during construction, then we would like to close the listener to free up the port.
|
||||||
|
if err != nil {
|
||||||
|
errs := []error{err}
|
||||||
|
if listener != nil {
|
||||||
|
errs = append(errs, listener.Close())
|
||||||
|
}
|
||||||
|
return nil, errors.NewAggregate(errs)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-op wrapping around AuthorizerFunc to allow for comparisons.
|
||||||
|
type comparableAuthorizer struct {
|
||||||
|
authorizer.AuthorizerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapiserver.Config) http.Handler, error) {
|
||||||
|
serverURL, err := url.Parse(restConfig.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse host URL from in-cluster config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http1RoundTripper, err := getTransportForProtocol(restConfig, "http/1.1")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get http/1.1 round tripper: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http2RoundTripper, err := getTransportForProtocol(restConfig, "h2")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get http/2.0 round tripper: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *genericapiserver.Config) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if len(r.Header.Values("Authorization")) != 0 {
|
||||||
|
plog.Warning("aggregated API server logic did not delete authorization header but it is always supposed to do so",
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
)
|
||||||
|
newInternalErrResponse(w, r, c.Serializer, "invalid authorization header")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureNoImpersonationHeaders(r); err != nil {
|
||||||
|
plog.Error("noImpersonationAuthorizer logic did not prevent nested impersonation but it is always supposed to do so",
|
||||||
|
err,
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
)
|
||||||
|
newInternalErrResponse(w, r, c.Serializer, "invalid impersonation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, ok := request.UserFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
plog.Warning("aggregated API server logic did not set user info but it is always supposed to do so",
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
)
|
||||||
|
newInternalErrResponse(w, r, c.Serializer, "invalid user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// KAS only supports upgrades via http/1.1 to websockets/SPDY (upgrades never use http/2.0)
|
||||||
|
// Thus we default to using http/2.0 when the request is not an upgrade, otherwise we use http/1.1
|
||||||
|
baseRT := http2RoundTripper
|
||||||
|
isUpgradeRequest := httpstream.IsUpgradeRequest(r)
|
||||||
|
if isUpgradeRequest {
|
||||||
|
baseRT = http1RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
rt, err := getTransportForUser(userInfo, baseRT)
|
||||||
|
if err != nil {
|
||||||
|
plog.WarningErr("rejecting request as we cannot act as the current user", err,
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
"isUpgradeRequest", isUpgradeRequest,
|
||||||
|
)
|
||||||
|
newInternalErrResponse(w, r, c.Serializer, "unimplemented functionality - unable to act as current user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Debug("impersonation proxy servicing request",
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
"isUpgradeRequest", isUpgradeRequest,
|
||||||
|
)
|
||||||
|
plog.Trace("impersonation proxy servicing request was for user",
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
"isUpgradeRequest", isUpgradeRequest,
|
||||||
|
"username", userInfo.GetName(), // this info leak seems fine for trace level logs
|
||||||
|
)
|
||||||
|
|
||||||
|
// The proxy library used below will panic when the client disconnects abruptly, so in order to
|
||||||
|
// assure that this log message is always printed at the end of this func, it must be deferred.
|
||||||
|
defer plog.Debug("impersonation proxy finished servicing request",
|
||||||
|
"url", r.URL.String(),
|
||||||
|
"method", r.Method,
|
||||||
|
"isUpgradeRequest", isUpgradeRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
// do not allow the client to cause log confusion by spoofing this header
|
||||||
|
if len(r.Header.Values("X-Forwarded-For")) > 0 {
|
||||||
|
r = utilnet.CloneRequest(r)
|
||||||
|
r.Header.Del("X-Forwarded-For")
|
||||||
|
}
|
||||||
|
|
||||||
|
reverseProxy := httputil.NewSingleHostReverseProxy(serverURL)
|
||||||
|
reverseProxy.Transport = rt
|
||||||
|
reverseProxy.FlushInterval = 200 * time.Millisecond // the "watch" verb will not work without this line
|
||||||
|
reverseProxy.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureNoImpersonationHeaders(r *http.Request) error {
|
||||||
|
for key := range r.Header {
|
||||||
|
if strings.HasPrefix(key, "Impersonate") {
|
||||||
|
return fmt.Errorf("%q header already exists", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportForUser(userInfo user.Info, delegate http.RoundTripper) (http.RoundTripper, error) {
|
||||||
|
if len(userInfo.GetUID()) == 0 {
|
||||||
|
impersonateConfig := transport.ImpersonationConfig{
|
||||||
|
UserName: userInfo.GetName(),
|
||||||
|
Groups: userInfo.GetGroups(),
|
||||||
|
Extra: userInfo.GetExtra(),
|
||||||
|
}
|
||||||
|
// transport.NewImpersonatingRoundTripper clones the request before setting headers
|
||||||
|
// thus it will not accidentally mutate the input request (see http.Handler docs)
|
||||||
|
return transport.NewImpersonatingRoundTripper(impersonateConfig, delegate), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0. in the case of a request that is not attempting to do nested impersonation
|
||||||
|
// 1. if we make the assumption that the TCR API does not issue tokens (or pass the TCR API bearer token
|
||||||
|
// authenticator into this func - we need to know the authentication cred is something KAS would honor)
|
||||||
|
// 2. then if preserve the incoming authorization header into the request's context
|
||||||
|
// 3. we could reauthenticate it here (it would be a free cache hit)
|
||||||
|
// 4. confirm that it matches the passed in user info (i.e. it was actually the cred used to authenticate and not a client cert)
|
||||||
|
// 5. then we could issue a reverse proxy request using an anonymous rest config and the bearer token
|
||||||
|
// 6. thus instead of impersonating the user, we would just be passing their request through
|
||||||
|
// 7. this would preserve the UID info and thus allow us to safely support all token based auth
|
||||||
|
// 8. the above would be safe even if in the future Kube started supporting UIDs asserted by client certs
|
||||||
|
return nil, constable.Error("unexpected uid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInternalErrResponse(w http.ResponseWriter, r *http.Request, s runtime.NegotiatedSerializer, msg string) {
|
||||||
|
newStatusErrResponse(w, r, s, apierrors.NewInternalError(constable.Error(msg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStatusErrResponse(w http.ResponseWriter, r *http.Request, s runtime.NegotiatedSerializer, err *apierrors.StatusError) {
|
||||||
|
requestInfo, ok := genericapirequest.RequestInfoFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
responsewriters.InternalError(w, r, constable.Error("no RequestInfo found in the context"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||||
|
responsewriters.ErrorNegotiated(err, s, gv, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportForProtocol(restConfig *rest.Config, protocol string) (http.RoundTripper, error) {
|
||||||
|
transportConfig, err := restConfig.TransportConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get in-cluster transport config: %w", err)
|
||||||
|
}
|
||||||
|
transportConfig.TLS.NextProtos = []string{protocol}
|
||||||
|
|
||||||
|
return transport.New(transportConfig)
|
||||||
|
}
|
638
internal/concierge/impersonator/impersonator_test.go
Normal file
638
internal/concierge/impersonator/impersonator_test.go
Normal file
@ -0,0 +1,638 @@
|
|||||||
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package impersonator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
|
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
|
"go.pinniped.dev/internal/here"
|
||||||
|
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||||
|
"go.pinniped.dev/internal/kubeclient"
|
||||||
|
"go.pinniped.dev/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImpersonator(t *testing.T) {
|
||||||
|
const port = 9444
|
||||||
|
|
||||||
|
ca, err := certauthority.New("ca", time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
caKey, err := ca.PrivateKeyToPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
caContent := dynamiccert.NewCA("ca")
|
||||||
|
err = caContent.SetCertKeyContent(ca.Bundle(), caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cert, key, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
certKeyContent := dynamiccert.NewServingCert("cert-key")
|
||||||
|
err = certKeyContent.SetCertKeyContent(cert, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
unrelatedCA, err := certauthority.New("ca", time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Punch out just enough stuff to make New actually run without error.
|
||||||
|
recOpts := func(options *genericoptions.RecommendedOptions) {
|
||||||
|
options.Authentication.RemoteKubeConfigFileOptional = true
|
||||||
|
options.Authorization.RemoteKubeConfigFileOptional = true
|
||||||
|
options.CoreAPI = nil
|
||||||
|
options.Admission = nil
|
||||||
|
}
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIPriorityAndFairness, false)()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
clientCert *clientCert
|
||||||
|
clientImpersonateUser rest.ImpersonationConfig
|
||||||
|
clientMutateHeaders func(http.Header)
|
||||||
|
clientNextProtos []string
|
||||||
|
kubeAPIServerClientBearerTokenFile string
|
||||||
|
kubeAPIServerStatusCode int
|
||||||
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
|
wantError string
|
||||||
|
wantConstructionError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"test-username"},
|
||||||
|
"Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with upgrade",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username2", []string{"test-group3", "test-group4"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Add("Connection", "Upgrade")
|
||||||
|
header.Add("Upgrade", "spdy/3.1")
|
||||||
|
|
||||||
|
if ok := httpstream.IsUpgradeRequest(&http.Request{Header: header}); !ok {
|
||||||
|
panic("request must be upgrade in this test")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clientNextProtos: []string{"http/1.1"}, // we need to use http1 as http2 does not support upgrades, see http2checkConnHeaders
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"test-username2"},
|
||||||
|
"Impersonate-Group": {"test-group3", "test-group4", "system:authenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"spdy/3.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path ignores forwarded header",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username2", []string{"test-group3", "test-group4"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Add("X-Forwarded-For", "example.com")
|
||||||
|
},
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"test-username2"},
|
||||||
|
"Impersonate-Group": {"test-group3", "test-group4", "system:authenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path ignores forwarded header canonicalization",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username2", []string{"test-group3", "test-group4"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Add("x-FORWARDED-for", "example.com")
|
||||||
|
},
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"test-username2"},
|
||||||
|
"Impersonate-Group": {"test-group3", "test-group4", "system:authenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user is authenticated but the kube API request returns an error",
|
||||||
|
kubeAPIServerStatusCode: http.StatusNotFound,
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: `the server could not find the requested resource (get namespaces)`,
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"test-username"},
|
||||||
|
"Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when there is no client cert on request, it is an anonymous request",
|
||||||
|
clientCert: &clientCert{},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"system:anonymous"},
|
||||||
|
"Impersonate-Group": {"system:unauthenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "failed client cert authentication",
|
||||||
|
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: "Unauthorized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double impersonation is not allowed by regular users",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
clientImpersonateUser: rest.ImpersonationConfig{UserName: "some-other-username"},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: `users "some-other-username" is forbidden: User "test-username" ` +
|
||||||
|
`cannot impersonate resource "users" in API group "" at the cluster scope: impersonation is not allowed or invalid verb`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double impersonation is not allowed by admin users",
|
||||||
|
clientCert: newClientCert(t, ca, "test-admin", []string{"system:masters", "test-group2"}),
|
||||||
|
clientImpersonateUser: rest.ImpersonationConfig{UserName: "some-other-username"},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: `users "some-other-username" is forbidden: User "test-admin" ` +
|
||||||
|
`cannot impersonate resource "users" in API group "" at the cluster scope: impersonation is not allowed or invalid verb`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no bearer token file in Kube API server client config",
|
||||||
|
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header canonicalization user header",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Set("imPerSonaTE-USer", "PANDA")
|
||||||
|
},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: `users "PANDA" is forbidden: User "test-username" ` +
|
||||||
|
`cannot impersonate resource "users" in API group "" at the cluster scope: impersonation is not allowed or invalid verb`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header canonicalization future UID header",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Set("imPerSonaTE-uid", "007")
|
||||||
|
},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: "Internal error occurred: invalid impersonation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "future UID header",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Set("Impersonate-Uid", "008")
|
||||||
|
},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantError: "Internal error occurred: invalid impersonation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
// This is a serial test because the production code binds to the port.
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// After failing to start and after shutdown, the impersonator port should be available again.
|
||||||
|
defer requireCanBindToPort(t, port)
|
||||||
|
|
||||||
|
if tt.kubeAPIServerStatusCode == 0 {
|
||||||
|
tt.kubeAPIServerStatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a fake Kube API server which will stand in for the real one. The impersonator
|
||||||
|
// will proxy incoming calls to this fake server.
|
||||||
|
testKubeAPIServerWasCalled := false
|
||||||
|
var testKubeAPIServerSawHeaders http.Header
|
||||||
|
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/namespaces/kube-system/configmaps":
|
||||||
|
// The production code uses NewDynamicCAFromConfigMapController which fetches a ConfigMap,
|
||||||
|
// so treat that differently. It wants to read the Kube API server CA from that ConfigMap
|
||||||
|
// to use it to validate client certs. We don't need it for this test, so return NotFound.
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
case "/api/v1/namespaces":
|
||||||
|
testKubeAPIServerWasCalled = true
|
||||||
|
testKubeAPIServerSawHeaders = r.Header
|
||||||
|
if tt.kubeAPIServerStatusCode != http.StatusOK {
|
||||||
|
w.WriteHeader(tt.kubeAPIServerStatusCode)
|
||||||
|
} else {
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
_, _ = w.Write([]byte(here.Doc(`
|
||||||
|
{
|
||||||
|
"kind": "NamespaceList",
|
||||||
|
"apiVersion":"v1",
|
||||||
|
"items": [
|
||||||
|
{"metadata":{"name": "namespace1"}},
|
||||||
|
{"metadata":{"name": "namespace2"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`)))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
require.Fail(t, "fake Kube API server got an unexpected request")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create the client config that the impersonation server should use to talk to the Kube API server.
|
||||||
|
testKubeAPIServerKubeconfig := rest.Config{
|
||||||
|
Host: testKubeAPIServerURL,
|
||||||
|
BearerToken: "some-service-account-token",
|
||||||
|
TLSClientConfig: rest.TLSClientConfig{CAData: []byte(testKubeAPIServerCA)},
|
||||||
|
BearerTokenFile: tt.kubeAPIServerClientBearerTokenFile,
|
||||||
|
}
|
||||||
|
clientOpts := []kubeclient.Option{kubeclient.WithConfig(&testKubeAPIServerKubeconfig)}
|
||||||
|
|
||||||
|
// Create an impersonator.
|
||||||
|
runner, constructionErr := newInternal(port, certKeyContent, caContent, clientOpts, recOpts)
|
||||||
|
if len(tt.wantConstructionError) > 0 {
|
||||||
|
require.EqualError(t, constructionErr, tt.wantConstructionError)
|
||||||
|
require.Nil(t, runner)
|
||||||
|
// The rest of the test doesn't make sense when you expect a construction error, so stop here.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, constructionErr)
|
||||||
|
require.NotNil(t, runner)
|
||||||
|
|
||||||
|
// Start the impersonator.
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
stopErr := runner(stopCh)
|
||||||
|
errCh <- stopErr
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a kubeconfig to talk to the impersonator as a client.
|
||||||
|
clientKubeconfig := &rest.Config{
|
||||||
|
Host: "https://127.0.0.1:" + strconv.Itoa(port),
|
||||||
|
TLSClientConfig: rest.TLSClientConfig{
|
||||||
|
CAData: ca.Bundle(),
|
||||||
|
CertData: tt.clientCert.certPEM,
|
||||||
|
KeyData: tt.clientCert.keyPEM,
|
||||||
|
NextProtos: tt.clientNextProtos,
|
||||||
|
},
|
||||||
|
UserAgent: "test-agent",
|
||||||
|
// BearerToken should be ignored during auth when there are valid client certs,
|
||||||
|
// and it should not passed into the impersonator handler func as an authorization header.
|
||||||
|
BearerToken: "must-be-ignored",
|
||||||
|
Impersonate: tt.clientImpersonateUser,
|
||||||
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
|
||||||
|
if tt.clientMutateHeaders == nil {
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
|
return roundtripper.Func(func(req *http.Request) (*http.Response, error) {
|
||||||
|
req = req.Clone(req.Context())
|
||||||
|
tt.clientMutateHeaders(req.Header)
|
||||||
|
return rt.RoundTrip(req)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a real Kube client to make API requests to the impersonator.
|
||||||
|
client, err := kubeclient.New(kubeclient.WithConfig(clientKubeconfig))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The fake Kube API server knows how to to list namespaces, so make that request using the client
|
||||||
|
// through the impersonator.
|
||||||
|
listResponse, err := client.Kubernetes.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
|
||||||
|
if len(tt.wantError) > 0 {
|
||||||
|
require.EqualError(t, err, tt.wantError)
|
||||||
|
require.Equal(t, &corev1.NamespaceList{}, listResponse)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &corev1.NamespaceList{
|
||||||
|
Items: []corev1.Namespace{
|
||||||
|
{ObjectMeta: metav1.ObjectMeta{Name: "namespace1"}},
|
||||||
|
{ObjectMeta: metav1.ObjectMeta{Name: "namespace2"}},
|
||||||
|
},
|
||||||
|
}, listResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we expect to see some headers, then the fake KAS should have been called.
|
||||||
|
require.Equal(t, len(tt.wantKubeAPIServerRequestHeaders) != 0, testKubeAPIServerWasCalled)
|
||||||
|
// If the impersonator proxied the request to the fake Kube API server, we should see the headers
|
||||||
|
// of the original request mutated by the impersonator. Otherwise the headers should be nil.
|
||||||
|
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
||||||
|
|
||||||
|
// Stop the impersonator server.
|
||||||
|
close(stopCh)
|
||||||
|
exitErr := <-errCh
|
||||||
|
require.NoError(t, exitErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||||
|
const testUser = "test-user"
|
||||||
|
|
||||||
|
testGroups := []string{"test-group-1", "test-group-2"}
|
||||||
|
testExtra := map[string][]string{
|
||||||
|
"extra-1": {"some", "extra", "stuff"},
|
||||||
|
"extra-2": {"some", "more", "extra", "stuff"},
|
||||||
|
}
|
||||||
|
|
||||||
|
validURL, _ := url.Parse("http://pinniped.dev/blah")
|
||||||
|
newRequest := func(h http.Header, userInfo user.Info) *http.Request {
|
||||||
|
ctx := context.Background()
|
||||||
|
if userInfo != nil {
|
||||||
|
ctx = request.WithUser(ctx, userInfo)
|
||||||
|
}
|
||||||
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, validURL.String(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.Header = h
|
||||||
|
reqInfo := &request.RequestInfo{
|
||||||
|
IsResourceRequest: false,
|
||||||
|
Path: validURL.Path,
|
||||||
|
Verb: "get",
|
||||||
|
}
|
||||||
|
r = r.WithContext(request.WithRequestInfo(ctx, reqInfo))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
restConfig *rest.Config
|
||||||
|
wantCreationErr string
|
||||||
|
request *http.Request
|
||||||
|
wantHTTPBody string
|
||||||
|
wantHTTPStatus int
|
||||||
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
|
kubeAPIServerStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid kubeconfig host",
|
||||||
|
restConfig: &rest.Config{Host: ":"},
|
||||||
|
wantCreationErr: "could not parse host URL from in-cluster config: parse \":\": missing protocol scheme",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid transport config",
|
||||||
|
restConfig: &rest.Config{
|
||||||
|
Host: "pinniped.dev/blah",
|
||||||
|
ExecProvider: &api.ExecConfig{},
|
||||||
|
AuthProvider: &api.AuthProviderConfig{},
|
||||||
|
},
|
||||||
|
wantCreationErr: "could not get http/1.1 round tripper: could not get in-cluster transport config: execProvider and authProvider cannot be used in combination",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail to get transport from config",
|
||||||
|
restConfig: &rest.Config{
|
||||||
|
Host: "pinniped.dev/blah",
|
||||||
|
BearerToken: "test-bearer-token",
|
||||||
|
Transport: http.DefaultTransport,
|
||||||
|
TLSClientConfig: rest.TLSClientConfig{Insecure: true},
|
||||||
|
},
|
||||||
|
wantCreationErr: "could not get http/1.1 round tripper: using a custom transport with TLS certificate options or the insecure flag is not allowed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Impersonate-User header already in request",
|
||||||
|
request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Impersonate-Group header already in request",
|
||||||
|
request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Impersonate-Extra header already in request",
|
||||||
|
request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Impersonate-* header already in request",
|
||||||
|
request: newRequest(map[string][]string{"Impersonate-Something": {"some-newfangled-impersonate-header"}}, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unexpected authorization header",
|
||||||
|
request: newRequest(map[string][]string{"Authorization": {"panda"}}, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid authorization header","reason":"InternalError","details":{"causes":[{"message":"invalid authorization header"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing user",
|
||||||
|
request: newRequest(map[string][]string{}, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid user","reason":"InternalError","details":{"causes":[{"message":"invalid user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unexpected UID",
|
||||||
|
request: newRequest(map[string][]string{}, &user.DefaultInfo{UID: "007"}),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
// happy path
|
||||||
|
{
|
||||||
|
name: "authenticated user",
|
||||||
|
request: newRequest(map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Accept": {"some-accepted-format"},
|
||||||
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
|
"Connection": {"Upgrade"}, // the value "Upgrade" is handled in a special way by `httputil.NewSingleHostReverseProxy`
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"}, // this header will be passed through
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
Name: testUser,
|
||||||
|
Groups: testGroups,
|
||||||
|
Extra: testExtra,
|
||||||
|
}),
|
||||||
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"Impersonate-Extra-Extra-1": {"some", "extra", "stuff"},
|
||||||
|
"Impersonate-Extra-Extra-2": {"some", "more", "extra", "stuff"},
|
||||||
|
"Impersonate-Group": {"test-group-1", "test-group-2"},
|
||||||
|
"Impersonate-User": {"test-user"},
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Accept": {"some-accepted-format"},
|
||||||
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
},
|
||||||
|
wantHTTPBody: "successful proxied response",
|
||||||
|
wantHTTPStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user is authenticated but the kube API request returns an error",
|
||||||
|
request: newRequest(map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
Name: testUser,
|
||||||
|
Groups: testGroups,
|
||||||
|
Extra: testExtra,
|
||||||
|
}),
|
||||||
|
kubeAPIServerStatusCode: http.StatusNotFound,
|
||||||
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
|
"Accept-Encoding": {"gzip"}, // because the rest client used in this test does not disable compression
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"Impersonate-Extra-Extra-1": {"some", "extra", "stuff"},
|
||||||
|
"Impersonate-Extra-Extra-2": {"some", "more", "extra", "stuff"},
|
||||||
|
"Impersonate-Group": {"test-group-1", "test-group-2"},
|
||||||
|
"Impersonate-User": {"test-user"},
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
},
|
||||||
|
wantHTTPStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if tt.kubeAPIServerStatusCode == 0 {
|
||||||
|
tt.kubeAPIServerStatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
testKubeAPIServerWasCalled := false
|
||||||
|
testKubeAPIServerSawHeaders := http.Header{}
|
||||||
|
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testKubeAPIServerWasCalled = true
|
||||||
|
testKubeAPIServerSawHeaders = r.Header
|
||||||
|
if tt.kubeAPIServerStatusCode != http.StatusOK {
|
||||||
|
w.WriteHeader(tt.kubeAPIServerStatusCode)
|
||||||
|
} else {
|
||||||
|
_, _ = w.Write([]byte("successful proxied response"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
testKubeAPIServerKubeconfig := rest.Config{
|
||||||
|
Host: testKubeAPIServerURL,
|
||||||
|
BearerToken: "some-service-account-token",
|
||||||
|
TLSClientConfig: rest.TLSClientConfig{CAData: []byte(testKubeAPIServerCA)},
|
||||||
|
}
|
||||||
|
if tt.restConfig == nil {
|
||||||
|
tt.restConfig = &testKubeAPIServerKubeconfig
|
||||||
|
}
|
||||||
|
|
||||||
|
impersonatorHTTPHandlerFunc, err := newImpersonationReverseProxyFunc(tt.restConfig)
|
||||||
|
if tt.wantCreationErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantCreationErr)
|
||||||
|
require.Nil(t, impersonatorHTTPHandlerFunc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, impersonatorHTTPHandlerFunc)
|
||||||
|
|
||||||
|
// this is not a valid way to get a server config, but it is good enough for a unit test
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
r := tt.request
|
||||||
|
wantKubeAPIServerRequestHeaders := tt.wantKubeAPIServerRequestHeaders
|
||||||
|
|
||||||
|
// take the isUpgradeRequest branch randomly to make sure we exercise both branches
|
||||||
|
forceUpgradeRequest := rand.Int()%2 == 0 //nolint:gosec // we do not care if this is cryptographically secure
|
||||||
|
if forceUpgradeRequest && len(r.Header.Get("Upgrade")) == 0 {
|
||||||
|
r = r.Clone(r.Context())
|
||||||
|
r.Header.Add("Connection", "Upgrade")
|
||||||
|
r.Header.Add("Upgrade", "spdy/3.1")
|
||||||
|
|
||||||
|
wantKubeAPIServerRequestHeaders = wantKubeAPIServerRequestHeaders.Clone()
|
||||||
|
if wantKubeAPIServerRequestHeaders == nil {
|
||||||
|
wantKubeAPIServerRequestHeaders = http.Header{}
|
||||||
|
}
|
||||||
|
wantKubeAPIServerRequestHeaders.Add("Connection", "Upgrade")
|
||||||
|
wantKubeAPIServerRequestHeaders.Add("Upgrade", "spdy/3.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBeforeServe := r.Clone(r.Context())
|
||||||
|
impersonatorHTTPHandlerFunc(&serverConfig.Config).ServeHTTP(w, r)
|
||||||
|
|
||||||
|
require.Equal(t, requestBeforeServe, r, "ServeHTTP() mutated the request, and it should not per http.Handler docs")
|
||||||
|
if tt.wantHTTPStatus != 0 {
|
||||||
|
require.Equalf(t, tt.wantHTTPStatus, w.Code, "fyi, response body was %q", w.Body.String())
|
||||||
|
}
|
||||||
|
if tt.wantHTTPBody != "" {
|
||||||
|
require.Equal(t, tt.wantHTTPBody, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantHTTPStatus == http.StatusOK || tt.kubeAPIServerStatusCode != http.StatusOK {
|
||||||
|
require.True(t, testKubeAPIServerWasCalled, "Should have proxied the request to the Kube API server, but didn't")
|
||||||
|
require.Equal(t, wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
||||||
|
} else {
|
||||||
|
require.False(t, testKubeAPIServerWasCalled, "Should not have proxied the request to the Kube API server, but did")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientCert struct {
|
||||||
|
certPEM, keyPEM []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientCert(t *testing.T, ca *certauthority.CA, username string, groups []string) *clientCert {
|
||||||
|
certPEM, keyPEM, err := ca.IssueClientCertPEM(username, groups, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return &clientCert{
|
||||||
|
certPEM: certPEM,
|
||||||
|
keyPEM: keyPEM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireCanBindToPort(t *testing.T, port int) {
|
||||||
|
ln, _, listenErr := genericoptions.CreateListener("", "0.0.0.0:"+strconv.Itoa(port), net.ListenConfig{})
|
||||||
|
require.NoError(t, listenErr)
|
||||||
|
require.NoError(t, ln.Close())
|
||||||
|
}
|
130
internal/concierge/scheme/scheme.go
Normal file
130
internal/concierge/scheme/scheme.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package scheme contains code to construct a proper runtime.Scheme for the Concierge aggregated
|
||||||
|
// API.
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
|
||||||
|
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
||||||
|
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||||
|
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
|
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||||
|
"go.pinniped.dev/internal/groupsuffix"
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a runtime.Scheme for use by the Concierge aggregated API running with the provided
|
||||||
|
// apiGroupSuffix.
|
||||||
|
func New(apiGroupSuffix string) (_ *runtime.Scheme, login, identity schema.GroupVersion) {
|
||||||
|
// 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 suffix
|
||||||
|
if apiGroupSuffix == groupsuffix.PinnipedDefaultSuffix {
|
||||||
|
schemeBuilder := runtime.NewSchemeBuilder(
|
||||||
|
loginv1alpha1.AddToScheme,
|
||||||
|
loginapi.AddToScheme,
|
||||||
|
identityv1alpha1.AddToScheme,
|
||||||
|
identityapi.AddToScheme,
|
||||||
|
)
|
||||||
|
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
||||||
|
return scheme, loginv1alpha1.SchemeGroupVersion, identityv1alpha1.SchemeGroupVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
loginConciergeGroupData, identityConciergeGroupData := groupsuffix.ConciergeAggregatedGroups(apiGroupSuffix)
|
||||||
|
|
||||||
|
addToSchemeAtNewGroup(scheme, loginv1alpha1.GroupName, loginConciergeGroupData.Group, loginv1alpha1.AddToScheme, loginapi.AddToScheme)
|
||||||
|
addToSchemeAtNewGroup(scheme, identityv1alpha1.GroupName, identityConciergeGroupData.Group, identityv1alpha1.AddToScheme, identityapi.AddToScheme)
|
||||||
|
|
||||||
|
// manually register conversions and defaulting into the correct scheme since we cannot directly call AddToScheme
|
||||||
|
schemeBuilder := runtime.NewSchemeBuilder(
|
||||||
|
loginv1alpha1.RegisterConversions,
|
||||||
|
loginv1alpha1.RegisterDefaults,
|
||||||
|
identityv1alpha1.RegisterConversions,
|
||||||
|
identityv1alpha1.RegisterDefaults,
|
||||||
|
)
|
||||||
|
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
||||||
|
|
||||||
|
// we do not want to return errors from the scheme and instead would prefer to defer
|
||||||
|
// to the REST storage layer for consistency. The simplest way to do this is to force
|
||||||
|
// a cache miss from the authenticator cache. Kube API groups are validated via the
|
||||||
|
// IsDNS1123Subdomain func thus we can easily create a group that is guaranteed never
|
||||||
|
// to be in the authenticator cache. Add a timestamp just to be extra sure.
|
||||||
|
const authenticatorCacheMissPrefix = "_INVALID_API_GROUP_"
|
||||||
|
authenticatorCacheMiss := authenticatorCacheMissPrefix + time.Now().UTC().String()
|
||||||
|
|
||||||
|
// we do not have any defaulting functions for *loginv1alpha1.TokenCredentialRequest
|
||||||
|
// today, but we may have some in the future. Calling AddTypeDefaultingFunc overwrites
|
||||||
|
// any previously registered defaulting function. Thus to make sure that we catch
|
||||||
|
// a situation where we add a defaulting func, we attempt to call it here with a nil
|
||||||
|
// *loginv1alpha1.TokenCredentialRequest. This will do nothing when there is no
|
||||||
|
// defaulting func registered, but it will almost certainly panic if one is added.
|
||||||
|
scheme.Default((*loginv1alpha1.TokenCredentialRequest)(nil))
|
||||||
|
|
||||||
|
// on incoming requests, restore the authenticator API group to the standard group
|
||||||
|
// note that we are responsible for duplicating this logic for every external API version
|
||||||
|
scheme.AddTypeDefaultingFunc(&loginv1alpha1.TokenCredentialRequest{}, func(obj interface{}) {
|
||||||
|
credentialRequest := obj.(*loginv1alpha1.TokenCredentialRequest)
|
||||||
|
|
||||||
|
if credentialRequest.Spec.Authenticator.APIGroup == nil {
|
||||||
|
// force a cache miss because this is an invalid request
|
||||||
|
plog.Debug("invalid token credential request, nil group", "authenticator", credentialRequest.Spec.Authenticator)
|
||||||
|
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredGroup, ok := groupsuffix.Unreplace(*credentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix)
|
||||||
|
if !ok {
|
||||||
|
// force a cache miss because this is an invalid request
|
||||||
|
plog.Debug("invalid token credential request, wrong group", "authenticator", credentialRequest.Spec.Authenticator)
|
||||||
|
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialRequest.Spec.Authenticator.APIGroup = &restoredGroup
|
||||||
|
})
|
||||||
|
|
||||||
|
return scheme, schema.GroupVersion(loginConciergeGroupData), schema.GroupVersion(identityConciergeGroupData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addToSchemeAtNewGroup(scheme *runtime.Scheme, oldGroup, newGroup string, funcs ...func(*runtime.Scheme) error) {
|
||||||
|
// we need a temporary place to register our types to avoid double registering them
|
||||||
|
tmpScheme := runtime.NewScheme()
|
||||||
|
schemeBuilder := runtime.NewSchemeBuilder(funcs...)
|
||||||
|
utilruntime.Must(schemeBuilder.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 != oldGroup {
|
||||||
|
panic(fmt.Errorf("tmp scheme has type not in the old aggregated API group %s: %s", oldGroup, gvk)) // programmer error
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := tmpScheme.New(gvk)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // programmer error, scheme internal code is broken
|
||||||
|
}
|
||||||
|
newGVK := schema.GroupVersionKind{
|
||||||
|
Group: newGroup,
|
||||||
|
Version: gvk.Version,
|
||||||
|
Kind: gvk.Kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
// register the existing type but with the new group in the correct scheme
|
||||||
|
scheme.AddKnownTypeWithName(newGVK, obj)
|
||||||
|
}
|
||||||
|
}
|
240
internal/concierge/scheme/scheme_test.go
Normal file
240
internal/concierge/scheme/scheme_test.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"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"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
||||||
|
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||||
|
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
|
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
// the standard group
|
||||||
|
regularLoginGV := schema.GroupVersion{
|
||||||
|
Group: "login.concierge.pinniped.dev",
|
||||||
|
Version: "v1alpha1",
|
||||||
|
}
|
||||||
|
regularLoginGVInternal := schema.GroupVersion{
|
||||||
|
Group: "login.concierge.pinniped.dev",
|
||||||
|
Version: runtime.APIVersionInternal,
|
||||||
|
}
|
||||||
|
regularIdentityGV := schema.GroupVersion{
|
||||||
|
Group: "identity.concierge.pinniped.dev",
|
||||||
|
Version: "v1alpha1",
|
||||||
|
}
|
||||||
|
regularIdentityGVInternal := schema.GroupVersion{
|
||||||
|
Group: "identity.concierge.pinniped.dev",
|
||||||
|
Version: runtime.APIVersionInternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// the canonical other group
|
||||||
|
otherLoginGV := schema.GroupVersion{
|
||||||
|
Group: "login.concierge.walrus.tld",
|
||||||
|
Version: "v1alpha1",
|
||||||
|
}
|
||||||
|
otherLoginGVInternal := schema.GroupVersion{
|
||||||
|
Group: "login.concierge.walrus.tld",
|
||||||
|
Version: runtime.APIVersionInternal,
|
||||||
|
}
|
||||||
|
otherIdentityGV := schema.GroupVersion{
|
||||||
|
Group: "identity.concierge.walrus.tld",
|
||||||
|
Version: "v1alpha1",
|
||||||
|
}
|
||||||
|
otherIdentityGVInternal := schema.GroupVersion{
|
||||||
|
Group: "identity.concierge.walrus.tld",
|
||||||
|
Version: runtime.APIVersionInternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// kube's core internal
|
||||||
|
internalGV := schema.GroupVersion{
|
||||||
|
Group: "",
|
||||||
|
Version: runtime.APIVersionInternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
apiGroupSuffix string
|
||||||
|
want map[schema.GroupVersionKind]reflect.Type
|
||||||
|
wantLoginGroupVersion schema.GroupVersion
|
||||||
|
wantIdentityGroupVersion schema.GroupVersion
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "regular api group",
|
||||||
|
apiGroupSuffix: "pinniped.dev",
|
||||||
|
want: map[schema.GroupVersionKind]reflect.Type{
|
||||||
|
// all the types that are in the aggregated API group
|
||||||
|
|
||||||
|
regularLoginGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
||||||
|
|
||||||
|
regularLoginGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
||||||
|
regularLoginGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
||||||
|
|
||||||
|
regularIdentityGV.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequest{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequestList{}).Elem(),
|
||||||
|
|
||||||
|
regularIdentityGVInternal.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityapi.WhoAmIRequest{}).Elem(),
|
||||||
|
regularIdentityGVInternal.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityapi.WhoAmIRequestList{}).Elem(),
|
||||||
|
|
||||||
|
regularLoginGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||||
|
regularLoginGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||||
|
|
||||||
|
regularIdentityGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||||
|
regularIdentityGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||||
|
|
||||||
|
regularLoginGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
|
||||||
|
|
||||||
|
regularIdentityGVInternal.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(),
|
||||||
|
},
|
||||||
|
wantLoginGroupVersion: regularLoginGV,
|
||||||
|
wantIdentityGroupVersion: regularIdentityGV,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other api group",
|
||||||
|
apiGroupSuffix: "walrus.tld",
|
||||||
|
want: map[schema.GroupVersionKind]reflect.Type{
|
||||||
|
// all the types that are in the aggregated API group
|
||||||
|
|
||||||
|
otherLoginGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
||||||
|
|
||||||
|
otherLoginGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
||||||
|
otherLoginGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
||||||
|
|
||||||
|
otherIdentityGV.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequest{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequestList{}).Elem(),
|
||||||
|
|
||||||
|
otherIdentityGVInternal.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityapi.WhoAmIRequest{}).Elem(),
|
||||||
|
otherIdentityGVInternal.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityapi.WhoAmIRequestList{}).Elem(),
|
||||||
|
|
||||||
|
otherLoginGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||||
|
otherLoginGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||||
|
|
||||||
|
otherIdentityGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||||
|
otherIdentityGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||||
|
|
||||||
|
otherLoginGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
|
||||||
|
|
||||||
|
otherIdentityGVInternal.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(),
|
||||||
|
},
|
||||||
|
wantLoginGroupVersion: otherLoginGV,
|
||||||
|
wantIdentityGroupVersion: otherIdentityGV,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
scheme, loginGV, identityGV := New(tt.apiGroupSuffix)
|
||||||
|
require.Equal(t, tt.want, scheme.AllKnownTypes())
|
||||||
|
require.Equal(t, tt.wantLoginGroupVersion, loginGV)
|
||||||
|
require.Equal(t, tt.wantIdentityGroupVersion, identityGV)
|
||||||
|
|
||||||
|
// make a credential request like a client would send
|
||||||
|
authenticationConciergeAPIGroup := "authentication.concierge." + tt.apiGroupSuffix
|
||||||
|
credentialRequest := &loginv1alpha1.TokenCredentialRequest{
|
||||||
|
Spec: loginv1alpha1.TokenCredentialRequestSpec{
|
||||||
|
Authenticator: corev1.TypedLocalObjectReference{
|
||||||
|
APIGroup: &authenticationConciergeAPIGroup,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// run defaulting on it
|
||||||
|
scheme.Default(credentialRequest)
|
||||||
|
|
||||||
|
// make sure the group is restored if needed
|
||||||
|
require.Equal(t, "authentication.concierge.pinniped.dev", *credentialRequest.Spec.Authenticator.APIGroup)
|
||||||
|
|
||||||
|
// make a credential request in the standard group
|
||||||
|
defaultAuthenticationConciergeAPIGroup := "authentication.concierge.pinniped.dev"
|
||||||
|
defaultCredentialRequest := &loginv1alpha1.TokenCredentialRequest{
|
||||||
|
Spec: loginv1alpha1.TokenCredentialRequestSpec{
|
||||||
|
Authenticator: corev1.TypedLocalObjectReference{
|
||||||
|
APIGroup: &defaultAuthenticationConciergeAPIGroup,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// run defaulting on it
|
||||||
|
scheme.Default(defaultCredentialRequest)
|
||||||
|
|
||||||
|
if tt.apiGroupSuffix == "pinniped.dev" { // when using the standard group, this should just work
|
||||||
|
require.Equal(t, "authentication.concierge.pinniped.dev", *defaultCredentialRequest.Spec.Authenticator.APIGroup)
|
||||||
|
} else { // when using any other group, this should always be a cache miss
|
||||||
|
require.True(t, strings.HasPrefix(*defaultCredentialRequest.Spec.Authenticator.APIGroup, "_INVALID_API_GROUP_2"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -11,27 +11,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||||
|
|
||||||
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
|
||||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
|
||||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
|
||||||
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
|
||||||
"go.pinniped.dev/internal/certauthority/dynamiccertauthority"
|
"go.pinniped.dev/internal/certauthority/dynamiccertauthority"
|
||||||
"go.pinniped.dev/internal/concierge/apiserver"
|
"go.pinniped.dev/internal/concierge/apiserver"
|
||||||
|
conciergescheme "go.pinniped.dev/internal/concierge/scheme"
|
||||||
"go.pinniped.dev/internal/config/concierge"
|
"go.pinniped.dev/internal/config/concierge"
|
||||||
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
||||||
"go.pinniped.dev/internal/controllermanager"
|
"go.pinniped.dev/internal/controllermanager"
|
||||||
"go.pinniped.dev/internal/downward"
|
"go.pinniped.dev/internal/downward"
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
"go.pinniped.dev/internal/groupsuffix"
|
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
|
"go.pinniped.dev/internal/issuer"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
"go.pinniped.dev/internal/registry/credentialrequest"
|
"go.pinniped.dev/internal/registry/credentialrequest"
|
||||||
)
|
)
|
||||||
@ -119,40 +114,57 @@ func (a *App) runServer(ctx context.Context) error {
|
|||||||
// is stored in a k8s Secret. Therefore it also effectively acting as
|
// is stored in a k8s Secret. Therefore it also effectively acting as
|
||||||
// an in-memory cache of what is stored in the k8s Secret, helping to
|
// an in-memory cache of what is stored in the k8s Secret, helping to
|
||||||
// keep incoming requests fast.
|
// keep incoming requests fast.
|
||||||
dynamicServingCertProvider := dynamiccert.New()
|
dynamicServingCertProvider := dynamiccert.NewServingCert("concierge-serving-cert")
|
||||||
|
|
||||||
// This cert provider will be used to provide a signing key to the
|
// This cert provider will be used to provide the Kube signing key to the
|
||||||
// cert issuer used to issue certs to Pinniped clients wishing to login.
|
// cert issuer used to issue certs to Pinniped clients wishing to login.
|
||||||
dynamicSigningCertProvider := dynamiccert.New()
|
dynamicSigningCertProvider := dynamiccert.NewCA("concierge-kube-signing-cert")
|
||||||
|
|
||||||
|
// This cert provider will be used to provide the impersonation proxy signing key to the
|
||||||
|
// cert issuer used to issue certs to Pinniped clients wishing to login.
|
||||||
|
impersonationProxySigningCertProvider := dynamiccert.NewCA("impersonation-proxy-signing-cert")
|
||||||
|
|
||||||
|
// Get the "real" name of the login concierge API group (i.e., the API group name with the
|
||||||
|
// injected suffix).
|
||||||
|
scheme, loginGV, identityGV := conciergescheme.New(*cfg.APIGroupSuffix)
|
||||||
|
|
||||||
// Prepare to start the controllers, but defer actually starting them until the
|
// Prepare to start the controllers, but defer actually starting them until the
|
||||||
// post start hook of the aggregated API server.
|
// post start hook of the aggregated API server.
|
||||||
startControllersFunc, err := controllermanager.PrepareControllers(
|
startControllersFunc, err := controllermanager.PrepareControllers(
|
||||||
&controllermanager.Config{
|
&controllermanager.Config{
|
||||||
ServerInstallationInfo: podInfo,
|
ServerInstallationInfo: podInfo,
|
||||||
APIGroupSuffix: *cfg.APIGroupSuffix,
|
APIGroupSuffix: *cfg.APIGroupSuffix,
|
||||||
NamesConfig: &cfg.NamesConfig,
|
NamesConfig: &cfg.NamesConfig,
|
||||||
Labels: cfg.Labels,
|
Labels: cfg.Labels,
|
||||||
KubeCertAgentConfig: &cfg.KubeCertAgentConfig,
|
KubeCertAgentConfig: &cfg.KubeCertAgentConfig,
|
||||||
DiscoveryURLOverride: cfg.DiscoveryInfo.URL,
|
DiscoveryURLOverride: cfg.DiscoveryInfo.URL,
|
||||||
DynamicServingCertProvider: dynamicServingCertProvider,
|
DynamicServingCertProvider: dynamicServingCertProvider,
|
||||||
DynamicSigningCertProvider: dynamicSigningCertProvider,
|
DynamicSigningCertProvider: dynamicSigningCertProvider,
|
||||||
ServingCertDuration: time.Duration(*cfg.APIConfig.ServingCertificateConfig.DurationSeconds) * time.Second,
|
ImpersonationSigningCertProvider: impersonationProxySigningCertProvider,
|
||||||
ServingCertRenewBefore: time.Duration(*cfg.APIConfig.ServingCertificateConfig.RenewBeforeSeconds) * time.Second,
|
ServingCertDuration: time.Duration(*cfg.APIConfig.ServingCertificateConfig.DurationSeconds) * time.Second,
|
||||||
AuthenticatorCache: authenticators,
|
ServingCertRenewBefore: time.Duration(*cfg.APIConfig.ServingCertificateConfig.RenewBeforeSeconds) * time.Second,
|
||||||
|
AuthenticatorCache: authenticators,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not prepare controllers: %w", err)
|
return fmt.Errorf("could not prepare controllers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
certIssuer := issuer.ClientCertIssuers{
|
||||||
|
dynamiccertauthority.New(dynamicSigningCertProvider), // attempt to use the real Kube CA if possible
|
||||||
|
dynamiccertauthority.New(impersonationProxySigningCertProvider), // fallback to our internal CA if we need to
|
||||||
|
}
|
||||||
|
|
||||||
// Get the aggregated API server config.
|
// Get the aggregated API server config.
|
||||||
aggregatedAPIServerConfig, err := getAggregatedAPIServerConfig(
|
aggregatedAPIServerConfig, err := getAggregatedAPIServerConfig(
|
||||||
dynamicServingCertProvider,
|
dynamicServingCertProvider,
|
||||||
authenticators,
|
authenticators,
|
||||||
dynamiccertauthority.New(dynamicSigningCertProvider),
|
certIssuer,
|
||||||
startControllersFunc,
|
startControllersFunc,
|
||||||
*cfg.APIGroupSuffix,
|
*cfg.APIGroupSuffix,
|
||||||
|
scheme,
|
||||||
|
loginGV,
|
||||||
|
identityGV,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not configure aggregated API server: %w", err)
|
return fmt.Errorf("could not configure aggregated API server: %w", err)
|
||||||
@ -170,13 +182,14 @@ func (a *App) runServer(ctx context.Context) error {
|
|||||||
|
|
||||||
// Create a configuration for the aggregated API server.
|
// Create a configuration for the aggregated API server.
|
||||||
func getAggregatedAPIServerConfig(
|
func getAggregatedAPIServerConfig(
|
||||||
dynamicCertProvider dynamiccert.Provider,
|
dynamicCertProvider dynamiccert.Private,
|
||||||
authenticator credentialrequest.TokenCredentialRequestAuthenticator,
|
authenticator credentialrequest.TokenCredentialRequestAuthenticator,
|
||||||
issuer credentialrequest.CertIssuer,
|
issuer issuer.ClientCertIssuer,
|
||||||
startControllersPostStartHook func(context.Context),
|
startControllersPostStartHook func(context.Context),
|
||||||
apiGroupSuffix string,
|
apiGroupSuffix string,
|
||||||
|
scheme *runtime.Scheme,
|
||||||
|
loginConciergeGroupVersion, identityConciergeGroupVersion schema.GroupVersion,
|
||||||
) (*apiserver.Config, error) {
|
) (*apiserver.Config, error) {
|
||||||
scheme, loginConciergeGroupVersion, identityConciergeGroupVersion := getAggregatedAPIServerScheme(apiGroupSuffix)
|
|
||||||
codecs := serializer.NewCodecFactory(scheme)
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
|
||||||
// this is unused for now but it is a safe value that we could use in the future
|
// this is unused for now but it is a safe value that we could use in the future
|
||||||
@ -218,108 +231,3 @@ func getAggregatedAPIServerConfig(
|
|||||||
}
|
}
|
||||||
return apiServerConfig, nil
|
return apiServerConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAggregatedAPIServerScheme(apiGroupSuffix string) (_ *runtime.Scheme, login, identity schema.GroupVersion) {
|
|
||||||
// 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 suffix
|
|
||||||
if apiGroupSuffix == groupsuffix.PinnipedDefaultSuffix {
|
|
||||||
schemeBuilder := runtime.NewSchemeBuilder(
|
|
||||||
loginv1alpha1.AddToScheme,
|
|
||||||
loginapi.AddToScheme,
|
|
||||||
identityv1alpha1.AddToScheme,
|
|
||||||
identityapi.AddToScheme,
|
|
||||||
)
|
|
||||||
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
|
||||||
return scheme, loginv1alpha1.SchemeGroupVersion, identityv1alpha1.SchemeGroupVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
loginConciergeGroupData, identityConciergeGroupData := groupsuffix.ConciergeAggregatedGroups(apiGroupSuffix)
|
|
||||||
|
|
||||||
addToSchemeAtNewGroup(scheme, loginv1alpha1.GroupName, loginConciergeGroupData.Group, loginv1alpha1.AddToScheme, loginapi.AddToScheme)
|
|
||||||
addToSchemeAtNewGroup(scheme, identityv1alpha1.GroupName, identityConciergeGroupData.Group, identityv1alpha1.AddToScheme, identityapi.AddToScheme)
|
|
||||||
|
|
||||||
// manually register conversions and defaulting into the correct scheme since we cannot directly call AddToScheme
|
|
||||||
schemeBuilder := runtime.NewSchemeBuilder(
|
|
||||||
loginv1alpha1.RegisterConversions,
|
|
||||||
loginv1alpha1.RegisterDefaults,
|
|
||||||
identityv1alpha1.RegisterConversions,
|
|
||||||
identityv1alpha1.RegisterDefaults,
|
|
||||||
)
|
|
||||||
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
|
||||||
|
|
||||||
// we do not want to return errors from the scheme and instead would prefer to defer
|
|
||||||
// to the REST storage layer for consistency. The simplest way to do this is to force
|
|
||||||
// a cache miss from the authenticator cache. Kube API groups are validated via the
|
|
||||||
// IsDNS1123Subdomain func thus we can easily create a group that is guaranteed never
|
|
||||||
// to be in the authenticator cache. Add a timestamp just to be extra sure.
|
|
||||||
const authenticatorCacheMissPrefix = "_INVALID_API_GROUP_"
|
|
||||||
authenticatorCacheMiss := authenticatorCacheMissPrefix + time.Now().UTC().String()
|
|
||||||
|
|
||||||
// we do not have any defaulting functions for *loginv1alpha1.TokenCredentialRequest
|
|
||||||
// today, but we may have some in the future. Calling AddTypeDefaultingFunc overwrites
|
|
||||||
// any previously registered defaulting function. Thus to make sure that we catch
|
|
||||||
// a situation where we add a defaulting func, we attempt to call it here with a nil
|
|
||||||
// *loginv1alpha1.TokenCredentialRequest. This will do nothing when there is no
|
|
||||||
// defaulting func registered, but it will almost certainly panic if one is added.
|
|
||||||
scheme.Default((*loginv1alpha1.TokenCredentialRequest)(nil))
|
|
||||||
|
|
||||||
// on incoming requests, restore the authenticator API group to the standard group
|
|
||||||
// note that we are responsible for duplicating this logic for every external API version
|
|
||||||
scheme.AddTypeDefaultingFunc(&loginv1alpha1.TokenCredentialRequest{}, func(obj interface{}) {
|
|
||||||
credentialRequest := obj.(*loginv1alpha1.TokenCredentialRequest)
|
|
||||||
|
|
||||||
if credentialRequest.Spec.Authenticator.APIGroup == nil {
|
|
||||||
// force a cache miss because this is an invalid request
|
|
||||||
plog.Debug("invalid token credential request, nil group", "authenticator", credentialRequest.Spec.Authenticator)
|
|
||||||
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
restoredGroup, ok := groupsuffix.Unreplace(*credentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix)
|
|
||||||
if !ok {
|
|
||||||
// force a cache miss because this is an invalid request
|
|
||||||
plog.Debug("invalid token credential request, wrong group", "authenticator", credentialRequest.Spec.Authenticator)
|
|
||||||
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
credentialRequest.Spec.Authenticator.APIGroup = &restoredGroup
|
|
||||||
})
|
|
||||||
|
|
||||||
return scheme, schema.GroupVersion(loginConciergeGroupData), schema.GroupVersion(identityConciergeGroupData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addToSchemeAtNewGroup(scheme *runtime.Scheme, oldGroup, newGroup string, funcs ...func(*runtime.Scheme) error) {
|
|
||||||
// we need a temporary place to register our types to avoid double registering them
|
|
||||||
tmpScheme := runtime.NewScheme()
|
|
||||||
schemeBuilder := runtime.NewSchemeBuilder(funcs...)
|
|
||||||
utilruntime.Must(schemeBuilder.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 != oldGroup {
|
|
||||||
panic(fmt.Errorf("tmp scheme has type not in the old aggregated API group %s: %s", oldGroup, gvk)) // programmer error
|
|
||||||
}
|
|
||||||
|
|
||||||
obj, err := tmpScheme.New(gvk)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // programmer error, scheme internal code is broken
|
|
||||||
}
|
|
||||||
newGVK := schema.GroupVersionKind{
|
|
||||||
Group: newGroup,
|
|
||||||
Version: gvk.Version,
|
|
||||||
Kind: gvk.Kind,
|
|
||||||
}
|
|
||||||
|
|
||||||
// register the existing type but with the new group in the correct scheme
|
|
||||||
scheme.AddKnownTypeWithName(newGVK, obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -6,22 +6,12 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
|
|
||||||
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
|
||||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
|
||||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
|
||||||
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const knownGoodUsage = `
|
const knownGoodUsage = `
|
||||||
@ -98,222 +88,3 @@ func TestCommand(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
|
||||||
// the standard group
|
|
||||||
regularLoginGV := schema.GroupVersion{
|
|
||||||
Group: "login.concierge.pinniped.dev",
|
|
||||||
Version: "v1alpha1",
|
|
||||||
}
|
|
||||||
regularLoginGVInternal := schema.GroupVersion{
|
|
||||||
Group: "login.concierge.pinniped.dev",
|
|
||||||
Version: runtime.APIVersionInternal,
|
|
||||||
}
|
|
||||||
regularIdentityGV := schema.GroupVersion{
|
|
||||||
Group: "identity.concierge.pinniped.dev",
|
|
||||||
Version: "v1alpha1",
|
|
||||||
}
|
|
||||||
regularIdentityGVInternal := schema.GroupVersion{
|
|
||||||
Group: "identity.concierge.pinniped.dev",
|
|
||||||
Version: runtime.APIVersionInternal,
|
|
||||||
}
|
|
||||||
|
|
||||||
// the canonical other group
|
|
||||||
otherLoginGV := schema.GroupVersion{
|
|
||||||
Group: "login.concierge.walrus.tld",
|
|
||||||
Version: "v1alpha1",
|
|
||||||
}
|
|
||||||
otherLoginGVInternal := schema.GroupVersion{
|
|
||||||
Group: "login.concierge.walrus.tld",
|
|
||||||
Version: runtime.APIVersionInternal,
|
|
||||||
}
|
|
||||||
otherIdentityGV := schema.GroupVersion{
|
|
||||||
Group: "identity.concierge.walrus.tld",
|
|
||||||
Version: "v1alpha1",
|
|
||||||
}
|
|
||||||
otherIdentityGVInternal := schema.GroupVersion{
|
|
||||||
Group: "identity.concierge.walrus.tld",
|
|
||||||
Version: runtime.APIVersionInternal,
|
|
||||||
}
|
|
||||||
|
|
||||||
// kube's core internal
|
|
||||||
internalGV := schema.GroupVersion{
|
|
||||||
Group: "",
|
|
||||||
Version: runtime.APIVersionInternal,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
apiGroupSuffix string
|
|
||||||
want map[schema.GroupVersionKind]reflect.Type
|
|
||||||
wantLoginGroupVersion schema.GroupVersion
|
|
||||||
wantIdentityGroupVersion schema.GroupVersion
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "regular api group",
|
|
||||||
apiGroupSuffix: "pinniped.dev",
|
|
||||||
want: map[schema.GroupVersionKind]reflect.Type{
|
|
||||||
// all the types that are in the aggregated API group
|
|
||||||
|
|
||||||
regularLoginGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
|
||||||
|
|
||||||
regularLoginGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
|
||||||
regularLoginGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
|
||||||
|
|
||||||
regularIdentityGV.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequest{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequestList{}).Elem(),
|
|
||||||
|
|
||||||
regularIdentityGVInternal.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityapi.WhoAmIRequest{}).Elem(),
|
|
||||||
regularIdentityGVInternal.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityapi.WhoAmIRequestList{}).Elem(),
|
|
||||||
|
|
||||||
regularLoginGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
|
||||||
regularLoginGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
|
||||||
|
|
||||||
regularIdentityGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
|
||||||
regularIdentityGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
|
||||||
|
|
||||||
regularLoginGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
|
|
||||||
|
|
||||||
regularIdentityGVInternal.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(),
|
|
||||||
},
|
|
||||||
wantLoginGroupVersion: regularLoginGV,
|
|
||||||
wantIdentityGroupVersion: regularIdentityGV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "other api group",
|
|
||||||
apiGroupSuffix: "walrus.tld",
|
|
||||||
want: map[schema.GroupVersionKind]reflect.Type{
|
|
||||||
// all the types that are in the aggregated API group
|
|
||||||
|
|
||||||
otherLoginGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
|
||||||
|
|
||||||
otherLoginGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
|
||||||
otherLoginGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
|
||||||
|
|
||||||
otherIdentityGV.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequest{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequestList{}).Elem(),
|
|
||||||
|
|
||||||
otherIdentityGVInternal.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityapi.WhoAmIRequest{}).Elem(),
|
|
||||||
otherIdentityGVInternal.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityapi.WhoAmIRequestList{}).Elem(),
|
|
||||||
|
|
||||||
otherLoginGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
|
||||||
otherLoginGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
|
||||||
|
|
||||||
otherIdentityGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
|
||||||
otherIdentityGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
|
||||||
|
|
||||||
otherLoginGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).Elem(),
|
|
||||||
|
|
||||||
otherIdentityGVInternal.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(),
|
|
||||||
},
|
|
||||||
wantLoginGroupVersion: otherLoginGV,
|
|
||||||
wantIdentityGroupVersion: otherIdentityGV,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
scheme, loginGV, identityGV := getAggregatedAPIServerScheme(tt.apiGroupSuffix)
|
|
||||||
require.Equal(t, tt.want, scheme.AllKnownTypes())
|
|
||||||
require.Equal(t, tt.wantLoginGroupVersion, loginGV)
|
|
||||||
require.Equal(t, tt.wantIdentityGroupVersion, identityGV)
|
|
||||||
|
|
||||||
// make a credential request like a client would send
|
|
||||||
authenticationConciergeAPIGroup := "authentication.concierge." + tt.apiGroupSuffix
|
|
||||||
credentialRequest := &loginv1alpha1.TokenCredentialRequest{
|
|
||||||
Spec: loginv1alpha1.TokenCredentialRequestSpec{
|
|
||||||
Authenticator: corev1.TypedLocalObjectReference{
|
|
||||||
APIGroup: &authenticationConciergeAPIGroup,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// run defaulting on it
|
|
||||||
scheme.Default(credentialRequest)
|
|
||||||
|
|
||||||
// make sure the group is restored if needed
|
|
||||||
require.Equal(t, "authentication.concierge.pinniped.dev", *credentialRequest.Spec.Authenticator.APIGroup)
|
|
||||||
|
|
||||||
// make a credential request in the standard group
|
|
||||||
defaultAuthenticationConciergeAPIGroup := "authentication.concierge.pinniped.dev"
|
|
||||||
defaultCredentialRequest := &loginv1alpha1.TokenCredentialRequest{
|
|
||||||
Spec: loginv1alpha1.TokenCredentialRequestSpec{
|
|
||||||
Authenticator: corev1.TypedLocalObjectReference{
|
|
||||||
APIGroup: &defaultAuthenticationConciergeAPIGroup,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// run defaulting on it
|
|
||||||
scheme.Default(defaultCredentialRequest)
|
|
||||||
|
|
||||||
if tt.apiGroupSuffix == "pinniped.dev" { // when using the standard group, this should just work
|
|
||||||
require.Equal(t, "authentication.concierge.pinniped.dev", *defaultCredentialRequest.Spec.Authenticator.APIGroup)
|
|
||||||
} else { // when using any other group, this should always be a cache miss
|
|
||||||
require.True(t, strings.HasPrefix(*defaultCredentialRequest.Spec.Authenticator.APIGroup, "_INVALID_API_GROUP_2"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -96,17 +96,31 @@ func maybeSetKubeCertAgentDefaults(cfg *KubeCertAgentSpec) {
|
|||||||
func validateNames(names *NamesConfigSpec) error {
|
func validateNames(names *NamesConfigSpec) error {
|
||||||
missingNames := []string{}
|
missingNames := []string{}
|
||||||
if names == nil {
|
if names == nil {
|
||||||
missingNames = append(missingNames, "servingCertificateSecret", "credentialIssuer", "apiService")
|
names = &NamesConfigSpec{}
|
||||||
} else {
|
}
|
||||||
if names.ServingCertificateSecret == "" {
|
if names.ServingCertificateSecret == "" {
|
||||||
missingNames = append(missingNames, "servingCertificateSecret")
|
missingNames = append(missingNames, "servingCertificateSecret")
|
||||||
}
|
}
|
||||||
if names.CredentialIssuer == "" {
|
if names.CredentialIssuer == "" {
|
||||||
missingNames = append(missingNames, "credentialIssuer")
|
missingNames = append(missingNames, "credentialIssuer")
|
||||||
}
|
}
|
||||||
if names.APIService == "" {
|
if names.APIService == "" {
|
||||||
missingNames = append(missingNames, "apiService")
|
missingNames = append(missingNames, "apiService")
|
||||||
}
|
}
|
||||||
|
if names.ImpersonationConfigMap == "" {
|
||||||
|
missingNames = append(missingNames, "impersonationConfigMap")
|
||||||
|
}
|
||||||
|
if names.ImpersonationLoadBalancerService == "" {
|
||||||
|
missingNames = append(missingNames, "impersonationLoadBalancerService")
|
||||||
|
}
|
||||||
|
if names.ImpersonationTLSCertificateSecret == "" {
|
||||||
|
missingNames = append(missingNames, "impersonationTLSCertificateSecret")
|
||||||
|
}
|
||||||
|
if names.ImpersonationCACertificateSecret == "" {
|
||||||
|
missingNames = append(missingNames, "impersonationCACertificateSecret")
|
||||||
|
}
|
||||||
|
if names.ImpersonationSignerSecret == "" {
|
||||||
|
missingNames = append(missingNames, "impersonationSignerSecret")
|
||||||
}
|
}
|
||||||
if len(missingNames) > 0 {
|
if len(missingNames) > 0 {
|
||||||
return constable.Error("missing required names: " + strings.Join(missingNames, ", "))
|
return constable.Error("missing required names: " + strings.Join(missingNames, ", "))
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFromPath(t *testing.T) {
|
func TestFromPath(t *testing.T) {
|
||||||
@ -21,7 +22,7 @@ func TestFromPath(t *testing.T) {
|
|||||||
wantError string
|
wantError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Happy",
|
name: "Fully filled out",
|
||||||
yaml: here.Doc(`
|
yaml: here.Doc(`
|
||||||
---
|
---
|
||||||
discovery:
|
discovery:
|
||||||
@ -36,13 +37,20 @@ func TestFromPath(t *testing.T) {
|
|||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
kubeCertAgentPrefix: kube-cert-agent-prefix
|
kubeCertAgentPrefix: kube-cert-agent-prefix
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
labels:
|
labels:
|
||||||
myLabelKey1: myLabelValue1
|
myLabelKey1: myLabelValue1
|
||||||
myLabelKey2: myLabelValue2
|
myLabelKey2: myLabelValue2
|
||||||
KubeCertAgent:
|
kubeCertAgent:
|
||||||
namePrefix: kube-cert-agent-name-prefix-
|
namePrefix: kube-cert-agent-name-prefix-
|
||||||
image: kube-cert-agent-image
|
image: kube-cert-agent-image
|
||||||
imagePullSecrets: [kube-cert-agent-image-pull-secret]
|
imagePullSecrets: [kube-cert-agent-image-pull-secret]
|
||||||
|
logLevel: debug
|
||||||
`),
|
`),
|
||||||
wantConfig: &Config{
|
wantConfig: &Config{
|
||||||
DiscoveryInfo: DiscoveryInfoSpec{
|
DiscoveryInfo: DiscoveryInfoSpec{
|
||||||
@ -56,9 +64,14 @@ func TestFromPath(t *testing.T) {
|
|||||||
},
|
},
|
||||||
APIGroupSuffix: stringPtr("some.suffix.com"),
|
APIGroupSuffix: stringPtr("some.suffix.com"),
|
||||||
NamesConfig: NamesConfigSpec{
|
NamesConfig: NamesConfigSpec{
|
||||||
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
|
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
|
||||||
CredentialIssuer: "pinniped-config",
|
CredentialIssuer: "pinniped-config",
|
||||||
APIService: "pinniped-api",
|
APIService: "pinniped-api",
|
||||||
|
ImpersonationConfigMap: "impersonationConfigMap-value",
|
||||||
|
ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value",
|
||||||
|
ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value",
|
||||||
|
ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value",
|
||||||
|
ImpersonationSignerSecret: "impersonationSignerSecret-value",
|
||||||
},
|
},
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"myLabelKey1": "myLabelValue1",
|
"myLabelKey1": "myLabelValue1",
|
||||||
@ -69,6 +82,7 @@ func TestFromPath(t *testing.T) {
|
|||||||
Image: stringPtr("kube-cert-agent-image"),
|
Image: stringPtr("kube-cert-agent-image"),
|
||||||
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
|
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
|
||||||
},
|
},
|
||||||
|
LogLevel: plog.LevelDebug,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -79,6 +93,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantConfig: &Config{
|
wantConfig: &Config{
|
||||||
DiscoveryInfo: DiscoveryInfoSpec{
|
DiscoveryInfo: DiscoveryInfoSpec{
|
||||||
@ -92,9 +111,14 @@ func TestFromPath(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
NamesConfig: NamesConfigSpec{
|
NamesConfig: NamesConfigSpec{
|
||||||
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
|
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
|
||||||
CredentialIssuer: "pinniped-config",
|
CredentialIssuer: "pinniped-config",
|
||||||
APIService: "pinniped-api",
|
APIService: "pinniped-api",
|
||||||
|
ImpersonationConfigMap: "impersonationConfigMap-value",
|
||||||
|
ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value",
|
||||||
|
ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value",
|
||||||
|
ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value",
|
||||||
|
ImpersonationSignerSecret: "impersonationSignerSecret-value",
|
||||||
},
|
},
|
||||||
Labels: map[string]string{},
|
Labels: map[string]string{},
|
||||||
KubeCertAgentConfig: KubeCertAgentSpec{
|
KubeCertAgentConfig: KubeCertAgentSpec{
|
||||||
@ -104,9 +128,12 @@ func TestFromPath(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty",
|
name: "Empty",
|
||||||
yaml: here.Doc(``),
|
yaml: here.Doc(``),
|
||||||
wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, apiService",
|
wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, " +
|
||||||
|
"apiService, impersonationConfigMap, impersonationLoadBalancerService, " +
|
||||||
|
"impersonationTLSCertificateSecret, impersonationCACertificateSecret, " +
|
||||||
|
"impersonationSignerSecret",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Missing apiService name",
|
name: "Missing apiService name",
|
||||||
@ -115,6 +142,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
names:
|
names:
|
||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate names: missing required names: apiService",
|
wantError: "validate names: missing required names: apiService",
|
||||||
},
|
},
|
||||||
@ -125,6 +157,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
names:
|
names:
|
||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate names: missing required names: credentialIssuer",
|
wantError: "validate names: missing required names: credentialIssuer",
|
||||||
},
|
},
|
||||||
@ -135,9 +172,103 @@ func TestFromPath(t *testing.T) {
|
|||||||
names:
|
names:
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate names: missing required names: servingCertificateSecret",
|
wantError: "validate names: missing required names: servingCertificateSecret",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Missing impersonationConfigMap name",
|
||||||
|
yaml: here.Doc(`
|
||||||
|
---
|
||||||
|
names:
|
||||||
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
|
credentialIssuer: pinniped-config
|
||||||
|
apiService: pinniped-api
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
|
`),
|
||||||
|
wantError: "validate names: missing required names: impersonationConfigMap",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing impersonationLoadBalancerService name",
|
||||||
|
yaml: here.Doc(`
|
||||||
|
---
|
||||||
|
names:
|
||||||
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
|
credentialIssuer: pinniped-config
|
||||||
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
|
`),
|
||||||
|
wantError: "validate names: missing required names: impersonationLoadBalancerService",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing impersonationTLSCertificateSecret name",
|
||||||
|
yaml: here.Doc(`
|
||||||
|
---
|
||||||
|
names:
|
||||||
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
|
credentialIssuer: pinniped-config
|
||||||
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
|
`),
|
||||||
|
wantError: "validate names: missing required names: impersonationTLSCertificateSecret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing impersonationCACertificateSecret name",
|
||||||
|
yaml: here.Doc(`
|
||||||
|
---
|
||||||
|
names:
|
||||||
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
|
credentialIssuer: pinniped-config
|
||||||
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
|
`),
|
||||||
|
wantError: "validate names: missing required names: impersonationCACertificateSecret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing impersonationSignerSecret name",
|
||||||
|
yaml: here.Doc(`
|
||||||
|
---
|
||||||
|
names:
|
||||||
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
|
credentialIssuer: pinniped-config
|
||||||
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
`),
|
||||||
|
wantError: "validate names: missing required names: impersonationSignerSecret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing several required names",
|
||||||
|
yaml: here.Doc(`
|
||||||
|
---
|
||||||
|
names:
|
||||||
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
|
credentialIssuer: pinniped-config
|
||||||
|
apiService: pinniped-api
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
|
`),
|
||||||
|
wantError: "validate names: missing required names: impersonationConfigMap, " +
|
||||||
|
"impersonationTLSCertificateSecret, impersonationCACertificateSecret",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "InvalidDurationRenewBefore",
|
name: "InvalidDurationRenewBefore",
|
||||||
yaml: here.Doc(`
|
yaml: here.Doc(`
|
||||||
@ -150,6 +281,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate api: durationSeconds cannot be smaller than renewBeforeSeconds",
|
wantError: "validate api: durationSeconds cannot be smaller than renewBeforeSeconds",
|
||||||
},
|
},
|
||||||
@ -165,6 +301,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate api: renewBefore must be positive",
|
wantError: "validate api: renewBefore must be positive",
|
||||||
},
|
},
|
||||||
@ -180,6 +321,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate api: renewBefore must be positive",
|
wantError: "validate api: renewBefore must be positive",
|
||||||
},
|
},
|
||||||
@ -196,6 +342,11 @@ func TestFromPath(t *testing.T) {
|
|||||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||||
credentialIssuer: pinniped-config
|
credentialIssuer: pinniped-config
|
||||||
apiService: pinniped-api
|
apiService: pinniped-api
|
||||||
|
impersonationConfigMap: impersonationConfigMap-value
|
||||||
|
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||||
|
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||||
|
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||||
|
impersonationSignerSecret: impersonationSignerSecret-value
|
||||||
`),
|
`),
|
||||||
wantError: "validate apiGroupSuffix: 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])?)*')",
|
wantError: "validate apiGroupSuffix: 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])?)*')",
|
||||||
},
|
},
|
||||||
|
@ -33,9 +33,14 @@ type APIConfigSpec struct {
|
|||||||
|
|
||||||
// NamesConfigSpec configures the names of some Kubernetes resources for the Concierge.
|
// NamesConfigSpec configures the names of some Kubernetes resources for the Concierge.
|
||||||
type NamesConfigSpec struct {
|
type NamesConfigSpec struct {
|
||||||
ServingCertificateSecret string `json:"servingCertificateSecret"`
|
ServingCertificateSecret string `json:"servingCertificateSecret"`
|
||||||
CredentialIssuer string `json:"credentialIssuer"`
|
CredentialIssuer string `json:"credentialIssuer"`
|
||||||
APIService string `json:"apiService"`
|
APIService string `json:"apiService"`
|
||||||
|
ImpersonationConfigMap string `json:"impersonationConfigMap"`
|
||||||
|
ImpersonationLoadBalancerService string `json:"impersonationLoadBalancerService"`
|
||||||
|
ImpersonationTLSCertificateSecret string `json:"impersonationTLSCertificateSecret"`
|
||||||
|
ImpersonationCACertificateSecret string `json:"impersonationCACertificateSecret"`
|
||||||
|
ImpersonationSignerSecret string `json:"impersonationSignerSecret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServingCertificateConfigSpec contains the configuration knobs for the API's
|
// ServingCertificateConfigSpec contains the configuration knobs for the API'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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
@ -64,7 +64,7 @@ func (c *apiServiceUpdaterController) Sync(ctx controllerlib.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the APIService to give it the new CA bundle.
|
// Update the APIService to give it the new CA bundle.
|
||||||
if err := UpdateAPIService(ctx.Context, c.aggregatorClient, c.apiServiceName, c.namespace, certSecret.Data[caCertificateSecretKey]); err != nil {
|
if err := UpdateAPIService(ctx.Context, c.aggregatorClient, c.apiServiceName, c.namespace, certSecret.Data[CACertificateSecretKey]); err != nil {
|
||||||
return fmt.Errorf("could not update the API service: %w", err)
|
return fmt.Errorf("could not update the API service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/sclevine/spec/report"
|
"github.com/sclevine/spec/report"
|
||||||
@ -112,8 +111,8 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
|||||||
var aggregatorAPIClient *aggregatorfake.Clientset
|
var aggregatorAPIClient *aggregatorfake.Clientset
|
||||||
var kubeInformerClient *kubernetesfake.Clientset
|
var kubeInformerClient *kubernetesfake.Clientset
|
||||||
var kubeInformers kubeinformers.SharedInformerFactory
|
var kubeInformers kubeinformers.SharedInformerFactory
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
@ -131,7 +130,7 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
@ -140,14 +139,14 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformers.Start(timeoutContext.Done())
|
kubeInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||||
@ -155,7 +154,7 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is not yet a serving cert Secret in the installation namespace or it was deleted", func() {
|
when("there is not yet a serving cert Secret in the installation namespace or it was deleted", func() {
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
@ -30,6 +30,8 @@ type certsExpirerController struct {
|
|||||||
// renewBefore is the amount of time after the cert's issuance where
|
// renewBefore is the amount of time after the cert's issuance where
|
||||||
// this controller will start to try to rotate it.
|
// this controller will start to try to rotate it.
|
||||||
renewBefore time.Duration
|
renewBefore time.Duration
|
||||||
|
|
||||||
|
secretKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCertsExpirerController returns a controllerlib.Controller that will delete a
|
// NewCertsExpirerController returns a controllerlib.Controller that will delete a
|
||||||
@ -42,6 +44,7 @@ func NewCertsExpirerController(
|
|||||||
secretInformer corev1informers.SecretInformer,
|
secretInformer corev1informers.SecretInformer,
|
||||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||||
renewBefore time.Duration,
|
renewBefore time.Duration,
|
||||||
|
secretKey string,
|
||||||
) controllerlib.Controller {
|
) controllerlib.Controller {
|
||||||
return controllerlib.New(
|
return controllerlib.New(
|
||||||
controllerlib.Config{
|
controllerlib.Config{
|
||||||
@ -52,6 +55,7 @@ func NewCertsExpirerController(
|
|||||||
k8sClient: k8sClient,
|
k8sClient: k8sClient,
|
||||||
secretInformer: secretInformer,
|
secretInformer: secretInformer,
|
||||||
renewBefore: renewBefore,
|
renewBefore: renewBefore,
|
||||||
|
secretKey: secretKey,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
withInformer(
|
withInformer(
|
||||||
@ -74,13 +78,9 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
notBefore, notAfter, err := getCertBounds(secret)
|
notBefore, notAfter, err := c.getCertBounds(secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If we can't read the cert, then really all we can do is log something,
|
return fmt.Errorf("failed to get cert bounds for secret %q with key %q: %w", secret.Name, c.secretKey, err)
|
||||||
// since if we returned an error then the controller lib would just call us
|
|
||||||
// again and again, which would probably yield the same results.
|
|
||||||
klog.Warningf("certsExpirerController Sync found that the secret is malformed: %s", err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
certAge := time.Since(notBefore)
|
certAge := time.Since(notBefore)
|
||||||
@ -105,8 +105,8 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
|
|||||||
// certificate in the provided secret, or an error. Not that it expects the
|
// certificate in the provided secret, or an error. Not that it expects the
|
||||||
// provided secret to contain the well-known data keys from this package (see
|
// provided secret to contain the well-known data keys from this package (see
|
||||||
// certs_manager.go).
|
// certs_manager.go).
|
||||||
func getCertBounds(secret *corev1.Secret) (time.Time, time.Time, error) {
|
func (c *certsExpirerController) getCertBounds(secret *corev1.Secret) (time.Time, time.Time, error) {
|
||||||
certPEM := secret.Data[tlsCertificateChainSecretKey]
|
certPEM := secret.Data[c.secretKey]
|
||||||
if certPEM == nil {
|
if certPEM == nil {
|
||||||
return time.Time{}, time.Time{}, constable.Error("failed to find certificate")
|
return time.Time{}, time.Time{}, constable.Error("failed to find certificate")
|
||||||
}
|
}
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
@ -98,7 +98,8 @@ func TestExpirerControllerFilters(t *testing.T) {
|
|||||||
nil, // k8sClient, not needed
|
nil, // k8sClient, not needed
|
||||||
secretsInformer,
|
secretsInformer,
|
||||||
withInformer.WithInformer,
|
withInformer.WithInformer,
|
||||||
0, // renewBefore, not needed
|
0, // renewBefore, not needed
|
||||||
|
"", // not needed
|
||||||
)
|
)
|
||||||
|
|
||||||
unrelated := corev1.Secret{}
|
unrelated := corev1.Secret{}
|
||||||
@ -115,6 +116,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const certsSecretResourceName = "some-resource-name"
|
const certsSecretResourceName = "some-resource-name"
|
||||||
|
const fakeTestKey = "some-awesome-key"
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -132,6 +134,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
name: "secret missing key",
|
name: "secret missing key",
|
||||||
fillSecretData: func(t *testing.T, m map[string][]byte) {},
|
fillSecretData: func(t *testing.T, m map[string][]byte) {},
|
||||||
wantDelete: false,
|
wantDelete: false,
|
||||||
|
wantError: `failed to get cert bounds for secret "some-resource-name" with key "some-awesome-key": failed to find certificate`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lifetime below threshold",
|
name: "lifetime below threshold",
|
||||||
@ -143,8 +146,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// See certs_manager.go for this constant.
|
m[fakeTestKey] = certPEM
|
||||||
m["tlsCertificateChain"] = certPEM
|
|
||||||
},
|
},
|
||||||
wantDelete: false,
|
wantDelete: false,
|
||||||
},
|
},
|
||||||
@ -158,8 +160,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// See certs_manager.go for this constant.
|
m[fakeTestKey] = certPEM
|
||||||
m["tlsCertificateChain"] = certPEM
|
|
||||||
},
|
},
|
||||||
wantDelete: true,
|
wantDelete: true,
|
||||||
},
|
},
|
||||||
@ -173,8 +174,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// See certs_manager.go for this constant.
|
m[fakeTestKey] = certPEM
|
||||||
m["tlsCertificateChain"] = certPEM
|
|
||||||
},
|
},
|
||||||
wantDelete: true,
|
wantDelete: true,
|
||||||
},
|
},
|
||||||
@ -188,8 +188,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// See certs_manager.go for this constant.
|
m[fakeTestKey] = certPEM
|
||||||
m["tlsCertificateChain"] = certPEM
|
|
||||||
},
|
},
|
||||||
configKubeAPIClient: func(c *kubernetesfake.Clientset) {
|
configKubeAPIClient: func(c *kubernetesfake.Clientset) {
|
||||||
c.PrependReactor("delete", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
c.PrependReactor("delete", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||||
@ -204,11 +203,11 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// See certs_manager.go for this constant.
|
m[fakeTestKey], err = x509.MarshalPKCS8PrivateKey(privateKey)
|
||||||
m["tlsCertificateChain"], err = x509.MarshalPKCS8PrivateKey(privateKey)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
wantDelete: false,
|
wantDelete: false,
|
||||||
|
wantError: `failed to get cert bounds for secret "some-resource-name" with key "some-awesome-key": failed to decode certificate PEM`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -216,7 +215,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
||||||
@ -253,6 +252,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
kubeInformers.Core().V1().Secrets(),
|
kubeInformers.Core().V1().Secrets(),
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
test.renewBefore,
|
test.renewBefore,
|
||||||
|
fakeTestKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously().
|
// Must start informers before calling TestRunSynchronously().
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509/pkix"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,9 +20,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
caCertificateSecretKey = "caCertificate"
|
CACertificateSecretKey = "caCertificate"
|
||||||
tlsPrivateKeySecretKey = "tlsPrivateKey"
|
CACertificatePrivateKeySecretKey = "caCertificatePrivateKey"
|
||||||
tlsCertificateChainSecretKey = "tlsCertificateChain"
|
tlsPrivateKeySecretKey = "tlsPrivateKey"
|
||||||
|
TLSCertificateChainSecretKey = "tlsCertificateChain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type certsManagerController struct {
|
type certsManagerController struct {
|
||||||
@ -93,28 +93,16 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a CA.
|
// Create a CA.
|
||||||
aggregatedAPIServerCA, err := certauthority.New(pkix.Name{CommonName: c.generatedCACommonName}, c.certDuration)
|
ca, err := certauthority.New(c.generatedCACommonName, c.certDuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize CA: %w", err)
|
return fmt.Errorf("could not initialize CA: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using the CA from above, create a TLS server cert for the aggregated API server to use.
|
caPrivateKeyPEM, err := ca.PrivateKeyToPEM()
|
||||||
serviceEndpoint := c.serviceNameForGeneratedCertCommonName + "." + c.namespace + ".svc"
|
|
||||||
aggregatedAPIServerTLSCert, err := aggregatedAPIServerCA.Issue(
|
|
||||||
pkix.Name{CommonName: serviceEndpoint},
|
|
||||||
[]string{serviceEndpoint},
|
|
||||||
nil,
|
|
||||||
c.certDuration,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not issue serving certificate: %w", err)
|
return fmt.Errorf("could not get CA private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the CA's public key bundle and the serving certs to a secret.
|
|
||||||
tlsCertChainPEM, tlsPrivateKeyPEM, err := certauthority.ToPEM(aggregatedAPIServerTLSCert)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not PEM encode serving certificate: %w", err)
|
|
||||||
}
|
|
||||||
secret := corev1.Secret{
|
secret := corev1.Secret{
|
||||||
TypeMeta: metav1.TypeMeta{},
|
TypeMeta: metav1.TypeMeta{},
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -123,11 +111,29 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error {
|
|||||||
Labels: c.certsSecretLabels,
|
Labels: c.certsSecretLabels,
|
||||||
},
|
},
|
||||||
StringData: map[string]string{
|
StringData: map[string]string{
|
||||||
caCertificateSecretKey: string(aggregatedAPIServerCA.Bundle()),
|
CACertificateSecretKey: string(ca.Bundle()),
|
||||||
tlsPrivateKeySecretKey: string(tlsPrivateKeyPEM),
|
CACertificatePrivateKeySecretKey: string(caPrivateKeyPEM),
|
||||||
tlsCertificateChainSecretKey: string(tlsCertChainPEM),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Using the CA from above, create a TLS server cert if we have service name.
|
||||||
|
if len(c.serviceNameForGeneratedCertCommonName) != 0 {
|
||||||
|
serviceEndpoint := c.serviceNameForGeneratedCertCommonName + "." + c.namespace + ".svc"
|
||||||
|
tlsCert, err := ca.IssueServerCert([]string{serviceEndpoint}, nil, c.certDuration)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not issue serving certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the CA's public key bundle and the serving certs to a secret.
|
||||||
|
tlsCertChainPEM, tlsPrivateKeyPEM, err := certauthority.ToPEM(tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not PEM encode serving certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.StringData[tlsPrivateKeySecretKey] = string(tlsPrivateKeyPEM)
|
||||||
|
secret.StringData[TLSCertificateChainSecretKey] = string(tlsCertChainPEM)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx.Context, &secret, metav1.CreateOptions{})
|
_, err = c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx.Context, &secret, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not create secret: %w", err)
|
return fmt.Errorf("could not create secret: %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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
@ -49,7 +49,7 @@ func TestManagerControllerOptions(t *testing.T) {
|
|||||||
observableWithInitialEventOption.WithInitialEvent,
|
observableWithInitialEventOption.WithInitialEvent,
|
||||||
0,
|
0,
|
||||||
"Pinniped CA",
|
"Pinniped CA",
|
||||||
"pinniped-api",
|
"ignored",
|
||||||
)
|
)
|
||||||
secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
|
secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
|
||||||
})
|
})
|
||||||
@ -118,6 +118,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
const installedInNamespace = "some-namespace"
|
const installedInNamespace = "some-namespace"
|
||||||
const certsSecretResourceName = "some-resource-name"
|
const certsSecretResourceName = "some-resource-name"
|
||||||
const certDuration = 12345678 * time.Second
|
const certDuration = 12345678 * time.Second
|
||||||
|
const defaultServiceName = "pinniped-api"
|
||||||
|
|
||||||
var r *require.Assertions
|
var r *require.Assertions
|
||||||
|
|
||||||
@ -125,13 +126,13 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
var kubeAPIClient *kubernetesfake.Clientset
|
var kubeAPIClient *kubernetesfake.Clientset
|
||||||
var kubeInformerClient *kubernetesfake.Clientset
|
var kubeInformerClient *kubernetesfake.Clientset
|
||||||
var kubeInformers kubeinformers.SharedInformerFactory
|
var kubeInformers kubeinformers.SharedInformerFactory
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
// nested Before's can keep adding things to the informer caches.
|
// nested Before's can keep adding things to the informer caches.
|
||||||
var startInformersAndController = func() {
|
var startInformersAndController = func(serviceName string) {
|
||||||
// Set this at the last second to allow for injection of server override.
|
// Set this at the last second to allow for injection of server override.
|
||||||
subject = NewCertsManagerController(
|
subject = NewCertsManagerController(
|
||||||
installedInNamespace,
|
installedInNamespace,
|
||||||
@ -146,12 +147,12 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
controllerlib.WithInitialEvent,
|
controllerlib.WithInitialEvent,
|
||||||
certDuration,
|
certDuration,
|
||||||
"Pinniped CA",
|
"Pinniped CA",
|
||||||
"pinniped-api",
|
serviceName,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
@ -160,14 +161,14 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformers.Start(timeoutContext.Done())
|
kubeInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||||
@ -175,7 +176,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is not yet a serving cert Secret in the installation namespace or it was deleted", func() {
|
when("there is not yet a serving cert Secret in the installation namespace or it was deleted", func() {
|
||||||
@ -191,7 +192,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("creates the serving cert Secret", func() {
|
it("creates the serving cert Secret", func() {
|
||||||
startInformersAndController()
|
startInformersAndController(defaultServiceName)
|
||||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
@ -208,23 +209,54 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
"myLabelKey2": "myLabelValue2",
|
"myLabelKey2": "myLabelValue2",
|
||||||
}, actualSecret.Labels)
|
}, actualSecret.Labels)
|
||||||
actualCACert := actualSecret.StringData["caCertificate"]
|
actualCACert := actualSecret.StringData["caCertificate"]
|
||||||
|
actualCAPrivateKey := actualSecret.StringData["caCertificatePrivateKey"]
|
||||||
actualPrivateKey := actualSecret.StringData["tlsPrivateKey"]
|
actualPrivateKey := actualSecret.StringData["tlsPrivateKey"]
|
||||||
actualCertChain := actualSecret.StringData["tlsCertificateChain"]
|
actualCertChain := actualSecret.StringData["tlsCertificateChain"]
|
||||||
r.NotEmpty(actualCACert)
|
r.NotEmpty(actualCACert)
|
||||||
|
r.NotEmpty(actualCAPrivateKey)
|
||||||
r.NotEmpty(actualPrivateKey)
|
r.NotEmpty(actualPrivateKey)
|
||||||
r.NotEmpty(actualCertChain)
|
r.NotEmpty(actualCertChain)
|
||||||
|
r.Len(actualSecret.StringData, 4)
|
||||||
|
|
||||||
// Validate the created CA's lifetime.
|
validCACert := testutil.ValidateServerCertificate(t, actualCACert, actualCACert)
|
||||||
validCACert := testutil.ValidateCertificate(t, actualCACert, actualCACert)
|
validCACert.RequireMatchesPrivateKey(actualCAPrivateKey)
|
||||||
validCACert.RequireLifetime(time.Now(), time.Now().Add(certDuration), 6*time.Minute)
|
validCACert.RequireLifetime(time.Now(), time.Now().Add(certDuration), 6*time.Minute)
|
||||||
|
|
||||||
// Validate the created cert using the CA, and also validate the cert's hostname
|
// Validate the created cert using the CA, and also validate the cert's hostname
|
||||||
validCert := testutil.ValidateCertificate(t, actualCACert, actualCertChain)
|
validCert := testutil.ValidateServerCertificate(t, actualCACert, actualCertChain)
|
||||||
validCert.RequireDNSName("pinniped-api." + installedInNamespace + ".svc")
|
validCert.RequireDNSName("pinniped-api." + installedInNamespace + ".svc")
|
||||||
validCert.RequireLifetime(time.Now(), time.Now().Add(certDuration), 6*time.Minute)
|
validCert.RequireLifetime(time.Now(), time.Now().Add(certDuration), 6*time.Minute)
|
||||||
validCert.RequireMatchesPrivateKey(actualPrivateKey)
|
validCert.RequireMatchesPrivateKey(actualPrivateKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("creates the CA but not service when the service name is empty", func() {
|
||||||
|
startInformersAndController("")
|
||||||
|
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
// Check all the relevant fields from the create Secret action
|
||||||
|
r.Len(kubeAPIClient.Actions(), 1)
|
||||||
|
actualAction := kubeAPIClient.Actions()[0].(coretesting.CreateActionImpl)
|
||||||
|
r.Equal(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, actualAction.GetResource())
|
||||||
|
r.Equal(installedInNamespace, actualAction.GetNamespace())
|
||||||
|
actualSecret := actualAction.GetObject().(*corev1.Secret)
|
||||||
|
r.Equal(certsSecretResourceName, actualSecret.Name)
|
||||||
|
r.Equal(installedInNamespace, actualSecret.Namespace)
|
||||||
|
r.Equal(map[string]string{
|
||||||
|
"myLabelKey1": "myLabelValue1",
|
||||||
|
"myLabelKey2": "myLabelValue2",
|
||||||
|
}, actualSecret.Labels)
|
||||||
|
actualCACert := actualSecret.StringData["caCertificate"]
|
||||||
|
actualCAPrivateKey := actualSecret.StringData["caCertificatePrivateKey"]
|
||||||
|
r.NotEmpty(actualCACert)
|
||||||
|
r.NotEmpty(actualCAPrivateKey)
|
||||||
|
r.Len(actualSecret.StringData, 2)
|
||||||
|
|
||||||
|
validCACert := testutil.ValidateServerCertificate(t, actualCACert, actualCACert)
|
||||||
|
validCACert.RequireMatchesPrivateKey(actualCAPrivateKey)
|
||||||
|
validCACert.RequireLifetime(time.Now(), time.Now().Add(certDuration), 6*time.Minute)
|
||||||
|
})
|
||||||
|
|
||||||
when("creating the Secret fails", func() {
|
when("creating the Secret fails", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
kubeAPIClient.PrependReactor(
|
kubeAPIClient.PrependReactor(
|
||||||
@ -237,7 +269,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("returns the create error", func() {
|
it("returns the create error", func() {
|
||||||
startInformersAndController()
|
startInformersAndController(defaultServiceName)
|
||||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||||
r.EqualError(err, "could not create secret: create failed")
|
r.EqualError(err, "could not create secret: create failed")
|
||||||
})
|
})
|
||||||
@ -257,7 +289,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("does not need to make any API calls with its API client", func() {
|
it("does not need to make any API calls with its API client", func() {
|
||||||
startInformersAndController()
|
startInformersAndController(defaultServiceName)
|
||||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.Empty(kubeAPIClient.Actions())
|
r.Empty(kubeAPIClient.Actions())
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
@ -18,14 +18,14 @@ import (
|
|||||||
type certsObserverController struct {
|
type certsObserverController struct {
|
||||||
namespace string
|
namespace string
|
||||||
certsSecretResourceName string
|
certsSecretResourceName string
|
||||||
dynamicCertProvider dynamiccert.Provider
|
dynamicCertProvider dynamiccert.Private
|
||||||
secretInformer corev1informers.SecretInformer
|
secretInformer corev1informers.SecretInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCertsObserverController(
|
func NewCertsObserverController(
|
||||||
namespace string,
|
namespace string,
|
||||||
certsSecretResourceName string,
|
certsSecretResourceName string,
|
||||||
dynamicCertProvider dynamiccert.Provider,
|
dynamicCertProvider dynamiccert.Private,
|
||||||
secretInformer corev1informers.SecretInformer,
|
secretInformer corev1informers.SecretInformer,
|
||||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||||
) controllerlib.Controller {
|
) controllerlib.Controller {
|
||||||
@ -57,12 +57,15 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error {
|
|||||||
if notFound {
|
if notFound {
|
||||||
klog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted")
|
klog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted")
|
||||||
// The secret does not exist yet or was deleted.
|
// The secret does not exist yet or was deleted.
|
||||||
c.dynamicCertProvider.Set(nil, nil)
|
c.dynamicCertProvider.UnsetCertKeyContent()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutate the in-memory cert provider to update with the latest cert values.
|
// Mutate the in-memory cert provider to update with the latest cert values.
|
||||||
c.dynamicCertProvider.Set(certSecret.Data[tlsCertificateChainSecretKey], certSecret.Data[tlsPrivateKeySecretKey])
|
if err := c.dynamicCertProvider.SetCertKeyContent(certSecret.Data[TLSCertificateChainSecretKey], certSecret.Data[tlsPrivateKeySecretKey]); err != nil {
|
||||||
|
return fmt.Errorf("failed to set serving cert/key content from secret %s/%s: %w", c.namespace, c.certsSecretResourceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
klog.Info("certsObserverController Sync updated certs in the dynamic cert provider")
|
klog.Info("certsObserverController Sync updated certs in the dynamic cert provider")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package apicerts
|
package apicerts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
kubeinformers "k8s.io/client-go/informers"
|
kubeinformers "k8s.io/client-go/informers"
|
||||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
@ -95,6 +97,7 @@ func TestObserverControllerInformerFilters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObserverControllerSync(t *testing.T) {
|
func TestObserverControllerSync(t *testing.T) {
|
||||||
|
name := t.Name()
|
||||||
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
|
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
const installedInNamespace = "some-namespace"
|
const installedInNamespace = "some-namespace"
|
||||||
const certsSecretResourceName = "some-resource-name"
|
const certsSecretResourceName = "some-resource-name"
|
||||||
@ -104,10 +107,10 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
var subject controllerlib.Controller
|
var subject controllerlib.Controller
|
||||||
var kubeInformerClient *kubernetesfake.Clientset
|
var kubeInformerClient *kubernetesfake.Clientset
|
||||||
var kubeInformers kubeinformers.SharedInformerFactory
|
var kubeInformers kubeinformers.SharedInformerFactory
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
var dynamicCertProvider dynamiccert.Provider
|
var dynamicCertProvider dynamiccert.Private
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
// nested Before's can keep adding things to the informer caches.
|
// nested Before's can keep adding things to the informer caches.
|
||||||
@ -123,7 +126,7 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
@ -132,22 +135,22 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformers.Start(timeoutContext.Done())
|
kubeInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||||
dynamicCertProvider = dynamiccert.New()
|
dynamicCertProvider = dynamiccert.NewServingCert(name)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is not yet a serving cert Secret in the installation namespace or it was deleted", func() {
|
when("there is not yet a serving cert Secret in the installation namespace or it was deleted", func() {
|
||||||
@ -161,7 +164,20 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
err := kubeInformerClient.Tracker().Add(unrelatedSecret)
|
err := kubeInformerClient.Tracker().Add(unrelatedSecret)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
dynamicCertProvider.Set([]byte("some cert"), []byte("some private key"))
|
caCrt, caKey, err := testutil.CreateCertificate(
|
||||||
|
time.Now().Add(-time.Hour),
|
||||||
|
time.Now().Add(time.Hour),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ca, err := certauthority.Load(string(caCrt), string(caKey))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
crt, key, err := ca.IssueServerCertPEM(nil, nil, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = dynamicCertProvider.SetCertKeyContent(crt, key)
|
||||||
|
r.NoError(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sets the dynamicCertProvider's cert and key to nil", func() {
|
it("sets the dynamicCertProvider's cert and key to nil", func() {
|
||||||
@ -177,6 +193,18 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
when("there is a serving cert Secret with the expected keys already in the installation namespace", func() {
|
when("there is a serving cert Secret with the expected keys already in the installation namespace", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
|
caCrt, caKey, err := testutil.CreateCertificate(
|
||||||
|
time.Now().Add(-time.Hour),
|
||||||
|
time.Now().Add(time.Hour),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ca, err := certauthority.Load(string(caCrt), string(caKey))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
crt, key, err := ca.IssueServerCertPEM(nil, nil, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
apiServingCertSecret := &corev1.Secret{
|
apiServingCertSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: certsSecretResourceName,
|
Name: certsSecretResourceName,
|
||||||
@ -184,24 +212,29 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caCertificate": []byte("fake cert"),
|
"caCertificate": []byte("fake cert"),
|
||||||
"tlsPrivateKey": []byte("fake private key"),
|
"tlsPrivateKey": key,
|
||||||
"tlsCertificateChain": []byte("fake cert chain"),
|
"tlsCertificateChain": crt,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
err = kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
dynamicCertProvider.Set(nil, nil)
|
dynamicCertProvider.UnsetCertKeyContent()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates the dynamicCertProvider's cert and key", func() {
|
it("updates the dynamicCertProvider's cert and key", func() {
|
||||||
startInformersAndController()
|
startInformersAndController()
|
||||||
|
|
||||||
|
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||||
|
r.Nil(actualCertChain)
|
||||||
|
r.Nil(actualKey)
|
||||||
|
|
||||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
actualCertChain, actualKey = dynamicCertProvider.CurrentCertKeyContent()
|
||||||
r.Equal("fake cert chain", string(actualCertChain))
|
r.True(strings.HasPrefix(string(actualCertChain), `-----BEGIN CERTIFICATE-----`), "not a cert:\n%s", string(actualCertChain))
|
||||||
r.Equal("fake private key", string(actualKey))
|
r.True(strings.HasPrefix(string(actualKey), `-----BEGIN PRIVATE KEY-----`), "not a key:\n%s", string(actualKey))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -217,17 +250,22 @@ func TestObserverControllerSync(t *testing.T) {
|
|||||||
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
dynamicCertProvider.Set(nil, nil)
|
dynamicCertProvider.UnsetCertKeyContent()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("set the missing values in the dynamicCertProvider as nil", func() {
|
it("returns an error and does not change the dynamicCertProvider", func() {
|
||||||
startInformersAndController()
|
startInformersAndController()
|
||||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
|
||||||
r.NoError(err)
|
|
||||||
|
|
||||||
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||||
r.Nil(actualCertChain)
|
r.Nil(actualCertChain)
|
||||||
r.Nil(actualKey)
|
r.Nil(actualKey)
|
||||||
|
|
||||||
|
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||||
|
r.EqualError(err, "failed to set serving cert/key content from secret some-namespace/some-resource-name: TestObserverControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input")
|
||||||
|
|
||||||
|
actualCertChain, actualKey = dynamicCertProvider.CurrentCertKeyContent()
|
||||||
|
r.Nil(actualCertChain)
|
||||||
|
r.Nil(actualKey)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
@ -6,7 +6,6 @@ package cachecleaner
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -150,7 +149,7 @@ func TestController(t *testing.T) {
|
|||||||
jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators()
|
jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators()
|
||||||
controller := New(cache, webhooks, jwtAuthenticators, testLog)
|
controller := New(cache, webhooks, jwtAuthenticators, testLog)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
informers.Start(ctx.Done())
|
informers.Start(ctx.Done())
|
||||||
|
@ -325,7 +325,7 @@ func TestController(t *testing.T) {
|
|||||||
|
|
||||||
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog)
|
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
informers.Start(ctx.Done())
|
informers.Start(ctx.Done())
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -93,7 +92,7 @@ func TestController(t *testing.T) {
|
|||||||
|
|
||||||
controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog)
|
controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
informers.Start(ctx.Done())
|
informers.Start(ctx.Done())
|
||||||
|
864
internal/controller/impersonatorconfig/impersonator_config.go
Normal file
864
internal/controller/impersonatorconfig/impersonator_config.go
Normal file
@ -0,0 +1,864 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package impersonatorconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/clock"
|
||||||
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
|
||||||
|
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/clusterhost"
|
||||||
|
"go.pinniped.dev/internal/concierge/impersonator"
|
||||||
|
"go.pinniped.dev/internal/constable"
|
||||||
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||||
|
"go.pinniped.dev/internal/controller/apicerts"
|
||||||
|
"go.pinniped.dev/internal/controller/issuerconfig"
|
||||||
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
impersonationProxyPort = 8444
|
||||||
|
defaultHTTPSPort = 443
|
||||||
|
approximatelyOneHundredYears = 100 * 365 * 24 * time.Hour
|
||||||
|
caCommonName = "Pinniped Impersonation Proxy CA"
|
||||||
|
caCrtKey = "ca.crt"
|
||||||
|
caKeyKey = "ca.key"
|
||||||
|
appLabelKey = "app"
|
||||||
|
)
|
||||||
|
|
||||||
|
type impersonatorConfigController struct {
|
||||||
|
namespace string
|
||||||
|
configMapResourceName string
|
||||||
|
credentialIssuerResourceName string
|
||||||
|
generatedLoadBalancerServiceName string
|
||||||
|
tlsSecretName string
|
||||||
|
caSecretName string
|
||||||
|
impersonationSignerSecretName string
|
||||||
|
|
||||||
|
k8sClient kubernetes.Interface
|
||||||
|
pinnipedAPIClient pinnipedclientset.Interface
|
||||||
|
|
||||||
|
configMapsInformer corev1informers.ConfigMapInformer
|
||||||
|
servicesInformer corev1informers.ServiceInformer
|
||||||
|
secretsInformer corev1informers.SecretInformer
|
||||||
|
|
||||||
|
labels map[string]string
|
||||||
|
clock clock.Clock
|
||||||
|
impersonationSigningCertProvider dynamiccert.Provider
|
||||||
|
impersonatorFunc impersonator.FactoryFunc
|
||||||
|
|
||||||
|
hasControlPlaneNodes *bool
|
||||||
|
serverStopCh chan struct{}
|
||||||
|
errorCh chan error
|
||||||
|
tlsServingCertDynamicCertProvider dynamiccert.Private
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImpersonatorConfigController(
|
||||||
|
namespace string,
|
||||||
|
configMapResourceName string,
|
||||||
|
credentialIssuerResourceName string,
|
||||||
|
k8sClient kubernetes.Interface,
|
||||||
|
pinnipedAPIClient pinnipedclientset.Interface,
|
||||||
|
configMapsInformer corev1informers.ConfigMapInformer,
|
||||||
|
servicesInformer corev1informers.ServiceInformer,
|
||||||
|
secretsInformer corev1informers.SecretInformer,
|
||||||
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||||
|
withInitialEvent pinnipedcontroller.WithInitialEventOptionFunc,
|
||||||
|
generatedLoadBalancerServiceName string,
|
||||||
|
tlsSecretName string,
|
||||||
|
caSecretName string,
|
||||||
|
labels map[string]string,
|
||||||
|
clock clock.Clock,
|
||||||
|
impersonatorFunc impersonator.FactoryFunc,
|
||||||
|
impersonationSignerSecretName string,
|
||||||
|
impersonationSigningCertProvider dynamiccert.Provider,
|
||||||
|
) controllerlib.Controller {
|
||||||
|
secretNames := sets.NewString(tlsSecretName, caSecretName, impersonationSignerSecretName)
|
||||||
|
return controllerlib.New(
|
||||||
|
controllerlib.Config{
|
||||||
|
Name: "impersonator-config-controller",
|
||||||
|
Syncer: &impersonatorConfigController{
|
||||||
|
namespace: namespace,
|
||||||
|
configMapResourceName: configMapResourceName,
|
||||||
|
credentialIssuerResourceName: credentialIssuerResourceName,
|
||||||
|
generatedLoadBalancerServiceName: generatedLoadBalancerServiceName,
|
||||||
|
tlsSecretName: tlsSecretName,
|
||||||
|
caSecretName: caSecretName,
|
||||||
|
impersonationSignerSecretName: impersonationSignerSecretName,
|
||||||
|
k8sClient: k8sClient,
|
||||||
|
pinnipedAPIClient: pinnipedAPIClient,
|
||||||
|
configMapsInformer: configMapsInformer,
|
||||||
|
servicesInformer: servicesInformer,
|
||||||
|
secretsInformer: secretsInformer,
|
||||||
|
labels: labels,
|
||||||
|
clock: clock,
|
||||||
|
impersonationSigningCertProvider: impersonationSigningCertProvider,
|
||||||
|
impersonatorFunc: impersonatorFunc,
|
||||||
|
tlsServingCertDynamicCertProvider: dynamiccert.NewServingCert("impersonation-proxy-serving-cert"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
withInformer(
|
||||||
|
configMapsInformer,
|
||||||
|
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configMapResourceName, namespace),
|
||||||
|
controllerlib.InformerOption{},
|
||||||
|
),
|
||||||
|
withInformer(
|
||||||
|
servicesInformer,
|
||||||
|
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(generatedLoadBalancerServiceName, namespace),
|
||||||
|
controllerlib.InformerOption{},
|
||||||
|
),
|
||||||
|
withInformer(
|
||||||
|
secretsInformer,
|
||||||
|
pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
|
||||||
|
return obj.GetNamespace() == namespace && secretNames.Has(obj.GetName())
|
||||||
|
}, nil),
|
||||||
|
controllerlib.InformerOption{},
|
||||||
|
),
|
||||||
|
// Be sure to run once even if the ConfigMap that the informer is watching doesn't exist so we can implement
|
||||||
|
// the default configuration behavior.
|
||||||
|
withInitialEvent(controllerlib.Key{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: configMapResourceName,
|
||||||
|
}),
|
||||||
|
// TODO fix these controller options to make this a singleton queue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) Sync(syncCtx controllerlib.Context) error {
|
||||||
|
plog.Debug("Starting impersonatorConfigController Sync")
|
||||||
|
|
||||||
|
strategy, err := c.doSync(syncCtx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
strategy = &v1alpha1.CredentialIssuerStrategy{
|
||||||
|
Type: v1alpha1.ImpersonationProxyStrategyType,
|
||||||
|
Status: v1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: v1alpha1.ErrorDuringSetupStrategyReason,
|
||||||
|
Message: err.Error(),
|
||||||
|
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||||
|
}
|
||||||
|
// The impersonator is not ready, so clear the signer CA from the dynamic provider.
|
||||||
|
c.clearSignerCA()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStrategyErr := c.updateStrategy(syncCtx.Context, strategy)
|
||||||
|
if updateStrategyErr != nil {
|
||||||
|
plog.Error("error while updating the CredentialIssuer status", err)
|
||||||
|
if err == nil {
|
||||||
|
err = updateStrategyErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
plog.Debug("Successfully finished impersonatorConfigController Sync")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type certNameInfo struct {
|
||||||
|
// ready will be true when the certificate name information is known.
|
||||||
|
// ready will be false when it is pending because we are waiting for a load balancer to get assigned an ip/hostname.
|
||||||
|
// When false, the other fields in this struct should not be considered meaningful and may be zero values.
|
||||||
|
ready bool
|
||||||
|
|
||||||
|
// The IP address or hostname which was selected to be used as the name in the cert.
|
||||||
|
// Either selectedIP or selectedHostname will be set, but not both.
|
||||||
|
selectedIP net.IP
|
||||||
|
selectedHostname string
|
||||||
|
|
||||||
|
// The name of the endpoint to which a client should connect to talk to the impersonator.
|
||||||
|
// This may be a hostname or an IP, and may include a port number.
|
||||||
|
clientEndpoint string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v1alpha1.CredentialIssuerStrategy, error) {
|
||||||
|
ctx := syncCtx.Context
|
||||||
|
|
||||||
|
config, err := c.loadImpersonationProxyConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a live API call to avoid the cost of having an informer watch all node changes on the cluster,
|
||||||
|
// since there could be lots and we don't especially care about node changes.
|
||||||
|
// Once we have concluded that there is or is not a visible control plane, then cache that decision
|
||||||
|
// to avoid listing nodes very often.
|
||||||
|
if c.hasControlPlaneNodes == nil {
|
||||||
|
hasControlPlaneNodes, err := clusterhost.New(c.k8sClient).HasControlPlaneNodes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.hasControlPlaneNodes = &hasControlPlaneNodes
|
||||||
|
plog.Debug("Queried for control plane nodes", "foundControlPlaneNodes", hasControlPlaneNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.shouldHaveImpersonator(config) {
|
||||||
|
if err = c.ensureImpersonatorIsStarted(syncCtx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = c.ensureImpersonatorIsStopped(true); err != nil {
|
||||||
|
return nil, err // TODO write unit test that errors during stopping the server are returned by sync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.shouldHaveLoadBalancer(config) {
|
||||||
|
if err = c.ensureLoadBalancerIsStarted(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = c.ensureLoadBalancerIsStopped(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nameInfo, err := c.findDesiredTLSCertificateName(config)
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected error while determining the name that should go into the certs, so clear any existing certs.
|
||||||
|
c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var impersonationCA *certauthority.CA
|
||||||
|
if c.shouldHaveTLSSecret(config) {
|
||||||
|
if impersonationCA, err = c.ensureCASecretIsCreated(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = c.ensureTLSSecret(ctx, nameInfo, impersonationCA); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialIssuerStrategyResult := c.doSyncResult(nameInfo, config, impersonationCA)
|
||||||
|
|
||||||
|
if err = c.loadSignerCA(credentialIssuerStrategyResult.Status); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentialIssuerStrategyResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) loadImpersonationProxyConfiguration() (*impersonator.Config, error) {
|
||||||
|
configMap, err := c.configMapsInformer.Lister().ConfigMaps(c.namespace).Get(c.configMapResourceName)
|
||||||
|
notFound := k8serrors.IsNotFound(err)
|
||||||
|
if err != nil && !notFound {
|
||||||
|
return nil, fmt.Errorf("failed to get %s/%s configmap: %w", c.namespace, c.configMapResourceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config *impersonator.Config
|
||||||
|
if notFound {
|
||||||
|
plog.Info("Did not find impersonation proxy config: using default config values",
|
||||||
|
"configmap", c.configMapResourceName,
|
||||||
|
"namespace", c.namespace,
|
||||||
|
)
|
||||||
|
config = impersonator.NewConfig() // use default configuration options
|
||||||
|
} else {
|
||||||
|
config, err = impersonator.ConfigFromConfigMap(configMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid impersonator configuration: %v", err)
|
||||||
|
}
|
||||||
|
plog.Info("Read impersonation proxy config",
|
||||||
|
"configmap", c.configMapResourceName,
|
||||||
|
"namespace", c.namespace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) shouldHaveImpersonator(config *impersonator.Config) bool {
|
||||||
|
return c.enabledByAutoMode(config) || config.Mode == impersonator.ModeEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) enabledByAutoMode(config *impersonator.Config) bool {
|
||||||
|
return config.Mode == impersonator.ModeAuto && !*c.hasControlPlaneNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) disabledByAutoMode(config *impersonator.Config) bool {
|
||||||
|
return config.Mode == impersonator.ModeAuto && *c.hasControlPlaneNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) disabledExplicitly(config *impersonator.Config) bool {
|
||||||
|
return config.Mode == impersonator.ModeDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) shouldHaveLoadBalancer(config *impersonator.Config) bool {
|
||||||
|
return c.shouldHaveImpersonator(config) && !config.HasEndpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) shouldHaveTLSSecret(config *impersonator.Config) bool {
|
||||||
|
return c.shouldHaveImpersonator(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) updateStrategy(ctx context.Context, strategy *v1alpha1.CredentialIssuerStrategy) error {
|
||||||
|
return issuerconfig.UpdateStrategy(ctx, c.credentialIssuerResourceName, c.labels, c.pinnipedAPIClient, *strategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) loadBalancerExists() (bool, error) {
|
||||||
|
_, err := c.servicesInformer.Lister().Services(c.namespace).Get(c.generatedLoadBalancerServiceName)
|
||||||
|
notFound := k8serrors.IsNotFound(err)
|
||||||
|
if notFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) tlsSecretExists() (bool, *v1.Secret, error) {
|
||||||
|
secret, err := c.secretsInformer.Lister().Secrets(c.namespace).Get(c.tlsSecretName)
|
||||||
|
notFound := k8serrors.IsNotFound(err)
|
||||||
|
if notFound {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return true, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureImpersonatorIsStarted(syncCtx controllerlib.Context) error {
|
||||||
|
if c.serverStopCh != nil {
|
||||||
|
// The server was already started, but it could have died in the background, so make a non-blocking
|
||||||
|
// check to see if it has sent any errors on the errorCh.
|
||||||
|
select {
|
||||||
|
case runningErr := <-c.errorCh:
|
||||||
|
if runningErr == nil {
|
||||||
|
// The server sent a nil error, meaning that it shutdown without reporting any particular
|
||||||
|
// error for some reason. We would still like to report this as an error for logging purposes.
|
||||||
|
runningErr = constable.Error("unexpected shutdown of proxy server")
|
||||||
|
}
|
||||||
|
// The server has stopped, so finish shutting it down.
|
||||||
|
// If that fails too, return both errors for logging purposes.
|
||||||
|
// By returning an error, the sync function will be called again
|
||||||
|
// and we'll have a chance to restart the server.
|
||||||
|
close(c.errorCh) // We don't want ensureImpersonatorIsStopped to block on reading this channel.
|
||||||
|
stoppingErr := c.ensureImpersonatorIsStopped(false)
|
||||||
|
return errors.NewAggregate([]error{runningErr, stoppingErr})
|
||||||
|
default:
|
||||||
|
// Seems like it is still running, so nothing to do.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Starting impersonation proxy", "port", impersonationProxyPort)
|
||||||
|
startImpersonatorFunc, err := c.impersonatorFunc(
|
||||||
|
impersonationProxyPort,
|
||||||
|
c.tlsServingCertDynamicCertProvider,
|
||||||
|
c.impersonationSigningCertProvider,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.serverStopCh = make(chan struct{})
|
||||||
|
// use a buffered channel so that startImpersonatorFunc can send
|
||||||
|
// on it without coordinating with the main controller go routine
|
||||||
|
c.errorCh = make(chan error, 1)
|
||||||
|
|
||||||
|
// startImpersonatorFunc will block until the server shuts down (or fails to start), so run it in the background.
|
||||||
|
go func() {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
|
||||||
|
// The server has stopped, so enqueue ourselves for another sync,
|
||||||
|
// so we can try to start the server again as quickly as possible.
|
||||||
|
defer syncCtx.Queue.AddRateLimited(syncCtx.Key)
|
||||||
|
|
||||||
|
// Forward any errors returned by startImpersonatorFunc on the errorCh.
|
||||||
|
c.errorCh <- startImpersonatorFunc(c.serverStopCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureImpersonatorIsStopped(shouldCloseErrChan bool) error {
|
||||||
|
if c.serverStopCh == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Stopping impersonation proxy", "port", impersonationProxyPort)
|
||||||
|
close(c.serverStopCh)
|
||||||
|
stopErr := <-c.errorCh
|
||||||
|
|
||||||
|
if shouldCloseErrChan {
|
||||||
|
close(c.errorCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.serverStopCh = nil
|
||||||
|
c.errorCh = nil
|
||||||
|
|
||||||
|
return stopErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureLoadBalancerIsStarted(ctx context.Context) error {
|
||||||
|
running, err := c.loadBalancerExists()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
appNameLabel := c.labels[appLabelKey]
|
||||||
|
loadBalancer := v1.Service{
|
||||||
|
Spec: v1.ServiceSpec{
|
||||||
|
Type: v1.ServiceTypeLoadBalancer,
|
||||||
|
Ports: []v1.ServicePort{
|
||||||
|
{
|
||||||
|
TargetPort: intstr.FromInt(impersonationProxyPort),
|
||||||
|
Port: defaultHTTPSPort,
|
||||||
|
Protocol: v1.ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Selector: map[string]string{appLabelKey: appNameLabel},
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: c.generatedLoadBalancerServiceName,
|
||||||
|
Namespace: c.namespace,
|
||||||
|
Labels: c.labels,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
plog.Info("creating load balancer for impersonation proxy",
|
||||||
|
"service", c.generatedLoadBalancerServiceName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
_, err = c.k8sClient.CoreV1().Services(c.namespace).Create(ctx, &loadBalancer, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureLoadBalancerIsStopped(ctx context.Context) error {
|
||||||
|
running, err := c.loadBalancerExists()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Deleting load balancer for impersonation proxy",
|
||||||
|
"service", c.generatedLoadBalancerServiceName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
return c.k8sClient.CoreV1().Services(c.namespace).Delete(ctx, c.generatedLoadBalancerServiceName, metav1.DeleteOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureTLSSecret(ctx context.Context, nameInfo *certNameInfo, ca *certauthority.CA) error {
|
||||||
|
secretFromInformer, err := c.secretsInformer.Lister().Secrets(c.namespace).Get(c.tlsSecretName)
|
||||||
|
notFound := k8serrors.IsNotFound(err)
|
||||||
|
if !notFound && err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !notFound {
|
||||||
|
secretWasDeleted, err := c.deleteTLSSecretWhenCertificateDoesNotMatchDesiredState(ctx, nameInfo, ca, secretFromInformer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// If it was deleted by the above call, then set it to nil. This allows us to avoid waiting
|
||||||
|
// for the informer cache to update before deciding to proceed to create the new Secret below.
|
||||||
|
if secretWasDeleted {
|
||||||
|
secretFromInformer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ensureTLSSecretIsCreatedAndLoaded(ctx, nameInfo, secretFromInformer, ca)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatchDesiredState(ctx context.Context, nameInfo *certNameInfo, ca *certauthority.CA, secret *v1.Secret) (bool, error) {
|
||||||
|
certPEM := secret.Data[v1.TLSCertKey]
|
||||||
|
block, _ := pem.Decode(certPEM)
|
||||||
|
if block == nil {
|
||||||
|
plog.Warning("Found missing or not PEM-encoded data in TLS Secret",
|
||||||
|
"invalidCertPEM", string(certPEM),
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
deleteErr := c.ensureTLSSecretIsRemoved(ctx)
|
||||||
|
if deleteErr != nil {
|
||||||
|
return false, fmt.Errorf("found missing or not PEM-encoded data in TLS Secret, but got error while deleting it: %w", deleteErr)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actualCertFromSecret, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
plog.Error("Found invalid PEM data in TLS Secret", err,
|
||||||
|
"invalidCertPEM", string(certPEM),
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
|
||||||
|
return false, fmt.Errorf("PEM data represented an invalid cert, but got error while deleting it: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPEM := secret.Data[v1.TLSPrivateKeyKey]
|
||||||
|
_, err = tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
if err != nil {
|
||||||
|
plog.Error("Found invalid private key PEM data in TLS Secret", err,
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
|
||||||
|
return false, fmt.Errorf("cert had an invalid private key, but got error while deleting it: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := x509.VerifyOptions{Roots: ca.Pool()}
|
||||||
|
if _, err = actualCertFromSecret.Verify(opts); err != nil {
|
||||||
|
// The TLS cert was not signed by the current CA. Since they are mismatched, delete the TLS cert
|
||||||
|
// so we can recreate it using the current CA.
|
||||||
|
if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nameInfo.ready {
|
||||||
|
// We currently have a secret but we are waiting for a load balancer to be assigned an ingress, so
|
||||||
|
// our current secret must be old/unwanted.
|
||||||
|
if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actualIPs := actualCertFromSecret.IPAddresses
|
||||||
|
actualHostnames := actualCertFromSecret.DNSNames
|
||||||
|
plog.Info("Checking TLS certificate names",
|
||||||
|
"desiredIP", nameInfo.selectedIP,
|
||||||
|
"desiredHostname", nameInfo.selectedHostname,
|
||||||
|
"actualIPs", actualIPs,
|
||||||
|
"actualHostnames", actualHostnames,
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
|
||||||
|
if certHostnameAndIPMatchDesiredState(nameInfo.selectedIP, actualIPs, nameInfo.selectedHostname, actualHostnames) {
|
||||||
|
// The cert already matches the desired state, so there is no need to delete/recreate it.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func certHostnameAndIPMatchDesiredState(desiredIP net.IP, actualIPs []net.IP, desiredHostname string, actualHostnames []string) bool {
|
||||||
|
if desiredIP != nil && len(actualIPs) == 1 && desiredIP.Equal(actualIPs[0]) && len(actualHostnames) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if desiredHostname != "" && len(actualHostnames) == 1 && desiredHostname == actualHostnames[0] && len(actualIPs) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureTLSSecretIsCreatedAndLoaded(ctx context.Context, nameInfo *certNameInfo, secret *v1.Secret, ca *certauthority.CA) error {
|
||||||
|
if secret != nil {
|
||||||
|
err := c.loadTLSCertFromSecret(secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nameInfo.ready {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newTLSSecret, err := c.createNewTLSSecret(ctx, ca, nameInfo.selectedIP, nameInfo.selectedHostname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.loadTLSCertFromSecret(newTLSSecret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureCASecretIsCreated(ctx context.Context) (*certauthority.CA, error) {
|
||||||
|
caSecret, err := c.secretsInformer.Lister().Secrets(c.namespace).Get(c.caSecretName)
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var impersonationCA *certauthority.CA
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
impersonationCA, err = c.createCASecret(ctx)
|
||||||
|
} else {
|
||||||
|
crtBytes := caSecret.Data[caCrtKey]
|
||||||
|
keyBytes := caSecret.Data[caKeyKey]
|
||||||
|
impersonationCA, err = certauthority.Load(string(crtBytes), string(keyBytes))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return impersonationCA, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) createCASecret(ctx context.Context) (*certauthority.CA, error) {
|
||||||
|
impersonationCA, err := certauthority.New(caCommonName, approximatelyOneHundredYears)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create impersonation CA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caPrivateKeyPEM, err := impersonationCA.PrivateKeyToPEM()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: c.caSecretName,
|
||||||
|
Namespace: c.namespace,
|
||||||
|
Labels: c.labels,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
caCrtKey: impersonationCA.Bundle(),
|
||||||
|
caKeyKey: caPrivateKeyPEM,
|
||||||
|
},
|
||||||
|
Type: v1.SecretTypeOpaque,
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Creating CA certificates for impersonation proxy",
|
||||||
|
"secret", c.caSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
if _, err = c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx, &secret, metav1.CreateOptions{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return impersonationCA, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) findDesiredTLSCertificateName(config *impersonator.Config) (*certNameInfo, error) {
|
||||||
|
if config.HasEndpoint() {
|
||||||
|
return c.findTLSCertificateNameFromEndpointConfig(config), nil
|
||||||
|
}
|
||||||
|
return c.findTLSCertificateNameFromLoadBalancer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) findTLSCertificateNameFromEndpointConfig(config *impersonator.Config) *certNameInfo {
|
||||||
|
endpointMaybeWithPort := config.Endpoint
|
||||||
|
endpointWithoutPort := strings.Split(endpointMaybeWithPort, ":")[0]
|
||||||
|
parsedAsIP := net.ParseIP(endpointWithoutPort)
|
||||||
|
if parsedAsIP != nil {
|
||||||
|
return &certNameInfo{ready: true, selectedIP: parsedAsIP, clientEndpoint: endpointMaybeWithPort}
|
||||||
|
}
|
||||||
|
return &certNameInfo{ready: true, selectedHostname: endpointWithoutPort, clientEndpoint: endpointMaybeWithPort}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) findTLSCertificateNameFromLoadBalancer() (*certNameInfo, error) {
|
||||||
|
lb, err := c.servicesInformer.Lister().Services(c.namespace).Get(c.generatedLoadBalancerServiceName)
|
||||||
|
notFound := k8serrors.IsNotFound(err)
|
||||||
|
if notFound {
|
||||||
|
// We aren't ready and will try again later in this case.
|
||||||
|
return &certNameInfo{ready: false}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ingresses := lb.Status.LoadBalancer.Ingress
|
||||||
|
if len(ingresses) == 0 || (ingresses[0].Hostname == "" && ingresses[0].IP == "") {
|
||||||
|
plog.Info("load balancer for impersonation proxy does not have an ingress yet, so skipping tls cert generation while we wait",
|
||||||
|
"service", c.generatedLoadBalancerServiceName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
return &certNameInfo{ready: false}, nil
|
||||||
|
}
|
||||||
|
for _, ingress := range ingresses {
|
||||||
|
hostname := ingress.Hostname
|
||||||
|
if hostname != "" {
|
||||||
|
return &certNameInfo{ready: true, selectedHostname: hostname, clientEndpoint: hostname}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ingress := range ingresses {
|
||||||
|
ip := ingress.IP
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
if parsedIP != nil {
|
||||||
|
return &certNameInfo{ready: true, selectedIP: parsedIP, clientEndpoint: ip}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("could not find valid IP addresses or hostnames from load balancer %s/%s", c.namespace, lb.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) createNewTLSSecret(ctx context.Context, ca *certauthority.CA, ip net.IP, hostname string) (*v1.Secret, error) {
|
||||||
|
var hostnames []string
|
||||||
|
var ips []net.IP
|
||||||
|
if hostname != "" {
|
||||||
|
hostnames = []string{hostname}
|
||||||
|
}
|
||||||
|
if ip != nil {
|
||||||
|
ips = []net.IP{ip}
|
||||||
|
}
|
||||||
|
|
||||||
|
impersonationCert, err := ca.IssueServerCert(hostnames, ips, approximatelyOneHundredYears)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create impersonation cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := certauthority.ToPEM(impersonationCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newTLSSecret := &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: c.tlsSecretName,
|
||||||
|
Namespace: c.namespace,
|
||||||
|
Labels: c.labels,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
v1.TLSPrivateKeyKey: keyPEM,
|
||||||
|
v1.TLSCertKey: certPEM,
|
||||||
|
},
|
||||||
|
Type: v1.SecretTypeTLS,
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Creating TLS certificates for impersonation proxy",
|
||||||
|
"ips", ips,
|
||||||
|
"hostnames", hostnames,
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
return c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx, newTLSSecret, metav1.CreateOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) loadTLSCertFromSecret(tlsSecret *v1.Secret) error {
|
||||||
|
certPEM := tlsSecret.Data[v1.TLSCertKey]
|
||||||
|
keyPEM := tlsSecret.Data[v1.TLSPrivateKeyKey]
|
||||||
|
|
||||||
|
if err := c.tlsServingCertDynamicCertProvider.SetCertKeyContent(certPEM, keyPEM); err != nil {
|
||||||
|
c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent()
|
||||||
|
return fmt.Errorf("could not parse TLS cert PEM data from Secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Loading TLS certificates for impersonation proxy",
|
||||||
|
"certPEM", string(certPEM),
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) ensureTLSSecretIsRemoved(ctx context.Context) error {
|
||||||
|
tlsSecretExists, _, err := c.tlsSecretExists()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !tlsSecretExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
plog.Info("Deleting TLS certificates for impersonation proxy",
|
||||||
|
"secret", c.tlsSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
err = c.k8sClient.CoreV1().Secrets(c.namespace).Delete(ctx, c.tlsSecretName, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) loadSignerCA(status v1alpha1.StrategyStatus) error {
|
||||||
|
// Clear it when the impersonator is not completely ready.
|
||||||
|
if status != v1alpha1.SuccessStrategyStatus {
|
||||||
|
c.clearSignerCA()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
signingCertSecret, err := c.secretsInformer.Lister().Secrets(c.namespace).Get(c.impersonationSignerSecretName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not load the impersonator's credential signing secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM := signingCertSecret.Data[apicerts.CACertificateSecretKey]
|
||||||
|
keyPEM := signingCertSecret.Data[apicerts.CACertificatePrivateKeySecretKey]
|
||||||
|
|
||||||
|
if err := c.impersonationSigningCertProvider.SetCertKeyContent(certPEM, keyPEM); err != nil {
|
||||||
|
return fmt.Errorf("could not load the impersonator's credential signing secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Info("Loading credential signing certificate for impersonation proxy",
|
||||||
|
"certPEM", string(certPEM),
|
||||||
|
"fromSecret", c.impersonationSignerSecretName,
|
||||||
|
"namespace", c.namespace)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) clearSignerCA() {
|
||||||
|
plog.Info("Clearing credential signing certificate for impersonation proxy")
|
||||||
|
c.impersonationSigningCertProvider.UnsetCertKeyContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, config *impersonator.Config, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy {
|
||||||
|
switch {
|
||||||
|
case c.disabledExplicitly(config):
|
||||||
|
return &v1alpha1.CredentialIssuerStrategy{
|
||||||
|
Type: v1alpha1.ImpersonationProxyStrategyType,
|
||||||
|
Status: v1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: v1alpha1.DisabledStrategyReason,
|
||||||
|
Message: "impersonation proxy was explicitly disabled by configuration",
|
||||||
|
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||||
|
}
|
||||||
|
case c.disabledByAutoMode(config):
|
||||||
|
return &v1alpha1.CredentialIssuerStrategy{
|
||||||
|
Type: v1alpha1.ImpersonationProxyStrategyType,
|
||||||
|
Status: v1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: v1alpha1.DisabledStrategyReason,
|
||||||
|
Message: "automatically determined that impersonation proxy should be disabled",
|
||||||
|
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||||
|
}
|
||||||
|
case !nameInfo.ready:
|
||||||
|
return &v1alpha1.CredentialIssuerStrategy{
|
||||||
|
Type: v1alpha1.ImpersonationProxyStrategyType,
|
||||||
|
Status: v1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: v1alpha1.PendingStrategyReason,
|
||||||
|
Message: "waiting for load balancer Service to be assigned IP or hostname",
|
||||||
|
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return &v1alpha1.CredentialIssuerStrategy{
|
||||||
|
Type: v1alpha1.ImpersonationProxyStrategyType,
|
||||||
|
Status: v1alpha1.SuccessStrategyStatus,
|
||||||
|
Reason: v1alpha1.ListeningStrategyReason,
|
||||||
|
Message: "impersonation proxy is ready to accept client connections",
|
||||||
|
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||||
|
Frontend: &v1alpha1.CredentialIssuerFrontend{
|
||||||
|
Type: v1alpha1.ImpersonationProxyFrontendType,
|
||||||
|
ImpersonationProxyInfo: &v1alpha1.ImpersonationProxyInfo{
|
||||||
|
Endpoint: "https://" + nameInfo.clientEndpoint,
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString(ca.Bundle()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2494
internal/controller/impersonatorconfig/impersonator_config_test.go
Normal file
2494
internal/controller/impersonatorconfig/impersonator_config_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -52,9 +52,21 @@ func mergeStrategy(configToUpdate *v1alpha1.CredentialIssuerStatus, strategy v1a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: sort strategies by server preference rather than alphanumerically by type.
|
// weights are a set of priorities for each strategy type.
|
||||||
|
//nolint: gochecknoglobals
|
||||||
|
var weights = map[v1alpha1.StrategyType]int{
|
||||||
|
v1alpha1.KubeClusterSigningCertificateStrategyType: 2, // most preferred strategy
|
||||||
|
v1alpha1.ImpersonationProxyStrategyType: 1,
|
||||||
|
// unknown strategy types will have weight 0 by default
|
||||||
|
}
|
||||||
|
|
||||||
type sortableStrategies []v1alpha1.CredentialIssuerStrategy
|
type sortableStrategies []v1alpha1.CredentialIssuerStrategy
|
||||||
|
|
||||||
func (s sortableStrategies) Len() int { return len(s) }
|
func (s sortableStrategies) Len() int { return len(s) }
|
||||||
func (s sortableStrategies) Less(i, j int) bool { return s[i].Type < s[j].Type }
|
func (s sortableStrategies) Less(i, j int) bool {
|
||||||
func (s sortableStrategies) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
if wi, wj := weights[s[i].Type], weights[s[j].Type]; wi != wj {
|
||||||
|
return wi > wj
|
||||||
|
}
|
||||||
|
return s[i].Type < s[j].Type
|
||||||
|
}
|
||||||
|
func (s sortableStrategies) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
@ -4,9 +4,13 @@
|
|||||||
package issuerconfig
|
package issuerconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/quick"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
@ -185,3 +189,30 @@ func TestMergeStrategy(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStrategySorting(t *testing.T) {
|
||||||
|
expected := []v1alpha1.CredentialIssuerStrategy{
|
||||||
|
{Type: v1alpha1.KubeClusterSigningCertificateStrategyType},
|
||||||
|
{Type: v1alpha1.ImpersonationProxyStrategyType},
|
||||||
|
{Type: "Type1"},
|
||||||
|
{Type: "Type2"},
|
||||||
|
{Type: "Type3"},
|
||||||
|
}
|
||||||
|
require.NoError(t, quick.Check(func(seed int64) bool {
|
||||||
|
// Create a randomly shuffled copy of the expected output.
|
||||||
|
//nolint:gosec // this is not meant to be a secure random, just a seeded RNG for shuffling deterministically
|
||||||
|
rng := rand.New(rand.NewSource(seed))
|
||||||
|
output := make([]v1alpha1.CredentialIssuerStrategy, len(expected))
|
||||||
|
copy(output, expected)
|
||||||
|
rng.Shuffle(
|
||||||
|
len(output),
|
||||||
|
func(i, j int) { output[i], output[j] = output[j], output[i] },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort it using the code under test.
|
||||||
|
sort.Stable(sortableStrategies(output))
|
||||||
|
|
||||||
|
// Assert that it's sorted back to the expected output order.
|
||||||
|
return assert.Equal(t, expected, output)
|
||||||
|
}, nil))
|
||||||
|
}
|
||||||
|
@ -79,8 +79,8 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
|||||||
var agentInformerClient *kubernetesfake.Clientset
|
var agentInformerClient *kubernetesfake.Clientset
|
||||||
var agentInformers kubeinformers.SharedInformerFactory
|
var agentInformers kubeinformers.SharedInformerFactory
|
||||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
var controllerManagerPod, agentPod *corev1.Pod
|
var controllerManagerPod, agentPod *corev1.Pod
|
||||||
var podsGVR schema.GroupVersionResource
|
var podsGVR schema.GroupVersionResource
|
||||||
@ -116,7 +116,7 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: kubeSystemNamespace,
|
Namespace: kubeSystemNamespace,
|
||||||
@ -125,8 +125,8 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeSystemInformers.Start(timeoutContext.Done())
|
kubeSystemInformers.Start(cancelContext.Done())
|
||||||
agentInformers.Start(timeoutContext.Done())
|
agentInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
|
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
|
||||||
kubeSystemNamespace, agentPodNamespace, certPath, keyPath,
|
kubeSystemNamespace, agentPodNamespace, certPath, keyPath,
|
||||||
@ -173,7 +173,7 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is an agent pod without annotations set", func() {
|
when("there is an agent pod without annotations set", func() {
|
||||||
|
@ -94,8 +94,8 @@ func TestCreaterControllerSync(t *testing.T) {
|
|||||||
var agentInformerClient *kubernetesfake.Clientset
|
var agentInformerClient *kubernetesfake.Clientset
|
||||||
var agentInformers kubeinformers.SharedInformerFactory
|
var agentInformers kubeinformers.SharedInformerFactory
|
||||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
var controllerManagerPod, agentPod *corev1.Pod
|
var controllerManagerPod, agentPod *corev1.Pod
|
||||||
var podsGVR schema.GroupVersionResource
|
var podsGVR schema.GroupVersionResource
|
||||||
@ -135,7 +135,7 @@ func TestCreaterControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: kubeSystemNamespace,
|
Namespace: kubeSystemNamespace,
|
||||||
@ -144,8 +144,8 @@ func TestCreaterControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeSystemInformers.Start(timeoutContext.Done())
|
kubeSystemInformers.Start(cancelContext.Done())
|
||||||
agentInformers.Start(timeoutContext.Done())
|
agentInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ func TestCreaterControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
|
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
|
||||||
kubeSystemNamespace, agentPodNamespace, "ignored for this test", "ignored for this test",
|
kubeSystemNamespace, agentPodNamespace, "ignored for this test", "ignored for this test",
|
||||||
@ -201,7 +201,7 @@ func TestCreaterControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is a controller manager pod", func() {
|
when("there is a controller manager pod", func() {
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package kubecertagent
|
package kubecertagent
|
||||||
@ -6,7 +6,6 @@ package kubecertagent
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/sclevine/spec/report"
|
"github.com/sclevine/spec/report"
|
||||||
@ -57,8 +56,8 @@ func TestDeleterControllerSync(t *testing.T) {
|
|||||||
var kubeSystemInformers kubeinformers.SharedInformerFactory
|
var kubeSystemInformers kubeinformers.SharedInformerFactory
|
||||||
var agentInformerClient *kubernetesfake.Clientset
|
var agentInformerClient *kubernetesfake.Clientset
|
||||||
var agentInformers kubeinformers.SharedInformerFactory
|
var agentInformers kubeinformers.SharedInformerFactory
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
var controllerManagerPod, agentPod *corev1.Pod
|
var controllerManagerPod, agentPod *corev1.Pod
|
||||||
var podsGVR schema.GroupVersionResource
|
var podsGVR schema.GroupVersionResource
|
||||||
@ -85,7 +84,7 @@ func TestDeleterControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: kubeSystemNamespace,
|
Namespace: kubeSystemNamespace,
|
||||||
@ -94,8 +93,8 @@ func TestDeleterControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeSystemInformers.Start(timeoutContext.Done())
|
kubeSystemInformers.Start(cancelContext.Done())
|
||||||
agentInformers.Start(timeoutContext.Done())
|
agentInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +108,7 @@ func TestDeleterControllerSync(t *testing.T) {
|
|||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||||
|
|
||||||
@ -139,7 +138,7 @@ func TestDeleterControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is an agent pod", func() {
|
when("there is an agent pod", func() {
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
"k8s.io/apimachinery/pkg/util/clock"
|
||||||
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"k8s.io/klog/v2"
|
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
@ -33,7 +33,7 @@ type execerController struct {
|
|||||||
credentialIssuerLocationConfig *CredentialIssuerLocationConfig
|
credentialIssuerLocationConfig *CredentialIssuerLocationConfig
|
||||||
credentialIssuerLabels map[string]string
|
credentialIssuerLabels map[string]string
|
||||||
discoveryURLOverride *string
|
discoveryURLOverride *string
|
||||||
dynamicCertProvider dynamiccert.Provider
|
dynamicCertProvider dynamiccert.Private
|
||||||
podCommandExecutor PodCommandExecutor
|
podCommandExecutor PodCommandExecutor
|
||||||
clock clock.Clock
|
clock clock.Clock
|
||||||
pinnipedAPIClient pinnipedclientset.Interface
|
pinnipedAPIClient pinnipedclientset.Interface
|
||||||
@ -51,7 +51,7 @@ func NewExecerController(
|
|||||||
credentialIssuerLocationConfig *CredentialIssuerLocationConfig,
|
credentialIssuerLocationConfig *CredentialIssuerLocationConfig,
|
||||||
credentialIssuerLabels map[string]string,
|
credentialIssuerLabels map[string]string,
|
||||||
discoveryURLOverride *string,
|
discoveryURLOverride *string,
|
||||||
dynamicCertProvider dynamiccert.Provider,
|
dynamicCertProvider dynamiccert.Private,
|
||||||
podCommandExecutor PodCommandExecutor,
|
podCommandExecutor PodCommandExecutor,
|
||||||
pinnipedAPIClient pinnipedclientset.Interface,
|
pinnipedAPIClient pinnipedclientset.Interface,
|
||||||
clock clock.Clock,
|
clock clock.Clock,
|
||||||
@ -119,8 +119,7 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
|
|||||||
c.pinnipedAPIClient,
|
c.pinnipedAPIClient,
|
||||||
strategyError(c.clock, err),
|
strategyError(c.clock, err),
|
||||||
)
|
)
|
||||||
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success")
|
return newAggregate(err, strategyResultUpdateErr)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keyPEM, err := c.podCommandExecutor.Exec(agentPod.Namespace, agentPod.Name, "cat", keyPath)
|
keyPEM, err := c.podCommandExecutor.Exec(agentPod.Namespace, agentPod.Name, "cat", keyPath)
|
||||||
@ -132,11 +131,20 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
|
|||||||
c.pinnipedAPIClient,
|
c.pinnipedAPIClient,
|
||||||
strategyError(c.clock, err),
|
strategyError(c.clock, err),
|
||||||
)
|
)
|
||||||
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success")
|
return newAggregate(err, strategyResultUpdateErr)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.dynamicCertProvider.Set([]byte(certPEM), []byte(keyPEM))
|
if err := c.dynamicCertProvider.SetCertKeyContent([]byte(certPEM), []byte(keyPEM)); err != nil {
|
||||||
|
err = fmt.Errorf("failed to set signing cert/key content from agent pod %s/%s: %w", agentPod.Namespace, agentPod.Name, err)
|
||||||
|
strategyResultUpdateErr := issuerconfig.UpdateStrategy(
|
||||||
|
ctx.Context,
|
||||||
|
c.credentialIssuerLocationConfig.Name,
|
||||||
|
c.credentialIssuerLabels,
|
||||||
|
c.pinnipedAPIClient,
|
||||||
|
strategyError(c.clock, err),
|
||||||
|
)
|
||||||
|
return newAggregate(err, strategyResultUpdateErr)
|
||||||
|
}
|
||||||
|
|
||||||
apiInfo, err := c.getTokenCredentialRequestAPIInfo()
|
apiInfo, err := c.getTokenCredentialRequestAPIInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -153,8 +161,7 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
|
|||||||
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success")
|
return newAggregate(err, strategyResultUpdateErr)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return issuerconfig.UpdateStrategy(
|
return issuerconfig.UpdateStrategy(
|
||||||
@ -219,3 +226,7 @@ func (c *execerController) getKeypairFilePaths(pod *v1.Pod) (string, string) {
|
|||||||
|
|
||||||
return certPath, keyPath
|
return certPath, keyPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newAggregate(errs ...error) error {
|
||||||
|
return errors.NewAggregate(errs)
|
||||||
|
}
|
||||||
|
@ -132,6 +132,7 @@ func (s *fakePodExecutor) Exec(podNamespace string, podName string, commandAndAr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManagerControllerSync(t *testing.T) {
|
func TestManagerControllerSync(t *testing.T) {
|
||||||
|
name := t.Name()
|
||||||
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
|
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
const agentPodNamespace = "some-namespace"
|
const agentPodNamespace = "some-namespace"
|
||||||
const agentPodName = "some-agent-pod-name-123"
|
const agentPodName = "some-agent-pod-name-123"
|
||||||
@ -139,15 +140,13 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
const keyPathAnnotationName = "kube-cert-agent.pinniped.dev/key-path"
|
const keyPathAnnotationName = "kube-cert-agent.pinniped.dev/key-path"
|
||||||
const fakeCertPath = "/some/cert/path"
|
const fakeCertPath = "/some/cert/path"
|
||||||
const fakeKeyPath = "/some/key/path"
|
const fakeKeyPath = "/some/key/path"
|
||||||
const defaultDynamicCertProviderCert = "initial-cert"
|
|
||||||
const defaultDynamicCertProviderKey = "initial-key"
|
|
||||||
const credentialIssuerResourceName = "ci-resource-name"
|
const credentialIssuerResourceName = "ci-resource-name"
|
||||||
|
|
||||||
var r *require.Assertions
|
var r *require.Assertions
|
||||||
|
|
||||||
var subject controllerlib.Controller
|
var subject controllerlib.Controller
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||||
var kubeInformerFactory kubeinformers.SharedInformerFactory
|
var kubeInformerFactory kubeinformers.SharedInformerFactory
|
||||||
@ -159,6 +158,8 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
var fakeCertPEM, fakeKeyPEM string
|
var fakeCertPEM, fakeKeyPEM string
|
||||||
var credentialIssuerGVR schema.GroupVersionResource
|
var credentialIssuerGVR schema.GroupVersionResource
|
||||||
var frozenNow time.Time
|
var frozenNow time.Time
|
||||||
|
var defaultDynamicCertProviderCert string
|
||||||
|
var defaultDynamicCertProviderKey string
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
// nested Before's can keep adding things to the informer caches.
|
// nested Before's can keep adding things to the informer caches.
|
||||||
@ -181,7 +182,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: agentPodNamespace,
|
Namespace: agentPodNamespace,
|
||||||
@ -190,7 +191,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformerFactory.Start(timeoutContext.Done())
|
kubeInformerFactory.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,14 +229,23 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
crt, key, err := testutil.CreateCertificate(
|
||||||
|
time.Now().Add(-time.Hour),
|
||||||
|
time.Now().Add(time.Hour),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defaultDynamicCertProviderCert = string(crt)
|
||||||
|
defaultDynamicCertProviderKey = string(key)
|
||||||
|
|
||||||
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
||||||
kubeClientset = kubernetesfake.NewSimpleClientset()
|
kubeClientset = kubernetesfake.NewSimpleClientset()
|
||||||
kubeInformerFactory = kubeinformers.NewSharedInformerFactory(kubeClientset, 0)
|
kubeInformerFactory = kubeinformers.NewSharedInformerFactory(kubeClientset, 0)
|
||||||
fakeExecutor = &fakePodExecutor{r: r}
|
fakeExecutor = &fakePodExecutor{r: r}
|
||||||
frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
|
frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
|
||||||
dynamicCertProvider = dynamiccert.New()
|
dynamicCertProvider = dynamiccert.NewCA(name)
|
||||||
dynamicCertProvider.Set([]byte(defaultDynamicCertProviderCert), []byte(defaultDynamicCertProviderKey))
|
err = dynamicCertProvider.SetCertKeyContent([]byte(defaultDynamicCertProviderCert), []byte(defaultDynamicCertProviderKey))
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
loadFile := func(filename string) string {
|
loadFile := func(filename string) string {
|
||||||
bytes, err := ioutil.ReadFile(filename)
|
bytes, err := ioutil.ReadFile(filename)
|
||||||
@ -253,7 +263,7 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there is not yet any agent pods or they were deleted", func() {
|
when("there is not yet any agent pods or they were deleted", func() {
|
||||||
@ -669,6 +679,55 @@ func TestManagerControllerSync(t *testing.T) {
|
|||||||
r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions())
|
r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
when("the third resulting pod exec has invalid key data", func() {
|
||||||
|
var keyParseErrorMessage string
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
keyParseErrorMessage = "failed to set signing cert/key content from agent pod some-namespace/some-agent-pod-name-123: TestManagerControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in key input"
|
||||||
|
fakeExecutor.errorsToReturn = []error{nil, nil}
|
||||||
|
fakeExecutor.resultsToReturn = []string{fakeCertPEM, ""}
|
||||||
|
startInformersAndController()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not update the dynamic certificates provider", func() {
|
||||||
|
r.EqualError(controllerlib.TestSync(t, subject, *syncContext), keyParseErrorMessage)
|
||||||
|
requireDynamicCertProviderHasDefaultValues()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates or updates the the CredentialIssuer status field with an error", func() {
|
||||||
|
r.EqualError(controllerlib.TestSync(t, subject, *syncContext), keyParseErrorMessage)
|
||||||
|
|
||||||
|
expectedCreateCredentialIssuer := &configv1alpha1.CredentialIssuer{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: credentialIssuerResourceName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedCredentialIssuer := &configv1alpha1.CredentialIssuer{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: credentialIssuerResourceName,
|
||||||
|
},
|
||||||
|
Status: configv1alpha1.CredentialIssuerStatus{
|
||||||
|
Strategies: []configv1alpha1.CredentialIssuerStrategy{
|
||||||
|
{
|
||||||
|
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
|
||||||
|
Status: configv1alpha1.ErrorStrategyStatus,
|
||||||
|
Reason: configv1alpha1.CouldNotFetchKeyStrategyReason,
|
||||||
|
Message: keyParseErrorMessage,
|
||||||
|
LastUpdateTime: metav1.NewTime(frozenNow),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedGetAction := coretesting.NewRootGetAction(credentialIssuerGVR, credentialIssuerResourceName)
|
||||||
|
expectedCreateAction := coretesting.NewRootCreateAction(credentialIssuerGVR, expectedCreateCredentialIssuer)
|
||||||
|
expectedUpdateAction := coretesting.NewRootUpdateSubresourceAction(credentialIssuerGVR, "status", expectedCredentialIssuer)
|
||||||
|
r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
}
|
}
|
||||||
|
@ -103,8 +103,8 @@ func TestSync(t *testing.T) {
|
|||||||
var federationDomainInformerClient *pinnipedfake.Clientset
|
var federationDomainInformerClient *pinnipedfake.Clientset
|
||||||
var federationDomainInformers pinnipedinformers.SharedInformerFactory
|
var federationDomainInformers pinnipedinformers.SharedInformerFactory
|
||||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||||
var timeoutContext context.Context
|
var cancelContext context.Context
|
||||||
var timeoutContextCancel context.CancelFunc
|
var cancelContextCancelFunc context.CancelFunc
|
||||||
var syncContext *controllerlib.Context
|
var syncContext *controllerlib.Context
|
||||||
var frozenNow time.Time
|
var frozenNow time.Time
|
||||||
var providersSetter *fakeProvidersSetter
|
var providersSetter *fakeProvidersSetter
|
||||||
@ -124,7 +124,7 @@ func TestSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
@ -133,7 +133,7 @@ func TestSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
federationDomainInformers.Start(timeoutContext.Done())
|
federationDomainInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ func TestSync(t *testing.T) {
|
|||||||
providersSetter = &fakeProvidersSetter{}
|
providersSetter = &fakeProvidersSetter{}
|
||||||
frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
|
frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
federationDomainInformerClient = pinnipedfake.NewSimpleClientset()
|
federationDomainInformerClient = pinnipedfake.NewSimpleClientset()
|
||||||
federationDomainInformers = pinnipedinformers.NewSharedInformerFactory(federationDomainInformerClient, 0)
|
federationDomainInformers = pinnipedinformers.NewSharedInformerFactory(federationDomainInformerClient, 0)
|
||||||
@ -157,7 +157,7 @@ func TestSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there are some valid FederationDomains in the informer", func() {
|
when("there are some valid FederationDomains in the informer", func() {
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -630,7 +629,7 @@ func TestFederationDomainSecretsControllerSync(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
|
pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
@ -412,7 +411,7 @@ func TestSupervisorSecretsControllerSync(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
// We cannot currently run this test in parallel since it uses the global generateKey function.
|
// We cannot currently run this test in parallel since it uses the global generateKey function.
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if test.generateKey != nil {
|
if test.generateKey != nil {
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/sclevine/spec/report"
|
"github.com/sclevine/spec/report"
|
||||||
@ -124,16 +123,16 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
const installedInNamespace = "some-namespace"
|
const installedInNamespace = "some-namespace"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
r *require.Assertions
|
r *require.Assertions
|
||||||
subject controllerlib.Controller
|
subject controllerlib.Controller
|
||||||
pinnipedInformerClient *pinnipedfake.Clientset
|
pinnipedInformerClient *pinnipedfake.Clientset
|
||||||
kubeInformerClient *kubernetesfake.Clientset
|
kubeInformerClient *kubernetesfake.Clientset
|
||||||
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||||
kubeInformers kubeinformers.SharedInformerFactory
|
kubeInformers kubeinformers.SharedInformerFactory
|
||||||
timeoutContext context.Context
|
cancelContext context.Context
|
||||||
timeoutContextCancel context.CancelFunc
|
cancelContextCancelFunc context.CancelFunc
|
||||||
syncContext *controllerlib.Context
|
syncContext *controllerlib.Context
|
||||||
issuerToJWKSSetter *fakeIssuerToJWKSMapSetter
|
issuerToJWKSSetter *fakeIssuerToJWKSMapSetter
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
@ -149,7 +148,7 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
@ -158,15 +157,15 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformers.Start(timeoutContext.Done())
|
kubeInformers.Start(cancelContext.Done())
|
||||||
pinnipedInformers.Start(timeoutContext.Done())
|
pinnipedInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||||
@ -184,7 +183,7 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there are no FederationDomains and no JWKS Secrets yet", func() {
|
when("there are no FederationDomains and no JWKS Secrets yet", func() {
|
||||||
|
@ -12,7 +12,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@ -674,7 +673,7 @@ func TestJWKSWriterControllerSync(t *testing.T) {
|
|||||||
return goodKey, test.generateKeyErr
|
return goodKey, test.generateKeyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/sclevine/spec/report"
|
"github.com/sclevine/spec/report"
|
||||||
@ -130,16 +129,16 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
r *require.Assertions
|
r *require.Assertions
|
||||||
subject controllerlib.Controller
|
subject controllerlib.Controller
|
||||||
pinnipedInformerClient *pinnipedfake.Clientset
|
pinnipedInformerClient *pinnipedfake.Clientset
|
||||||
kubeInformerClient *kubernetesfake.Clientset
|
kubeInformerClient *kubernetesfake.Clientset
|
||||||
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||||
kubeInformers kubeinformers.SharedInformerFactory
|
kubeInformers kubeinformers.SharedInformerFactory
|
||||||
timeoutContext context.Context
|
cancelContext context.Context
|
||||||
timeoutContextCancel context.CancelFunc
|
cancelContextCancelFunc context.CancelFunc
|
||||||
syncContext *controllerlib.Context
|
syncContext *controllerlib.Context
|
||||||
issuerTLSCertSetter *fakeIssuerTLSCertSetter
|
issuerTLSCertSetter *fakeIssuerTLSCertSetter
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
@ -156,7 +155,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
@ -165,8 +164,8 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformers.Start(timeoutContext.Done())
|
kubeInformers.Start(cancelContext.Done())
|
||||||
pinnipedInformers.Start(timeoutContext.Done())
|
pinnipedInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +178,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
|||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||||
@ -197,7 +196,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there are no FederationDomains and no TLS Secrets yet", func() {
|
when("there are no FederationDomains and no TLS Secrets yet", func() {
|
||||||
|
@ -624,7 +624,7 @@ func TestController(t *testing.T) {
|
|||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
pinnipedInformers.Start(ctx.Done())
|
pinnipedInformers.Start(ctx.Done())
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package supervisorstorage
|
package supervisorstorage
|
||||||
@ -108,16 +108,16 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
r *require.Assertions
|
r *require.Assertions
|
||||||
subject controllerlib.Controller
|
subject controllerlib.Controller
|
||||||
kubeInformerClient *kubernetesfake.Clientset
|
kubeInformerClient *kubernetesfake.Clientset
|
||||||
kubeClient *kubernetesfake.Clientset
|
kubeClient *kubernetesfake.Clientset
|
||||||
kubeInformers kubeinformers.SharedInformerFactory
|
kubeInformers kubeinformers.SharedInformerFactory
|
||||||
timeoutContext context.Context
|
cancelContext context.Context
|
||||||
timeoutContextCancel context.CancelFunc
|
cancelContextCancelFunc context.CancelFunc
|
||||||
syncContext *controllerlib.Context
|
syncContext *controllerlib.Context
|
||||||
fakeClock *clock.FakeClock
|
fakeClock *clock.FakeClock
|
||||||
frozenNow time.Time
|
frozenNow time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defer starting the informers until the last possible moment so that the
|
// Defer starting the informers until the last possible moment so that the
|
||||||
@ -133,7 +133,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
// Set this at the last second to support calling subject.Name().
|
// Set this at the last second to support calling subject.Name().
|
||||||
syncContext = &controllerlib.Context{
|
syncContext = &controllerlib.Context{
|
||||||
Context: timeoutContext,
|
Context: cancelContext,
|
||||||
Name: subject.Name(),
|
Name: subject.Name(),
|
||||||
Key: controllerlib.Key{
|
Key: controllerlib.Key{
|
||||||
Namespace: "",
|
Namespace: "",
|
||||||
@ -142,14 +142,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must start informers before calling TestRunSynchronously()
|
// Must start informers before calling TestRunSynchronously()
|
||||||
kubeInformers.Start(timeoutContext.Done())
|
kubeInformers.Start(cancelContext.Done())
|
||||||
controllerlib.TestRunSynchronously(t, subject)
|
controllerlib.TestRunSynchronously(t, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
|
|
||||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeClient = kubernetesfake.NewSimpleClientset()
|
kubeClient = kubernetesfake.NewSimpleClientset()
|
||||||
@ -168,7 +168,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
timeoutContextCancel()
|
cancelContextCancelFunc()
|
||||||
})
|
})
|
||||||
|
|
||||||
when("there are secrets without the garbage-collect-after annotation", func() {
|
when("there are secrets without the garbage-collect-after annotation", func() {
|
||||||
|
@ -18,12 +18,14 @@ import (
|
|||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
|
pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
|
||||||
"go.pinniped.dev/internal/apiserviceref"
|
"go.pinniped.dev/internal/apiserviceref"
|
||||||
|
"go.pinniped.dev/internal/concierge/impersonator"
|
||||||
"go.pinniped.dev/internal/config/concierge"
|
"go.pinniped.dev/internal/config/concierge"
|
||||||
"go.pinniped.dev/internal/controller/apicerts"
|
"go.pinniped.dev/internal/controller/apicerts"
|
||||||
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
||||||
"go.pinniped.dev/internal/controller/authenticator/cachecleaner"
|
"go.pinniped.dev/internal/controller/authenticator/cachecleaner"
|
||||||
"go.pinniped.dev/internal/controller/authenticator/jwtcachefiller"
|
"go.pinniped.dev/internal/controller/authenticator/jwtcachefiller"
|
||||||
"go.pinniped.dev/internal/controller/authenticator/webhookcachefiller"
|
"go.pinniped.dev/internal/controller/authenticator/webhookcachefiller"
|
||||||
|
"go.pinniped.dev/internal/controller/impersonatorconfig"
|
||||||
"go.pinniped.dev/internal/controller/kubecertagent"
|
"go.pinniped.dev/internal/controller/kubecertagent"
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
"go.pinniped.dev/internal/deploymentref"
|
"go.pinniped.dev/internal/deploymentref"
|
||||||
@ -61,13 +63,23 @@ type Config struct {
|
|||||||
DiscoveryURLOverride *string
|
DiscoveryURLOverride *string
|
||||||
|
|
||||||
// DynamicServingCertProvider provides a setter and a getter to the Pinniped API's serving cert.
|
// DynamicServingCertProvider provides a setter and a getter to the Pinniped API's serving cert.
|
||||||
DynamicServingCertProvider dynamiccert.Provider
|
DynamicServingCertProvider dynamiccert.Private
|
||||||
|
|
||||||
// DynamicSigningCertProvider provides a setter and a getter to the Pinniped API's
|
// DynamicSigningCertProvider provides a setter and a getter to the Pinniped API's
|
||||||
// signing cert, i.e., the cert that it uses to sign certs for Pinniped clients wishing to login.
|
// signing cert, i.e., the cert that it uses to sign certs for Pinniped clients wishing to login.
|
||||||
DynamicSigningCertProvider dynamiccert.Provider
|
// This is filled with the Kube API server's signing cert by a controller, if it can be found.
|
||||||
|
DynamicSigningCertProvider dynamiccert.Private
|
||||||
|
|
||||||
|
// ImpersonationSigningCertProvider provides a setter and a getter to the CA cert that should be
|
||||||
|
// used to sign client certs for authentication to the impersonation proxy. This CA is used by
|
||||||
|
// the TokenCredentialRequest to sign certs and by the impersonation proxy to check certs.
|
||||||
|
// When the impersonation proxy is not running, the getter will return nil cert and nil key.
|
||||||
|
// (Note that the impersonation proxy also accepts client certs signed by the Kube API server's cert.)
|
||||||
|
ImpersonationSigningCertProvider dynamiccert.Provider
|
||||||
|
|
||||||
// ServingCertDuration is the validity period, in seconds, of the API serving certificate.
|
// ServingCertDuration is the validity period, in seconds, of the API serving certificate.
|
||||||
ServingCertDuration time.Duration
|
ServingCertDuration time.Duration
|
||||||
|
|
||||||
// ServingCertRenewBefore is the period of time, in seconds, that pinniped will wait before
|
// ServingCertRenewBefore is the period of time, in seconds, that pinniped will wait before
|
||||||
// rotating the serving certificate. This period of time starts upon issuance of the serving
|
// rotating the serving certificate. This period of time starts upon issuance of the serving
|
||||||
// certificate.
|
// certificate.
|
||||||
@ -179,6 +191,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
|||||||
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
c.ServingCertRenewBefore,
|
c.ServingCertRenewBefore,
|
||||||
|
apicerts.TLSCertificateChainSecretKey,
|
||||||
),
|
),
|
||||||
singletonWorker,
|
singletonWorker,
|
||||||
).
|
).
|
||||||
@ -266,6 +279,58 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
|||||||
klogr.New(),
|
klogr.New(),
|
||||||
),
|
),
|
||||||
singletonWorker,
|
singletonWorker,
|
||||||
|
).
|
||||||
|
|
||||||
|
// The impersonator configuration controller dynamically configures the impersonation proxy feature.
|
||||||
|
WithController(
|
||||||
|
impersonatorconfig.NewImpersonatorConfigController(
|
||||||
|
c.ServerInstallationInfo.Namespace,
|
||||||
|
c.NamesConfig.ImpersonationConfigMap,
|
||||||
|
c.NamesConfig.CredentialIssuer,
|
||||||
|
client.Kubernetes,
|
||||||
|
client.PinnipedConcierge,
|
||||||
|
informers.installationNamespaceK8s.Core().V1().ConfigMaps(),
|
||||||
|
informers.installationNamespaceK8s.Core().V1().Services(),
|
||||||
|
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||||
|
controllerlib.WithInformer,
|
||||||
|
controllerlib.WithInitialEvent,
|
||||||
|
c.NamesConfig.ImpersonationLoadBalancerService,
|
||||||
|
c.NamesConfig.ImpersonationTLSCertificateSecret,
|
||||||
|
c.NamesConfig.ImpersonationCACertificateSecret,
|
||||||
|
c.Labels,
|
||||||
|
clock.RealClock{},
|
||||||
|
impersonator.New,
|
||||||
|
c.NamesConfig.ImpersonationSignerSecret,
|
||||||
|
c.ImpersonationSigningCertProvider,
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
|
).
|
||||||
|
WithController(
|
||||||
|
apicerts.NewCertsManagerController(
|
||||||
|
c.ServerInstallationInfo.Namespace,
|
||||||
|
c.NamesConfig.ImpersonationSignerSecret,
|
||||||
|
c.Labels,
|
||||||
|
client.Kubernetes,
|
||||||
|
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||||
|
controllerlib.WithInformer,
|
||||||
|
controllerlib.WithInitialEvent,
|
||||||
|
365*24*time.Hour, // 1 year hard coded value
|
||||||
|
"Pinniped Impersonation Proxy CA",
|
||||||
|
"", // optional, means do not give me a serving cert
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
|
).
|
||||||
|
WithController(
|
||||||
|
apicerts.NewCertsExpirerController(
|
||||||
|
c.ServerInstallationInfo.Namespace,
|
||||||
|
c.NamesConfig.ImpersonationSignerSecret,
|
||||||
|
client.Kubernetes,
|
||||||
|
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||||
|
controllerlib.WithInformer,
|
||||||
|
c.ServingCertRenewBefore,
|
||||||
|
apicerts.CACertificateSecretKey,
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a function which starts the informers and controllers.
|
// Return a function which starts the informers and controllers.
|
||||||
|
@ -1,45 +1,153 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package dynamiccert
|
package dynamiccert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider provides a getter, CurrentCertKeyContent(), and a setter, Set(), for a PEM-formatted
|
|
||||||
// certificate and matching key.
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
dynamiccertificates.CertKeyContentProvider
|
Private
|
||||||
Set(certPEM, keyPEM []byte)
|
Public
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Private interface {
|
||||||
|
dynamiccertificates.CertKeyContentProvider
|
||||||
|
SetCertKeyContent(certPEM, keyPEM []byte) error
|
||||||
|
UnsetCertKeyContent()
|
||||||
|
|
||||||
|
notifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type Public interface {
|
||||||
|
dynamiccertificates.CAContentProvider
|
||||||
|
|
||||||
|
notifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type notifier interface {
|
||||||
|
dynamiccertificates.Notifier
|
||||||
|
dynamiccertificates.ControllerRunner // we do not need this today, but it could grow and change in the future
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Provider = &provider{}
|
||||||
|
|
||||||
type provider struct {
|
type provider struct {
|
||||||
certPEM []byte
|
// these fields are constant after struct initialization and thus do not need locking
|
||||||
keyPEM []byte
|
name string
|
||||||
mutex sync.RWMutex
|
isCA bool
|
||||||
|
|
||||||
|
// mutex guards all the fields below it
|
||||||
|
mutex sync.RWMutex
|
||||||
|
certPEM []byte
|
||||||
|
keyPEM []byte
|
||||||
|
listeners []dynamiccertificates.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns an empty Provider. The returned Provider is thread-safe.
|
// NewServingCert returns a Private that is go routine safe.
|
||||||
func New() Provider {
|
// It can only hold key pairs that have IsCA=false.
|
||||||
return &provider{}
|
func NewServingCert(name string) Private {
|
||||||
|
return &provider{name: name}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *provider) Set(certPEM, keyPEM []byte) {
|
// NewCA returns a Provider that is go routine safe.
|
||||||
p.mutex.Lock() // acquire a write lock
|
// It can only hold key pairs that have IsCA=true.
|
||||||
defer p.mutex.Unlock()
|
func NewCA(name string) Provider {
|
||||||
p.certPEM = certPEM
|
return &provider{name: name, isCA: true}
|
||||||
p.keyPEM = keyPEM
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *provider) Name() string {
|
func (p *provider) Name() string {
|
||||||
return "DynamicCertProvider"
|
return p.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *provider) CurrentCertKeyContent() (cert []byte, key []byte) {
|
func (p *provider) CurrentCertKeyContent() (cert []byte, key []byte) {
|
||||||
p.mutex.RLock() // acquire a read lock
|
p.mutex.RLock()
|
||||||
defer p.mutex.RUnlock()
|
defer p.mutex.RUnlock()
|
||||||
|
|
||||||
return p.certPEM, p.keyPEM
|
return p.certPEM, p.keyPEM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *provider) SetCertKeyContent(certPEM, keyPEM []byte) error {
|
||||||
|
// always make sure that we have valid PEM data, otherwise
|
||||||
|
// dynamiccertificates.NewUnionCAContentProvider.VerifyOptions will panic
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: attempt to set invalid key pair: %w", p.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// these checks should always pass if tls.X509KeyPair did not error
|
||||||
|
if len(cert.Certificate) == 0 {
|
||||||
|
return fmt.Errorf("%s: key pair has empty cert slice", p.name)
|
||||||
|
}
|
||||||
|
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: failed to parse key pair as x509 cert: %w", p.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm that we are not trying to use a CA as a serving cert and vice versa
|
||||||
|
if p.isCA != x509Cert.IsCA {
|
||||||
|
return fmt.Errorf("%s: attempt to set x509 cert with unexpected IsCA=%v", p.name, x509Cert.IsCA)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setCertKeyContent(certPEM, keyPEM)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) UnsetCertKeyContent() {
|
||||||
|
p.setCertKeyContent(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) setCertKeyContent(certPEM, keyPEM []byte) {
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
|
p.certPEM = certPEM
|
||||||
|
p.keyPEM = keyPEM
|
||||||
|
|
||||||
|
// technically this only reads a read lock but we already have the write lock
|
||||||
|
for _, listener := range p.listeners {
|
||||||
|
listener.Enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) CurrentCABundleContent() []byte {
|
||||||
|
if !p.isCA {
|
||||||
|
panic("*provider from NewServingCert was cast into wrong CA interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, _ := p.CurrentCertKeyContent()
|
||||||
|
return ca
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) VerifyOptions() (x509.VerifyOptions, bool) {
|
||||||
|
if !p.isCA {
|
||||||
|
panic("*provider from NewServingCert was cast into wrong CA interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
plog.Warning("unexpected call to *provider.VerifyOptions; CA union logic is broken")
|
||||||
|
return x509.VerifyOptions{}, false // assume we are unioned via dynamiccertificates.NewUnionCAContentProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) AddListener(listener dynamiccertificates.Listener) {
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
|
p.listeners = append(p.listeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) RunOnce() error {
|
||||||
|
return nil // no-op, but we want to make sure to stay in sync with dynamiccertificates.ControllerRunner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) Run(workers int, stopCh <-chan struct{}) {
|
||||||
|
// no-op, but we want to make sure to stay in sync with dynamiccertificates.ControllerRunner
|
||||||
|
}
|
||||||
|
226
internal/dynamiccert/provider_test.go
Normal file
226
internal/dynamiccert/provider_test.go
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package dynamiccert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/test/library"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProviderWithDynamicServingCertificateController(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
f func(t *testing.T, ca Provider, certKey Private) (wantClientCASubjects [][]byte, wantCerts []tls.Certificate)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-op leave everything alone",
|
||||||
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
ok := pool.AppendCertsFromPEM(ca.CurrentCABundleContent())
|
||||||
|
require.True(t, ok, "should have valid non-empty CA bundle")
|
||||||
|
|
||||||
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return pool.Subjects(), []tls.Certificate{cert}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset the CA",
|
||||||
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
||||||
|
ca.UnsetCertKeyContent()
|
||||||
|
|
||||||
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return nil, []tls.Certificate{cert}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset the serving cert - still serves the old content",
|
||||||
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
ok := pool.AppendCertsFromPEM(ca.CurrentCABundleContent())
|
||||||
|
require.True(t, ok, "should have valid non-empty CA bundle")
|
||||||
|
|
||||||
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
certKey.UnsetCertKeyContent()
|
||||||
|
|
||||||
|
return pool.Subjects(), []tls.Certificate{cert}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change to a new CA",
|
||||||
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
||||||
|
// use unique names for all CAs to make sure the pool subjects are different
|
||||||
|
newCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-ca"), time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
caKey, err := newCA.PrivateKeyToPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ca.SetCertKeyContent(newCA.Bundle(), caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return newCA.Pool().Subjects(), []tls.Certificate{cert}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change to new serving cert",
|
||||||
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
||||||
|
// use unique names for all CAs to make sure the pool subjects are different
|
||||||
|
newCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-ca"), time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := newCA.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.2")}, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = certKey.SetCertKeyContent(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
ok := pool.AppendCertsFromPEM(ca.CurrentCABundleContent())
|
||||||
|
require.True(t, ok, "should have valid non-empty CA bundle")
|
||||||
|
|
||||||
|
return pool.Subjects(), []tls.Certificate{cert}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change both CA and serving cert",
|
||||||
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
||||||
|
// use unique names for all CAs to make sure the pool subjects are different
|
||||||
|
newCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-ca"), time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := newCA.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.3")}, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = certKey.SetCertKeyContent(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// use unique names for all CAs to make sure the pool subjects are different
|
||||||
|
newOtherCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-other-ca"), time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
caKey, err := newOtherCA.PrivateKeyToPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ca.SetCertKeyContent(newOtherCA.Bundle(), caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return newOtherCA.Pool().Subjects(), []tls.Certificate{cert}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// use unique names for all CAs to make sure the pool subjects are different
|
||||||
|
ca, err := certauthority.New(names.SimpleNameGenerator.GenerateName("ca"), time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
caKey, err := ca.PrivateKeyToPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
caContent := NewCA("ca")
|
||||||
|
err = caContent.SetCertKeyContent(ca.Bundle(), caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cert, key, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
certKeyContent := NewServingCert("cert-key")
|
||||||
|
err = certKeyContent.SetCertKeyContent(cert, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
ClientAuth: tls.RequestClientCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicCertificateController := dynamiccertificates.NewDynamicServingCertificateController(
|
||||||
|
tlsConfig,
|
||||||
|
caContent,
|
||||||
|
certKeyContent,
|
||||||
|
nil, // we do not care about SNI
|
||||||
|
nil, // we do not care about events
|
||||||
|
)
|
||||||
|
|
||||||
|
caContent.AddListener(dynamicCertificateController)
|
||||||
|
certKeyContent.AddListener(dynamicCertificateController)
|
||||||
|
|
||||||
|
err = dynamicCertificateController.RunOnce()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
defer close(stopCh)
|
||||||
|
go dynamicCertificateController.Run(1, stopCh)
|
||||||
|
|
||||||
|
tlsConfig.GetConfigForClient = dynamicCertificateController.GetConfigForClient
|
||||||
|
|
||||||
|
wantClientCASubjects, wantCerts := tt.f(t, caContent, certKeyContent)
|
||||||
|
|
||||||
|
var lastTLSConfig *tls.Config
|
||||||
|
|
||||||
|
// it will take some time for the controller to catch up
|
||||||
|
err = wait.PollImmediate(time.Second, 30*time.Second, func() (bool, error) {
|
||||||
|
actualTLSConfig, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "force-standard-sni"})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTLSConfig = actualTLSConfig
|
||||||
|
|
||||||
|
return reflect.DeepEqual(wantClientCASubjects, poolSubjects(actualTLSConfig.ClientCAs)) &&
|
||||||
|
reflect.DeepEqual(wantCerts, actualTLSConfig.Certificates), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && lastTLSConfig != nil {
|
||||||
|
// for debugging failures
|
||||||
|
t.Log("diff between client CAs:\n", cmp.Diff(
|
||||||
|
library.Sdump(wantClientCASubjects),
|
||||||
|
library.Sdump(poolSubjects(lastTLSConfig.ClientCAs)),
|
||||||
|
))
|
||||||
|
t.Log("diff between serving certs:\n", cmp.Diff(
|
||||||
|
library.Sdump(wantCerts),
|
||||||
|
library.Sdump(lastTLSConfig.Certificates),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolSubjects(pool *x509.CertPool) [][]byte {
|
||||||
|
if pool == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pool.Subjects()
|
||||||
|
}
|
14
internal/httputil/roundtripper/roundtripper.go
Normal file
14
internal/httputil/roundtripper/roundtripper.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package roundtripper
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
var _ http.RoundTripper = Func(nil)
|
||||||
|
|
||||||
|
type Func func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f Func) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return f(req)
|
||||||
|
}
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package securityheader
|
package securityheader
|
||||||
@ -22,7 +22,7 @@ func TestWrap(t *testing.T) {
|
|||||||
})))
|
})))
|
||||||
t.Cleanup(testServer.Close)
|
t.Cleanup(testServer.Close)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, testServer.URL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, testServer.URL, nil)
|
||||||
|
57
internal/issuer/issuer.go
Normal file
57
internal/issuer/issuer.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package issuer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/constable"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultCertIssuerErr = constable.Error("failed to issue cert")
|
||||||
|
|
||||||
|
type ClientCertIssuer interface {
|
||||||
|
Name() string
|
||||||
|
IssueClientCertPEM(username string, groups []string, ttl time.Duration) (certPEM, keyPEM []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ClientCertIssuer = ClientCertIssuers{}
|
||||||
|
|
||||||
|
type ClientCertIssuers []ClientCertIssuer
|
||||||
|
|
||||||
|
func (c ClientCertIssuers) Name() string {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "empty-client-cert-issuers"
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(c))
|
||||||
|
for _, issuer := range c {
|
||||||
|
names = append(names, issuer.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(names, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ClientCertIssuers) IssueClientCertPEM(username string, groups []string, ttl time.Duration) ([]byte, []byte, error) {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
for _, issuer := range c {
|
||||||
|
certPEM, keyPEM, err := issuer.IssueClientCertPEM(username, groups, ttl)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%s failed to issue client cert: %w", issuer.Name(), err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return certPEM, keyPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := errors.NewAggregate(errs); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, defaultCertIssuerErr
|
||||||
|
}
|
@ -3,61 +3,20 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: go.pinniped.dev/internal/registry/credentialrequest (interfaces: CertIssuer,TokenCredentialRequestAuthenticator)
|
// Source: go.pinniped.dev/internal/registry/credentialrequest (interfaces: TokenCredentialRequestAuthenticator)
|
||||||
|
|
||||||
// Package credentialrequestmocks is a generated GoMock package.
|
// Package credentialrequestmocks is a generated GoMock package.
|
||||||
package credentialrequestmocks
|
package credentialrequestmocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
pkix "crypto/x509/pkix"
|
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
time "time"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
login "go.pinniped.dev/generated/latest/apis/concierge/login"
|
login "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
user "k8s.io/apiserver/pkg/authentication/user"
|
user "k8s.io/apiserver/pkg/authentication/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockCertIssuer is a mock of CertIssuer interface.
|
|
||||||
type MockCertIssuer struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockCertIssuerMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCertIssuerMockRecorder is the mock recorder for MockCertIssuer.
|
|
||||||
type MockCertIssuerMockRecorder struct {
|
|
||||||
mock *MockCertIssuer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockCertIssuer creates a new mock instance.
|
|
||||||
func NewMockCertIssuer(ctrl *gomock.Controller) *MockCertIssuer {
|
|
||||||
mock := &MockCertIssuer{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockCertIssuerMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockCertIssuer) EXPECT() *MockCertIssuerMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// IssuePEM mocks base method.
|
|
||||||
func (m *MockCertIssuer) IssuePEM(arg0 pkix.Name, arg1 []string, arg2 time.Duration) ([]byte, []byte, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "IssuePEM", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].([]byte)
|
|
||||||
ret1, _ := ret[1].([]byte)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// IssuePEM indicates an expected call of IssuePEM.
|
|
||||||
func (mr *MockCertIssuerMockRecorder) IssuePEM(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssuePEM", reflect.TypeOf((*MockCertIssuer)(nil).IssuePEM), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockTokenCredentialRequestAuthenticator is a mock of TokenCredentialRequestAuthenticator interface.
|
// MockTokenCredentialRequestAuthenticator is a mock of TokenCredentialRequestAuthenticator interface.
|
||||||
type MockTokenCredentialRequestAuthenticator struct {
|
type MockTokenCredentialRequestAuthenticator struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package credentialrequestmocks
|
package credentialrequestmocks
|
||||||
|
|
||||||
//go:generate go run -v github.com/golang/mock/mockgen -destination=credentialrequestmocks.go -package=credentialrequestmocks -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/registry/credentialrequest CertIssuer,TokenCredentialRequestAuthenticator
|
//go:generate go run -v github.com/golang/mock/mockgen -destination=credentialrequestmocks.go -package=credentialrequestmocks -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/registry/credentialrequest TokenCredentialRequestAuthenticator
|
||||||
|
6
internal/mocks/issuermocks/generate.go
Normal file
6
internal/mocks/issuermocks/generate.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package issuermocks
|
||||||
|
|
||||||
|
//go:generate go run -v github.com/golang/mock/mockgen -destination=issuermocks.go -package=issuermocks -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/issuer ClientCertIssuer
|
69
internal/mocks/issuermocks/issuermocks.go
Normal file
69
internal/mocks/issuermocks/issuermocks.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: go.pinniped.dev/internal/issuer (interfaces: ClientCertIssuer)
|
||||||
|
|
||||||
|
// Package issuermocks is a generated GoMock package.
|
||||||
|
package issuermocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
time "time"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockClientCertIssuer is a mock of ClientCertIssuer interface.
|
||||||
|
type MockClientCertIssuer struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockClientCertIssuerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClientCertIssuerMockRecorder is the mock recorder for MockClientCertIssuer.
|
||||||
|
type MockClientCertIssuerMockRecorder struct {
|
||||||
|
mock *MockClientCertIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockClientCertIssuer creates a new mock instance.
|
||||||
|
func NewMockClientCertIssuer(ctrl *gomock.Controller) *MockClientCertIssuer {
|
||||||
|
mock := &MockClientCertIssuer{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockClientCertIssuerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockClientCertIssuer) EXPECT() *MockClientCertIssuerMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueClientCertPEM mocks base method.
|
||||||
|
func (m *MockClientCertIssuer) IssueClientCertPEM(arg0 string, arg1 []string, arg2 time.Duration) ([]byte, []byte, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "IssueClientCertPEM", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].([]byte)
|
||||||
|
ret1, _ := ret[1].([]byte)
|
||||||
|
ret2, _ := ret[2].(error)
|
||||||
|
return ret0, ret1, ret2
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueClientCertPEM indicates an expected call of IssueClientCertPEM.
|
||||||
|
func (mr *MockClientCertIssuerMockRecorder) IssueClientCertPEM(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssueClientCertPEM", reflect.TypeOf((*MockClientCertIssuer)(nil).IssueClientCertPEM), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name mocks base method.
|
||||||
|
func (m *MockClientCertIssuer) Name() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Name")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name indicates an expected call of Name.
|
||||||
|
func (mr *MockClientCertIssuerMockRecorder) Name() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockClientCertIssuer)(nil).Name))
|
||||||
|
}
|
@ -6,7 +6,6 @@ package credentialrequest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -22,20 +21,17 @@ import (
|
|||||||
"k8s.io/utils/trace"
|
"k8s.io/utils/trace"
|
||||||
|
|
||||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
|
"go.pinniped.dev/internal/issuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// clientCertificateTTL is the TTL for short-lived client certificates returned by this API.
|
// clientCertificateTTL is the TTL for short-lived client certificates returned by this API.
|
||||||
const clientCertificateTTL = 5 * time.Minute
|
const clientCertificateTTL = 5 * time.Minute
|
||||||
|
|
||||||
type CertIssuer interface {
|
|
||||||
IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenCredentialRequestAuthenticator interface {
|
type TokenCredentialRequestAuthenticator interface {
|
||||||
AuthenticateTokenCredentialRequest(ctx context.Context, req *loginapi.TokenCredentialRequest) (user.Info, error)
|
AuthenticateTokenCredentialRequest(ctx context.Context, req *loginapi.TokenCredentialRequest) (user.Info, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer CertIssuer, resource schema.GroupResource) *REST {
|
func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer issuer.ClientCertIssuer, resource schema.GroupResource) *REST {
|
||||||
return &REST{
|
return &REST{
|
||||||
authenticator: authenticator,
|
authenticator: authenticator,
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
@ -45,7 +41,7 @@ func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer CertIssue
|
|||||||
|
|
||||||
type REST struct {
|
type REST struct {
|
||||||
authenticator TokenCredentialRequestAuthenticator
|
authenticator TokenCredentialRequestAuthenticator
|
||||||
issuer CertIssuer
|
issuer issuer.ClientCertIssuer
|
||||||
tableConvertor rest.TableConvertor
|
tableConvertor rest.TableConvertor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,30 +96,23 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := r.authenticator.AuthenticateTokenCredentialRequest(ctx, credentialRequest)
|
userInfo, err := r.authenticator.AuthenticateTokenCredentialRequest(ctx, credentialRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
traceFailureWithError(t, "token authentication", err)
|
traceFailureWithError(t, "token authentication", err)
|
||||||
return failureResponse(), nil
|
return failureResponse(), nil
|
||||||
}
|
}
|
||||||
if user == nil || user.GetName() == "" {
|
if userInfo == nil || userInfo.GetName() == "" {
|
||||||
traceSuccess(t, user, false)
|
traceSuccess(t, userInfo, false)
|
||||||
return failureResponse(), nil
|
return failureResponse(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
certPEM, keyPEM, err := r.issuer.IssuePEM(
|
certPEM, keyPEM, err := r.issuer.IssueClientCertPEM(userInfo.GetName(), userInfo.GetGroups(), clientCertificateTTL)
|
||||||
pkix.Name{
|
|
||||||
CommonName: user.GetName(),
|
|
||||||
Organization: user.GetGroups(),
|
|
||||||
},
|
|
||||||
[]string{},
|
|
||||||
clientCertificateTTL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
traceFailureWithError(t, "cert issuer", err)
|
traceFailureWithError(t, "cert issuer", err)
|
||||||
return failureResponse(), nil
|
return failureResponse(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
traceSuccess(t, user, true)
|
traceSuccess(t, userInfo, true)
|
||||||
|
|
||||||
return &loginapi.TokenCredentialRequest{
|
return &loginapi.TokenCredentialRequest{
|
||||||
Status: loginapi.TokenCredentialRequestStatus{
|
Status: loginapi.TokenCredentialRequestStatus{
|
||||||
|
@ -5,7 +5,6 @@ package credentialrequest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
@ -24,7 +23,9 @@ import (
|
|||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
|
"go.pinniped.dev/internal/issuer"
|
||||||
"go.pinniped.dev/internal/mocks/credentialrequestmocks"
|
"go.pinniped.dev/internal/mocks/credentialrequestmocks"
|
||||||
|
"go.pinniped.dev/internal/mocks/issuermocks"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -88,16 +89,14 @@ func TestCreate(t *testing.T) {
|
|||||||
Groups: []string{"test-group-1", "test-group-2"},
|
Groups: []string{"test-group-1", "test-group-2"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
issuer := credentialrequestmocks.NewMockCertIssuer(ctrl)
|
clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl)
|
||||||
issuer.EXPECT().IssuePEM(
|
clientCertIssuer.EXPECT().IssueClientCertPEM(
|
||||||
pkix.Name{
|
"test-user",
|
||||||
CommonName: "test-user",
|
[]string{"test-group-1", "test-group-2"},
|
||||||
Organization: []string{"test-group-1", "test-group-2"}},
|
|
||||||
[]string{},
|
|
||||||
5*time.Minute,
|
5*time.Minute,
|
||||||
).Return([]byte("test-cert"), []byte("test-key"), nil)
|
).Return([]byte("test-cert"), []byte("test-key"), nil)
|
||||||
|
|
||||||
storage := NewREST(requestAuthenticator, issuer, schema.GroupResource{})
|
storage := NewREST(requestAuthenticator, clientCertIssuer, schema.GroupResource{})
|
||||||
|
|
||||||
response, err := callCreate(context.Background(), storage, req)
|
response, err := callCreate(context.Background(), storage, req)
|
||||||
|
|
||||||
@ -131,12 +130,12 @@ func TestCreate(t *testing.T) {
|
|||||||
Groups: []string{"test-group-1", "test-group-2"},
|
Groups: []string{"test-group-1", "test-group-2"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
issuer := credentialrequestmocks.NewMockCertIssuer(ctrl)
|
clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl)
|
||||||
issuer.EXPECT().
|
clientCertIssuer.EXPECT().
|
||||||
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
IssueClientCertPEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
Return(nil, nil, fmt.Errorf("some certificate authority error"))
|
Return(nil, nil, fmt.Errorf("some certificate authority error"))
|
||||||
|
|
||||||
storage := NewREST(requestAuthenticator, issuer, schema.GroupResource{})
|
storage := NewREST(requestAuthenticator, clientCertIssuer, schema.GroupResource{})
|
||||||
|
|
||||||
response, err := callCreate(context.Background(), storage, req)
|
response, err := callCreate(context.Background(), storage, req)
|
||||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||||
@ -353,12 +352,12 @@ func requireSuccessfulResponseWithAuthenticationFailureMessage(t *testing.T, err
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func successfulIssuer(ctrl *gomock.Controller) CertIssuer {
|
func successfulIssuer(ctrl *gomock.Controller) issuer.ClientCertIssuer {
|
||||||
issuer := credentialrequestmocks.NewMockCertIssuer(ctrl)
|
clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl)
|
||||||
issuer.EXPECT().
|
clientCertIssuer.EXPECT().
|
||||||
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
IssueClientCertPEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
Return([]byte("test-cert"), []byte("test-key"), nil)
|
Return([]byte("test-cert"), []byte("test-key"), nil)
|
||||||
return issuer
|
return clientCertIssuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func stringPtr(s string) *string {
|
func stringPtr(s string) *string {
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package secret
|
package secret
|
||||||
@ -68,7 +68,7 @@ func TestCacheSynchronized(t *testing.T) {
|
|||||||
c.SetStateEncoderHashKey(issuer, stateEncoderHashKey)
|
c.SetStateEncoderHashKey(issuer, stateEncoderHashKey)
|
||||||
c.SetStateEncoderBlockKey(issuer, stateEncoderBlockKey)
|
c.SetStateEncoderBlockKey(issuer, stateEncoderBlockKey)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package testutil
|
package testutil
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -25,10 +26,18 @@ type ValidCert struct {
|
|||||||
parsed *x509.Certificate
|
parsed *x509.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCertificate validates a certificate and provides an object for asserting properties of the certificate.
|
// ValidateServerCertificate validates a certificate and provides an object for asserting properties of the certificate.
|
||||||
func ValidateCertificate(t *testing.T, caPEM string, certPEM string) *ValidCert {
|
func ValidateServerCertificate(t *testing.T, caPEM string, certPEM string) *ValidCert {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
return validateCertificate(t, x509.ExtKeyUsageServerAuth, caPEM, certPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateClientCertificate(t *testing.T, caPEM string, certPEM string) *ValidCert {
|
||||||
|
t.Helper()
|
||||||
|
return validateCertificate(t, x509.ExtKeyUsageClientAuth, caPEM, certPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCertificate(t *testing.T, extKeyUsage x509.ExtKeyUsage, caPEM string, certPEM string) *ValidCert {
|
||||||
block, _ := pem.Decode([]byte(certPEM))
|
block, _ := pem.Decode([]byte(certPEM))
|
||||||
require.NotNil(t, block)
|
require.NotNil(t, block)
|
||||||
parsed, err := x509.ParseCertificate(block.Bytes)
|
parsed, err := x509.ParseCertificate(block.Bytes)
|
||||||
@ -37,7 +46,10 @@ func ValidateCertificate(t *testing.T, caPEM string, certPEM string) *ValidCert
|
|||||||
// Validate the created cert using the CA.
|
// Validate the created cert using the CA.
|
||||||
roots := x509.NewCertPool()
|
roots := x509.NewCertPool()
|
||||||
require.True(t, roots.AppendCertsFromPEM([]byte(caPEM)))
|
require.True(t, roots.AppendCertsFromPEM([]byte(caPEM)))
|
||||||
opts := x509.VerifyOptions{Roots: roots}
|
opts := x509.VerifyOptions{
|
||||||
|
Roots: roots,
|
||||||
|
KeyUsages: []x509.ExtKeyUsage{extKeyUsage},
|
||||||
|
}
|
||||||
_, err = parsed.Verify(opts)
|
_, err = parsed.Verify(opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -61,6 +73,35 @@ func (v *ValidCert) RequireDNSName(expectDNSName string) {
|
|||||||
require.Contains(v.t, v.parsed.DNSNames, expectDNSName, "expected an explicit DNS SAN, not just Common Name")
|
require.Contains(v.t, v.parsed.DNSNames, expectDNSName, "expected an explicit DNS SAN, not just Common Name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *ValidCert) RequireDNSNames(names []string) {
|
||||||
|
v.t.Helper()
|
||||||
|
require.Equal(v.t, names, v.parsed.DNSNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ValidCert) RequireEmptyDNSNames() {
|
||||||
|
v.t.Helper()
|
||||||
|
require.Empty(v.t, v.parsed.DNSNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ValidCert) RequireIPs(ips []net.IP) {
|
||||||
|
v.t.Helper()
|
||||||
|
actualIPs := v.parsed.IPAddresses
|
||||||
|
actualIPsStrings := make([]string, len(actualIPs))
|
||||||
|
for i := range actualIPs {
|
||||||
|
actualIPsStrings[i] = actualIPs[i].String()
|
||||||
|
}
|
||||||
|
expectedIPsStrings := make([]string, len(ips))
|
||||||
|
for i := range ips {
|
||||||
|
expectedIPsStrings[i] = ips[i].String()
|
||||||
|
}
|
||||||
|
require.Equal(v.t, expectedIPsStrings, actualIPsStrings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ValidCert) RequireEmptyIPs() {
|
||||||
|
v.t.Helper()
|
||||||
|
require.Empty(v.t, v.parsed.IPAddresses)
|
||||||
|
}
|
||||||
|
|
||||||
// RequireLifetime asserts that the lifetime of the certificate matches the expected timestamps.
|
// RequireLifetime asserts that the lifetime of the certificate matches the expected timestamps.
|
||||||
func (v *ValidCert) RequireLifetime(expectNotBefore time.Time, expectNotAfter time.Time, delta time.Duration) {
|
func (v *ValidCert) RequireLifetime(expectNotBefore time.Time, expectNotAfter time.Time, delta time.Duration) {
|
||||||
v.t.Helper()
|
v.t.Helper()
|
||||||
@ -81,6 +122,11 @@ func (v *ValidCert) RequireCommonName(commonName string) {
|
|||||||
require.Equal(v.t, commonName, v.parsed.Subject.CommonName)
|
require.Equal(v.t, commonName, v.parsed.Subject.CommonName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *ValidCert) RequireOrganizations(orgs []string) {
|
||||||
|
v.t.Helper()
|
||||||
|
require.Equal(v.t, orgs, v.parsed.Subject.Organization)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCertificate creates a certificate with the provided time bounds, and returns the PEM
|
// CreateCertificate creates a certificate with the provided time bounds, and returns the PEM
|
||||||
// representation of the certificate and its private key. The returned certificate is capable of
|
// representation of the certificate and its private key. The returned certificate is capable of
|
||||||
// signing child certificates.
|
// signing child certificates.
|
||||||
|
@ -6,6 +6,7 @@ package testlogger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -13,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"github.com/go-logr/stdr"
|
"github.com/go-logr/stdr"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger implements logr.Logger in a way that captures logs for test assertions.
|
// Logger implements logr.Logger in a way that captures logs for test assertions.
|
||||||
@ -46,6 +48,12 @@ func (l *Logger) Lines() []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expect the emitted lines to match known-good output.
|
||||||
|
func (l *Logger) Expect(expected []string) {
|
||||||
|
actual := l.Lines()
|
||||||
|
require.Equalf(l.t, expected, actual, "did not see expected log output, actual output:\n%#v", backtickStrings(actual))
|
||||||
|
}
|
||||||
|
|
||||||
// syncBuffer synchronizes access to a bytes.Buffer.
|
// syncBuffer synchronizes access to a bytes.Buffer.
|
||||||
type syncBuffer struct {
|
type syncBuffer struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
@ -57,3 +65,18 @@ func (s *syncBuffer) Write(p []byte) (n int, err error) {
|
|||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
return s.buffer.Write(p)
|
return s.buffer.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type backtickStrings []string
|
||||||
|
|
||||||
|
func (b backtickStrings) GoString() string {
|
||||||
|
lines := make([]string, 0, len(b))
|
||||||
|
for _, s := range b {
|
||||||
|
if strings.Contains(s, "`") {
|
||||||
|
s = fmt.Sprintf("%#v", s)
|
||||||
|
} else {
|
||||||
|
s = fmt.Sprintf("`%s`", s)
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("\t%s,", s))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[]string{\n%s\n}", strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user