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)
|
||||
# vendor/
|
||||
|
||||
# goland
|
||||
# GoLand
|
||||
.idea
|
||||
|
||||
# Intermediate files used by Tilt
|
||||
/hack/lib/tilt/build
|
||||
|
||||
# MacOS Desktop Services Store
|
||||
.DS_Store
|
||||
|
@ -96,36 +96,29 @@ docker build .
|
||||
- [`kapp`](https://carvel.dev/#getting-started)
|
||||
- [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start)
|
||||
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [`tilt`](https://docs.tilt.dev/install.html)
|
||||
- [`ytt`](https://carvel.dev/#getting-started)
|
||||
|
||||
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
|
||||
|
||||
```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
|
||||
./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:
|
||||
|
||||
```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`.
|
||||
|
||||
### 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
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
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.
|
||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
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
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
@ -54,12 +54,12 @@ const (
|
||||
)
|
||||
|
||||
type webhook struct {
|
||||
certProvider dynamiccert.Provider
|
||||
certProvider dynamiccert.Private
|
||||
secretInformer corev1informers.SecretInformer
|
||||
}
|
||||
|
||||
func newWebhook(
|
||||
certProvider dynamiccert.Provider,
|
||||
certProvider dynamiccert.Private,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) *webhook {
|
||||
return &webhook{
|
||||
@ -281,7 +281,7 @@ func respondWithAuthenticated(
|
||||
|
||||
func startControllers(
|
||||
ctx context.Context,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
kubeClient kubernetes.Interface,
|
||||
kubeInformers kubeinformers.SharedInformerFactory,
|
||||
) {
|
||||
@ -328,7 +328,7 @@ func startControllers(
|
||||
func startWebhook(
|
||||
ctx context.Context,
|
||||
l net.Listener,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) error {
|
||||
return newWebhook(dynamicCertProvider, secretInformer).start(ctx, l)
|
||||
@ -355,7 +355,7 @@ func run() error {
|
||||
kubeinformers.WithNamespace(namespace),
|
||||
)
|
||||
|
||||
dynamicCertProvider := dynamiccert.New()
|
||||
dynamicCertProvider := dynamiccert.NewServingCert("local-user-authenticator-tls-serving-certificate")
|
||||
|
||||
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
|
||||
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
|
||||
|
||||
package main
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -99,7 +98,7 @@ func TestWebhook(t *testing.T) {
|
||||
},
|
||||
}))
|
||||
|
||||
secretInformer := createSecretInformer(t, kubeClient)
|
||||
secretInformer := createSecretInformer(ctx, t, kubeClient)
|
||||
|
||||
certProvider, caBundle, serverName := newCertProvider(t)
|
||||
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()
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
||||
@ -448,9 +447,6 @@ func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1i
|
||||
// informer factory before syncing it.
|
||||
secretInformer.Informer()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
defer cancel()
|
||||
|
||||
kubeInformers.Start(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
|
||||
// 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.
|
||||
func newCertProvider(t *testing.T) (dynamiccert.Provider, []byte, string) {
|
||||
func newCertProvider(t *testing.T) (dynamiccert.Private, []byte, string) {
|
||||
t.Helper()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
certPEM, keyPEM, err := certauthority.ToPEM(cert)
|
||||
require.NoError(t, err)
|
||||
|
||||
certProvider := dynamiccert.New()
|
||||
certProvider.Set(certPEM, keyPEM)
|
||||
certProvider := dynamiccert.NewServingCert(t.Name())
|
||||
err = certProvider.SetCertKeyContent(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/stdr"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
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"
|
||||
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"
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
)
|
||||
@ -32,12 +36,14 @@ import (
|
||||
type kubeconfigDeps struct {
|
||||
getPathToSelf func() (string, error)
|
||||
getClientset getConciergeClientsetFunc
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func kubeconfigRealDeps() kubeconfigDeps {
|
||||
return kubeconfigDeps{
|
||||
getPathToSelf: os.Executable,
|
||||
getClientset: getRealConciergeClientset,
|
||||
log: stdr.New(log.New(os.Stderr, "", 0)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,20 +60,28 @@ type getKubeconfigOIDCParams struct {
|
||||
skipBrowser bool
|
||||
sessionCachePath string
|
||||
debugSessionCache bool
|
||||
caBundlePaths []string
|
||||
caBundle caBundleFlag
|
||||
requestAudience string
|
||||
}
|
||||
|
||||
type getKubeconfigConciergeParams struct {
|
||||
disabled bool
|
||||
credentialIssuer string
|
||||
authenticatorName string
|
||||
authenticatorType string
|
||||
apiGroupSuffix string
|
||||
caBundle caBundleFlag
|
||||
endpoint string
|
||||
mode conciergeModeFlag
|
||||
skipWait bool
|
||||
}
|
||||
|
||||
type getKubeconfigParams struct {
|
||||
kubeconfigPath string
|
||||
kubeconfigContextOverride string
|
||||
skipValidate bool
|
||||
timeout time.Duration
|
||||
outputPath string
|
||||
staticToken string
|
||||
staticTokenEnvName string
|
||||
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.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.StringVar(&namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
||||
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(&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.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.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.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.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.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.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.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")
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
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
|
||||
}
|
||||
|
||||
//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.
|
||||
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{
|
||||
@ -137,11 +173,6 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
||||
}
|
||||
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)
|
||||
currentKubeConfig, err := clientConfig.RawConfig()
|
||||
if err != nil {
|
||||
@ -157,17 +188,39 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
||||
}
|
||||
|
||||
if !flags.concierge.disabled {
|
||||
credentialIssuer, err := waitForCredentialIssuer(ctx, clientset, flags, deps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authenticator, err := lookupAuthenticator(
|
||||
clientset,
|
||||
flags.concierge.authenticatorType,
|
||||
flags.concierge.authenticatorName,
|
||||
deps.log,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configureConcierge(authenticator, &flags, cluster, &oidcCABundle, &execConfig); err != nil {
|
||||
if err := discoverConciergeParams(credentialIssuer, &flags, cluster, deps.log); err != nil {
|
||||
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`.
|
||||
@ -182,13 +235,18 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
||||
if 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`.
|
||||
execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...)
|
||||
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,
|
||||
"--issuer="+flags.oidc.issuer,
|
||||
@ -201,8 +259,8 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar
|
||||
if flags.oidc.listenPort != 0 {
|
||||
execConfig.Args = append(execConfig.Args, "--listen-port="+strconv.Itoa(int(flags.oidc.listenPort)))
|
||||
}
|
||||
if oidcCABundle != "" {
|
||||
execConfig.Args = append(execConfig.Args, "--ca-bundle-data="+base64.StdEncoding.EncodeToString([]byte(oidcCABundle)))
|
||||
if len(flags.oidc.caBundle) != 0 {
|
||||
execConfig.Args = append(execConfig.Args, "--ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.oidc.caBundle))
|
||||
}
|
||||
if 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 != "" {
|
||||
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) {
|
||||
case *conciergev1alpha1.WebhookAuthenticator:
|
||||
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
||||
// them to point at the discovered WebhookAuthenticator.
|
||||
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
||||
log.Info("discovered WebhookAuthenticator", "name", auth.Name)
|
||||
flags.concierge.authenticatorType = "webhook"
|
||||
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
|
||||
// them to point at the discovered JWTAuthenticator.
|
||||
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
||||
log.Info("discovered JWTAuthenticator", "name", auth.Name)
|
||||
flags.concierge.authenticatorType = "jwt"
|
||||
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 flags.oidc.issuer == "" {
|
||||
log.Info("discovered OIDC issuer", "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 flags.oidc.requestAudience == "" {
|
||||
log.Info("discovered OIDC audience", "audience", auth.Spec.Audience)
|
||||
flags.oidc.requestAudience = auth.Spec.Audience
|
||||
}
|
||||
|
||||
// If the --oidc-ca-bundle flags was not set explicitly, default it to the
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func loadCABundlePaths(paths []string) (string, error) {
|
||||
if len(paths) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
blobs := make([][]byte, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
pem, err := ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
func getConciergeFrontend(credentialIssuer *configv1alpha1.CredentialIssuer, mode conciergeModeFlag) (*configv1alpha1.CredentialIssuerFrontend, error) {
|
||||
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||
// Skip unhealthy strategies.
|
||||
if strategy.Status != configv1alpha1.SuccessStrategyStatus {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
@ -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)
|
||||
defer cancelFunc()
|
||||
|
||||
@ -331,6 +537,12 @@ func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authN
|
||||
return nil, fmt.Errorf("no authenticators were found")
|
||||
}
|
||||
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 results[0], nil
|
||||
@ -353,9 +565,101 @@ func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Con
|
||||
if currentContextNameOverride != "" {
|
||||
contextName = currentContextNameOverride
|
||||
}
|
||||
context := currentKubeConfig.Contexts[contextName]
|
||||
if context == nil {
|
||||
ctx := currentKubeConfig.Contexts[contextName]
|
||||
if ctx == nil {
|
||||
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 (
|
||||
"bytes"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -20,19 +19,26 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
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"
|
||||
fakeconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/internal/testutil/testlogger"
|
||||
)
|
||||
|
||||
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)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
testOIDCCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
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 {
|
||||
name string
|
||||
@ -42,6 +48,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
getClientsetErr error
|
||||
conciergeObjects []runtime.Object
|
||||
conciergeReactions []kubetesting.Reactor
|
||||
wantLogs []string
|
||||
wantError bool
|
||||
wantStdout 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-authenticator-name string Concierge authenticator name (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
|
||||
--kubeconfig string Path to kubeconfig file
|
||||
--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
|
||||
--oidc-ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||
--no-concierge Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly
|
||||
--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-issuer string OpenID Connect issuer URL (default: autodiscover)
|
||||
--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-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)
|
||||
-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-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{
|
||||
"--oidc-ca-bundle", "./does/not/exist",
|
||||
},
|
||||
wantError: true,
|
||||
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
|
||||
`),
|
||||
},
|
||||
{
|
||||
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",
|
||||
args: []string{
|
||||
@ -135,6 +186,12 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--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,
|
||||
wantStderr: here.Doc(`
|
||||
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-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,
|
||||
wantStderr: here.Doc(`
|
||||
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-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,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt"
|
||||
@ -169,6 +238,12 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
args: []string{
|
||||
"--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{
|
||||
&kubetesting.SimpleReactor{
|
||||
Verb: "*",
|
||||
@ -188,6 +263,9 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}},
|
||||
},
|
||||
conciergeReactions: []kubetesting.Reactor{
|
||||
&kubetesting.SimpleReactor{
|
||||
Verb: "*",
|
||||
@ -197,6 +275,9 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantLogs: []string{
|
||||
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error
|
||||
@ -207,6 +288,12 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
args: []string{
|
||||
"--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,
|
||||
wantStderr: here.Doc(`
|
||||
Error: no authenticators were found
|
||||
@ -218,27 +305,145 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
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-2"}},
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}},
|
||||
&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,
|
||||
wantStderr: here.Doc(`
|
||||
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",
|
||||
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.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"}},
|
||||
},
|
||||
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,
|
||||
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",
|
||||
},
|
||||
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{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: "https://test-issuer.example.com",
|
||||
Audience: "some-test-audience",
|
||||
TLS: &conciergev1alpha1.TLSSpec{
|
||||
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,
|
||||
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
|
||||
@ -269,21 +503,45 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
"--static-token-env", "TEST_TOKEN",
|
||||
},
|
||||
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"}},
|
||||
},
|
||||
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,
|
||||
wantStderr: here.Doc(`
|
||||
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{
|
||||
"--concierge-api-group-suffix", ".starts.with.dot",
|
||||
},
|
||||
wantError: true,
|
||||
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{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--static-token", "test-token",
|
||||
"--skip-validation",
|
||||
},
|
||||
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"}},
|
||||
},
|
||||
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(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
@ -335,10 +618,35 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--static-token-env", "TEST_TOKEN",
|
||||
"--skip-validation",
|
||||
},
|
||||
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"}},
|
||||
},
|
||||
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(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
@ -378,19 +686,47 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
name: "autodetect 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: 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{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: "https://example.com/issuer",
|
||||
Audience: "test-audience",
|
||||
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(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
@ -428,34 +764,57 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
provideClusterInfo: true
|
||||
`, base64.StdEncoding.EncodeToString(testCA.Bundle())),
|
||||
`, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())),
|
||||
},
|
||||
{
|
||||
name: "autodetect nothing, set a bunch of options",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-credential-issuer", "test-credential-issuer",
|
||||
"--concierge-api-group-suffix", "tuna.io",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--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-skip-browser",
|
||||
"--oidc-listen-port", "1234",
|
||||
"--oidc-ca-bundle", testCABundlePath,
|
||||
"--oidc-ca-bundle", testOIDCCABundlePath,
|
||||
"--oidc-session-cache", "/path/to/cache/dir/sessions.yaml",
|
||||
"--oidc-debug-session-cache",
|
||||
"--oidc-request-audience", "test-audience",
|
||||
"--skip-validation",
|
||||
},
|
||||
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{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"},
|
||||
},
|
||||
},
|
||||
wantLogs: nil,
|
||||
wantStdout: here.Docf(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
server: https://fake-server-url-value
|
||||
certificate-authority-data: %s
|
||||
server: https://explicit-concierge-endpoint.example.com
|
||||
name: pinniped
|
||||
contexts:
|
||||
- context:
|
||||
@ -477,8 +836,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --concierge-api-group-suffix=tuna.io
|
||||
- --concierge-authenticator-name=test-authenticator
|
||||
- --concierge-authenticator-type=webhook
|
||||
- --concierge-endpoint=https://fake-server-url-value
|
||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
- --concierge-endpoint=https://explicit-concierge-endpoint.example.com
|
||||
- --concierge-ca-bundle-data=%s
|
||||
- --issuer=https://example.com/issuer
|
||||
- --client-id=pinniped-cli
|
||||
- --scopes=offline_access,openid,pinniped:request-audience
|
||||
@ -491,13 +850,229 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
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",
|
||||
},
|
||||
{
|
||||
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 {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testLog := testlogger.New(t)
|
||||
cmd := kubeconfigCommand(kubeconfigDeps{
|
||||
getPathToSelf: func() (string, error) {
|
||||
if tt.getPathToSelfErr != nil {
|
||||
@ -516,10 +1091,11 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
}
|
||||
fake := fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects...)
|
||||
if len(tt.conciergeReactions) > 0 {
|
||||
fake.ReactionChain = tt.conciergeReactions
|
||||
fake.ReactionChain = append(tt.conciergeReactions, fake.ReactionChain...)
|
||||
}
|
||||
return fake, nil
|
||||
},
|
||||
log: testLog,
|
||||
})
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
@ -533,6 +1109,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
testLog.Expect(tt.wantLogs)
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
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().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.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().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().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
||||
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(&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.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
cmd.Flags().StringVar(&flags.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.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
|
||||
mustMarkHidden(cmd, "debug-session-cache")
|
||||
@ -144,7 +144,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
)
|
||||
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)
|
||||
|
||||
// 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 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
|
||||
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)
|
||||
}
|
||||
|
||||
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
for _, p := range caBundlePaths {
|
||||
|
@ -6,7 +6,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -29,7 +28,7 @@ import (
|
||||
func TestLoginOIDCCommand(t *testing.T) {
|
||||
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)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
@ -58,14 +57,14 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
|
||||
Flags:
|
||||
--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")
|
||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||
--concierge-authenticator-name string Concierge authenticator name
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the concierge
|
||||
--concierge-endpoint string API base for the Pinniped concierge endpoint
|
||||
--enable-concierge Exchange the OIDC ID token with the Pinniped concierge during login
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||
--concierge-endpoint string API base for the Concierge endpoint
|
||||
--enable-concierge Use the Concierge to login
|
||||
-h, --help help for oidc
|
||||
--issuer string OpenID Connect issuer URL
|
||||
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||
@ -92,7 +91,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
},
|
||||
wantError: true,
|
||||
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{
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
@ -131,7 +130,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
},
|
||||
wantError: true,
|
||||
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,
|
||||
wantError: true,
|
||||
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.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().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
||||
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(&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.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
cmd.Flags().StringVar(&flags.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.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) }
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
@ -92,7 +93,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
)
|
||||
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}})
|
||||
|
||||
// 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 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@ -120,7 +121,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
|
||||
var err error
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token)
|
||||
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)
|
||||
|
@ -6,7 +6,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
@ -24,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
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-authenticator-name string Concierge authenticator name
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the concierge
|
||||
--concierge-endpoint string API base for the Pinniped concierge endpoint
|
||||
--enable-concierge Exchange the token with the Pinniped concierge during login
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||
--concierge-endpoint string API base for the Concierge endpoint
|
||||
--enable-concierge Use the Concierge to login
|
||||
-h, --help help for static
|
||||
--token string Static token to present during login
|
||||
--token-env string Environment variable containing a static token
|
||||
@ -78,7 +77,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
},
|
||||
wantError: true,
|
||||
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"),
|
||||
wantError: true,
|
||||
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{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
@ -141,7 +140,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
},
|
||||
wantError: true,
|
||||
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"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
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"
|
||||
conciergescheme "go.pinniped.dev/internal/concierge/scheme"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
//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 {
|
||||
scheme, _, identityGV := conciergeschemeNew(apiGroupSuffix)
|
||||
scheme, _, identityGV := conciergescheme.New(apiGroupSuffix)
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
respInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), contentType)
|
||||
if !ok {
|
||||
@ -194,110 +189,3 @@ func prettyStrings(ss []string) 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
|
||||
this strategy.
|
||||
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:
|
||||
description: TokenCredentialRequestAPIInfo describes the
|
||||
parameters for the TokenCredentialRequest API on this
|
||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the Kubernetes
|
||||
API server CA bundle.
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
Kubernetes API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
@ -91,6 +111,7 @@ spec:
|
||||
can use with a strategy.
|
||||
enum:
|
||||
- TokenCredentialRequestAPI
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
@ -106,8 +127,13 @@ spec:
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- Listening
|
||||
- Pending
|
||||
- Disabled
|
||||
- ErrorDuringSetup
|
||||
- CouldNotFetchKey
|
||||
- CouldNotGetClusterInfo
|
||||
- FetchedKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
@ -119,6 +145,7 @@ spec:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- lastUpdateTime
|
||||
|
@ -42,6 +42,11 @@ data:
|
||||
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
|
||||
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)
|
||||
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() @)
|
||||
kubeCertAgent:
|
||||
namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @)
|
||||
@ -189,6 +194,20 @@ spec:
|
||||
port: 443
|
||||
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
|
||||
kind: APIService
|
||||
metadata:
|
||||
|
@ -31,6 +31,12 @@ rules:
|
||||
resources: [ securitycontextconstraints ]
|
||||
verbs: [ use ]
|
||||
resourceNames: [ nonroot ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "users", "groups" ]
|
||||
verbs: [ "impersonate" ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ nodes ]
|
||||
verbs: [ list ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
|
||||
resources: [ credentialissuers ]
|
||||
@ -69,7 +75,7 @@ metadata:
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ services ]
|
||||
verbs: [ create, get, list, patch, update, watch ]
|
||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ secrets ]
|
||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||
@ -81,9 +87,12 @@ rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ pods/exec ]
|
||||
verbs: [ create ]
|
||||
- apiGroups: [apps]
|
||||
resources: [replicasets,deployments]
|
||||
verbs: [get]
|
||||
- apiGroups: [ apps ]
|
||||
resources: [ replicasets,deployments ]
|
||||
verbs: [ get ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ configmaps ]
|
||||
verbs: [ list, get, watch ]
|
||||
---
|
||||
kind: RoleBinding
|
||||
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
|
||||
| *`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".
|
||||
| *`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"]
|
||||
==== TokenCredentialRequestAPIInfo
|
||||
|
||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
||||
|===
|
||||
| Field | Description
|
||||
| *`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
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
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.
|
||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
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
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
||||
*out = new(TokenCredentialRequestAPIInfo)
|
||||
**out = **in
|
||||
}
|
||||
if in.ImpersonationProxyInfo != nil {
|
||||
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||
*out = new(ImpersonationProxyInfo)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
||||
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.
|
||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||
*out = *in
|
||||
|
@ -67,14 +67,34 @@ spec:
|
||||
description: Frontend describes how clients can connect using
|
||||
this strategy.
|
||||
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:
|
||||
description: TokenCredentialRequestAPIInfo describes the
|
||||
parameters for the TokenCredentialRequest API on this
|
||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the Kubernetes
|
||||
API server CA bundle.
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
Kubernetes API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
@ -91,6 +111,7 @@ spec:
|
||||
can use with a strategy.
|
||||
enum:
|
||||
- TokenCredentialRequestAPI
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
@ -106,8 +127,13 @@ spec:
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- Listening
|
||||
- Pending
|
||||
- Disabled
|
||||
- ErrorDuringSetup
|
||||
- CouldNotFetchKey
|
||||
- CouldNotGetClusterInfo
|
||||
- FetchedKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
@ -119,6 +145,7 @@ spec:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- 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
|
||||
| *`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".
|
||||
| *`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"]
|
||||
==== TokenCredentialRequestAPIInfo
|
||||
|
||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
||||
|===
|
||||
| Field | Description
|
||||
| *`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
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
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.
|
||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
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
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
||||
*out = new(TokenCredentialRequestAPIInfo)
|
||||
**out = **in
|
||||
}
|
||||
if in.ImpersonationProxyInfo != nil {
|
||||
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||
*out = new(ImpersonationProxyInfo)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
||||
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.
|
||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||
*out = *in
|
||||
|
@ -67,14 +67,34 @@ spec:
|
||||
description: Frontend describes how clients can connect using
|
||||
this strategy.
|
||||
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:
|
||||
description: TokenCredentialRequestAPIInfo describes the
|
||||
parameters for the TokenCredentialRequest API on this
|
||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the Kubernetes
|
||||
API server CA bundle.
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
Kubernetes API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
@ -91,6 +111,7 @@ spec:
|
||||
can use with a strategy.
|
||||
enum:
|
||||
- TokenCredentialRequestAPI
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
@ -106,8 +127,13 @@ spec:
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- Listening
|
||||
- Pending
|
||||
- Disabled
|
||||
- ErrorDuringSetup
|
||||
- CouldNotFetchKey
|
||||
- CouldNotGetClusterInfo
|
||||
- FetchedKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
@ -119,6 +145,7 @@ spec:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- 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
|
||||
| *`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".
|
||||
| *`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"]
|
||||
==== TokenCredentialRequestAPIInfo
|
||||
|
||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
||||
|===
|
||||
| Field | Description
|
||||
| *`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
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
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.
|
||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
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
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
||||
*out = new(TokenCredentialRequestAPIInfo)
|
||||
**out = **in
|
||||
}
|
||||
if in.ImpersonationProxyInfo != nil {
|
||||
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||
*out = new(ImpersonationProxyInfo)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
||||
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.
|
||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||
*out = *in
|
||||
|
@ -67,14 +67,34 @@ spec:
|
||||
description: Frontend describes how clients can connect using
|
||||
this strategy.
|
||||
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:
|
||||
description: TokenCredentialRequestAPIInfo describes the
|
||||
parameters for the TokenCredentialRequest API on this
|
||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the Kubernetes
|
||||
API server CA bundle.
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
Kubernetes API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
@ -91,6 +111,7 @@ spec:
|
||||
can use with a strategy.
|
||||
enum:
|
||||
- TokenCredentialRequestAPI
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
@ -106,8 +127,13 @@ spec:
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- Listening
|
||||
- Pending
|
||||
- Disabled
|
||||
- ErrorDuringSetup
|
||||
- CouldNotFetchKey
|
||||
- CouldNotGetClusterInfo
|
||||
- FetchedKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
@ -119,6 +145,7 @@ spec:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- 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
|
||||
| *`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".
|
||||
| *`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"]
|
||||
==== TokenCredentialRequestAPIInfo
|
||||
|
||||
@ -328,7 +347,7 @@ Status of a credential issuer.
|
||||
|===
|
||||
| Field | Description
|
||||
| *`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
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
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.
|
||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
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
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
||||
*out = new(TokenCredentialRequestAPIInfo)
|
||||
**out = **in
|
||||
}
|
||||
if in.ImpersonationProxyInfo != nil {
|
||||
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||
*out = new(ImpersonationProxyInfo)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
||||
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.
|
||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||
*out = *in
|
||||
|
@ -67,14 +67,34 @@ spec:
|
||||
description: Frontend describes how clients can connect using
|
||||
this strategy.
|
||||
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:
|
||||
description: TokenCredentialRequestAPIInfo describes the
|
||||
parameters for the TokenCredentialRequest API on this
|
||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the Kubernetes
|
||||
API server CA bundle.
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
Kubernetes API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
@ -91,6 +111,7 @@ spec:
|
||||
can use with a strategy.
|
||||
enum:
|
||||
- TokenCredentialRequestAPI
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
@ -106,8 +127,13 @@ spec:
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- Listening
|
||||
- Pending
|
||||
- Disabled
|
||||
- ErrorDuringSetup
|
||||
- CouldNotFetchKey
|
||||
- CouldNotGetClusterInfo
|
||||
- FetchedKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
@ -119,6 +145,7 @@ spec:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- 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
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
@ -82,6 +88,10 @@ type CredentialIssuerFrontend struct {
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
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.
|
||||
@ -91,7 +101,19 @@ type TokenCredentialRequestAPIInfo struct {
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
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
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
@ -46,6 +46,11 @@ func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend)
|
||||
*out = new(TokenCredentialRequestAPIInfo)
|
||||
**out = **in
|
||||
}
|
||||
if in.ImpersonationProxyInfo != nil {
|
||||
in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo
|
||||
*out = new(ImpersonationProxyInfo)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,6 +163,22 @@ func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy {
|
||||
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.
|
||||
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
|
||||
*out = *in
|
||||
|
2
go.mod
2
go.mod
@ -15,6 +15,7 @@ require (
|
||||
github.com/google/go-cmp v0.5.5
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/oleiade/reflections v1.0.1 // indirect
|
||||
github.com/onsi/ginkgo v1.13.0 // indirect
|
||||
github.com/ory/fosite v0.39.0
|
||||
@ -26,6 +27,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
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/sync v0.0.0-20201207232520-09787c993a3a
|
||||
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-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-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-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-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
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 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
|
||||
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)"
|
||||
@ -292,7 +308,7 @@ kind_capabilities_file="$pinniped_path/test/cluster_capabilities/kind.yaml"
|
||||
pinniped_cluster_capability_file_content=$(cat "$kind_capabilities_file")
|
||||
|
||||
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_APP_NAME=${concierge_app_name}
|
||||
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 "🚀 Ready to run integration tests! For example..."
|
||||
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 '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}"
|
||||
|
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
|
||||
|
||||
// Package certauthority implements a simple x509 certificate authority suitable for use in an aggregated API service.
|
||||
@ -19,6 +19,8 @@ import (
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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
|
||||
|
||||
// signer is the private key for the current CA.
|
||||
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 env
|
||||
}
|
||||
@ -66,7 +73,7 @@ func secureEnv() env {
|
||||
}
|
||||
|
||||
// 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).
|
||||
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 {
|
||||
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{
|
||||
caCertBytes: cert.Certificate[0],
|
||||
signer: cert.PrivateKey.(crypto.Signer),
|
||||
@ -84,13 +98,13 @@ func Load(certPEM string, keyPEM string) (*CA, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// New generates a fresh certificate authority with the given subject and ttl.
|
||||
func New(subject pkix.Name, ttl time.Duration) (*CA, error) {
|
||||
return newInternal(subject, ttl, secureEnv())
|
||||
// New generates a fresh certificate authority with the given Common Name and TTL.
|
||||
func New(commonName string, ttl time.Duration) (*CA, error) {
|
||||
return newInternal(commonName, ttl, secureEnv())
|
||||
}
|
||||
|
||||
// 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}
|
||||
// Generate a random serial for the CA
|
||||
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.
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), env.keygenRNG)
|
||||
ca.privateKey, err = ecdsa.GenerateKey(elliptic.P256(), env.keygenRNG)
|
||||
if err != nil {
|
||||
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.
|
||||
now := env.clock()
|
||||
@ -113,7 +127,7 @@ func newInternal(subject pkix.Name, ttl time.Duration, env env) (*CA, error) {
|
||||
// Create CA cert template
|
||||
caTemplate := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: subject,
|
||||
Subject: pkix.Name{CommonName: commonName},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
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.
|
||||
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 {
|
||||
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})
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *CA) Pool() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
@ -143,8 +169,31 @@ func (c *CA) Pool() *x509.CertPool {
|
||||
return pool
|
||||
}
|
||||
|
||||
// Issue a new server certificate for the given identity and duration.
|
||||
func (c *CA) Issue(subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.Duration) (*tls.Certificate, error) {
|
||||
// IssueClientCert issues a new client certificate with username and groups included in the Kube-style
|
||||
// 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.
|
||||
serialNumber, err := randomSerial(c.env.serialRNG)
|
||||
if err != nil {
|
||||
@ -174,8 +223,7 @@ func (c *CA) Issue(subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.
|
||||
Subject: subject,
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{extKeyUsage},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
DNSNames: dnsNames,
|
||||
@ -200,14 +248,8 @@ func (c *CA) Issue(subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.
|
||||
}, 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) {
|
||||
// If the wrapped Issue() returned an error, pass it back.
|
||||
// If the wrapped IssueServerCert() returned an error, pass it back.
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
package certauthority
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@ -17,6 +16,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
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.NotEmpty(t, ca.caCertBytes)
|
||||
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) {
|
||||
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.NotNil(t, got)
|
||||
require.NotNil(t, ca)
|
||||
|
||||
// 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.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(time.Minute), caCert.NotAfter, 10*time.Second)
|
||||
|
||||
require.NotNil(t, ca.privateKey)
|
||||
}
|
||||
|
||||
func TestNewInternal(t *testing.T) {
|
||||
@ -155,7 +159,7 @@ func TestNewInternal(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
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 != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
require.Nil(t, got)
|
||||
@ -175,21 +179,34 @@ func TestNewInternal(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}}
|
||||
got := ca.Bundle()
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----\nAQIDBAUGBwg=\n-----END CERTIFICATE-----\n", string(got))
|
||||
})
|
||||
ca := CA{caCertBytes: []byte{1, 2, 3, 4, 5, 6, 7, 8}}
|
||||
certPEM := ca.Bundle()
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----\nAQIDBAUGBwg=\n-----END CERTIFICATE-----\n", string(certPEM))
|
||||
}
|
||||
|
||||
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) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ca, err := New(pkix.Name{CommonName: "test"}, 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
ca, err := New("test", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := ca.Pool()
|
||||
require.Len(t, got.Subjects(), 1)
|
||||
})
|
||||
pool := ca.Pool()
|
||||
require.Len(t, pool.Subjects(), 1)
|
||||
}
|
||||
|
||||
type errSigner struct {
|
||||
@ -204,6 +221,8 @@ func (e *errSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, er
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
realCA, err := loadFromFiles(t, "./testdata/test.crt", "./testdata/test.key")
|
||||
@ -227,7 +246,7 @@ func TestIssue(t *testing.T) {
|
||||
name: "failed to generate keypair",
|
||||
ca: CA{
|
||||
env: env{
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
keygenRNG: strings.NewReader(""),
|
||||
},
|
||||
},
|
||||
@ -237,8 +256,8 @@ func TestIssue(t *testing.T) {
|
||||
name: "invalid CA certificate",
|
||||
ca: CA{
|
||||
env: env{
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
clock: func() time.Time { return now },
|
||||
},
|
||||
},
|
||||
@ -248,8 +267,8 @@ func TestIssue(t *testing.T) {
|
||||
name: "signing error",
|
||||
ca: CA{
|
||||
env: env{
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
clock: func() time.Time { return now },
|
||||
},
|
||||
caCertBytes: realCA.caCertBytes,
|
||||
@ -261,11 +280,11 @@ func TestIssue(t *testing.T) {
|
||||
wantErr: "could not sign certificate: some signer error",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
name: "parse certificate error",
|
||||
ca: CA{
|
||||
env: env{
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
clock: func() time.Time { return now },
|
||||
parseCert: func(_ []byte) (*x509.Certificate, error) {
|
||||
return nil, fmt.Errorf("some parse certificate error")
|
||||
@ -280,8 +299,8 @@ func TestIssue(t *testing.T) {
|
||||
name: "success",
|
||||
ca: CA{
|
||||
env: env{
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
|
||||
serialRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
keygenRNG: strings.NewReader(strings.Repeat("x", numRandBytes)),
|
||||
clock: func() time.Time { return now },
|
||||
parseCert: x509.ParseCertificate,
|
||||
},
|
||||
@ -293,28 +312,26 @@ func TestIssue(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
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 != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
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) {
|
||||
realCert, err := tls.LoadX509KeyPair("./testdata/test.crt", "./testdata/test.key")
|
||||
require.NoError(t, err)
|
||||
@ -342,3 +359,90 @@ func TestToPEM(t *testing.T) {
|
||||
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
|
||||
|
||||
// Package dynamiccertauthority implements a x509 certificate authority capable of issuing
|
||||
@ -6,34 +6,42 @@
|
||||
package dynamiccertauthority
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/issuer"
|
||||
)
|
||||
|
||||
// CA is a type capable of issuing certificates.
|
||||
type CA struct {
|
||||
provider dynamiccert.Provider
|
||||
// ca is a type capable of issuing certificates.
|
||||
type ca struct {
|
||||
provider dynamiccertificates.CertKeyContentProvider
|
||||
}
|
||||
|
||||
// New creates a new CA, ready to issue certs whenever the provided provider has a keypair to
|
||||
// provide.
|
||||
func New(provider dynamiccert.Provider) *CA {
|
||||
return &CA{
|
||||
// New creates a ClientCertIssuer, ready to issue certs whenever
|
||||
// the given CertKeyContentProvider has a keypair to provide.
|
||||
func New(provider dynamiccertificates.CertKeyContentProvider) issuer.ClientCertIssuer {
|
||||
return &ca{
|
||||
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.
|
||||
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()
|
||||
// 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))
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
package dynamiccertauthority
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/issuer"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestCAIssuePEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
provider := dynamiccert.New()
|
||||
provider := dynamiccert.NewCA(t.Name())
|
||||
ca := New(provider)
|
||||
|
||||
goodCACrtPEM0, goodCAKeyPEM0, err := testutil.CreateCertificate(
|
||||
@ -44,12 +44,12 @@ func TestCAIssuePEM(t *testing.T) {
|
||||
{
|
||||
name: "only cert",
|
||||
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",
|
||||
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",
|
||||
@ -68,19 +68,19 @@ func TestCAIssuePEM(t *testing.T) {
|
||||
name: "bad cert",
|
||||
caCrtPEM: []byte("this is not a cert"),
|
||||
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",
|
||||
caCrtPEM: goodCACrtPEM0,
|
||||
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",
|
||||
caCrtPEM: goodCACrtPEM0,
|
||||
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",
|
||||
@ -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
|
||||
// run.
|
||||
|
||||
if step.caCrtPEM != nil || step.caKeyPEM != nil {
|
||||
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,
|
||||
)
|
||||
crtPEM, keyPEM, err := issuePEM(provider, ca, step.caCrtPEM, step.caKeyPEM)
|
||||
|
||||
if step.wantError != "" {
|
||||
require.EqualError(t, err, step.wantError)
|
||||
@ -116,13 +106,24 @@ func TestCAIssuePEM(t *testing.T) {
|
||||
require.NotEmpty(t, keyPEM)
|
||||
|
||||
caCrtPEM, _ := provider.CurrentCertKeyContent()
|
||||
crtAssertions := testutil.ValidateCertificate(t, string(caCrtPEM), string(crtPEM))
|
||||
crtAssertions.RequireCommonName("some-common-name")
|
||||
crtAssertions.RequireDNSName("some-dns-name")
|
||||
crtAssertions.RequireDNSName("some-other-dns-name")
|
||||
crtAssertions := testutil.ValidateClientCertificate(t, string(caCrtPEM), string(crtPEM))
|
||||
crtAssertions.RequireCommonName("some-username")
|
||||
crtAssertions.RequireOrganizations([]string{"some-group1", "some-group2"})
|
||||
crtAssertions.RequireLifetime(time.Now(), time.Now().Add(time.Hour*24), time.Minute*10)
|
||||
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"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
|
||||
"go.pinniped.dev/internal/issuer"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/registry/credentialrequest"
|
||||
"go.pinniped.dev/internal/registry/whoamirequest"
|
||||
@ -27,7 +28,7 @@ type Config struct {
|
||||
|
||||
type ExtraConfig struct {
|
||||
Authenticator credentialrequest.TokenCredentialRequestAuthenticator
|
||||
Issuer credentialrequest.CertIssuer
|
||||
Issuer issuer.ClientCertIssuer
|
||||
StartControllersPostStartHook func(ctx context.Context)
|
||||
Scheme *runtime.Scheme
|
||||
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"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||
|
||||
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/concierge/apiserver"
|
||||
conciergescheme "go.pinniped.dev/internal/concierge/scheme"
|
||||
"go.pinniped.dev/internal/config/concierge"
|
||||
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
||||
"go.pinniped.dev/internal/controllermanager"
|
||||
"go.pinniped.dev/internal/downward"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/issuer"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"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
|
||||
// an in-memory cache of what is stored in the k8s Secret, helping to
|
||||
// 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.
|
||||
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
|
||||
// post start hook of the aggregated API server.
|
||||
startControllersFunc, err := controllermanager.PrepareControllers(
|
||||
&controllermanager.Config{
|
||||
ServerInstallationInfo: podInfo,
|
||||
APIGroupSuffix: *cfg.APIGroupSuffix,
|
||||
NamesConfig: &cfg.NamesConfig,
|
||||
Labels: cfg.Labels,
|
||||
KubeCertAgentConfig: &cfg.KubeCertAgentConfig,
|
||||
DiscoveryURLOverride: cfg.DiscoveryInfo.URL,
|
||||
DynamicServingCertProvider: dynamicServingCertProvider,
|
||||
DynamicSigningCertProvider: dynamicSigningCertProvider,
|
||||
ServingCertDuration: time.Duration(*cfg.APIConfig.ServingCertificateConfig.DurationSeconds) * time.Second,
|
||||
ServingCertRenewBefore: time.Duration(*cfg.APIConfig.ServingCertificateConfig.RenewBeforeSeconds) * time.Second,
|
||||
AuthenticatorCache: authenticators,
|
||||
ServerInstallationInfo: podInfo,
|
||||
APIGroupSuffix: *cfg.APIGroupSuffix,
|
||||
NamesConfig: &cfg.NamesConfig,
|
||||
Labels: cfg.Labels,
|
||||
KubeCertAgentConfig: &cfg.KubeCertAgentConfig,
|
||||
DiscoveryURLOverride: cfg.DiscoveryInfo.URL,
|
||||
DynamicServingCertProvider: dynamicServingCertProvider,
|
||||
DynamicSigningCertProvider: dynamicSigningCertProvider,
|
||||
ImpersonationSigningCertProvider: impersonationProxySigningCertProvider,
|
||||
ServingCertDuration: time.Duration(*cfg.APIConfig.ServingCertificateConfig.DurationSeconds) * time.Second,
|
||||
ServingCertRenewBefore: time.Duration(*cfg.APIConfig.ServingCertificateConfig.RenewBeforeSeconds) * time.Second,
|
||||
AuthenticatorCache: authenticators,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
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.
|
||||
aggregatedAPIServerConfig, err := getAggregatedAPIServerConfig(
|
||||
dynamicServingCertProvider,
|
||||
authenticators,
|
||||
dynamiccertauthority.New(dynamicSigningCertProvider),
|
||||
certIssuer,
|
||||
startControllersFunc,
|
||||
*cfg.APIGroupSuffix,
|
||||
scheme,
|
||||
loginGV,
|
||||
identityGV,
|
||||
)
|
||||
if err != nil {
|
||||
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.
|
||||
func getAggregatedAPIServerConfig(
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
authenticator credentialrequest.TokenCredentialRequestAuthenticator,
|
||||
issuer credentialrequest.CertIssuer,
|
||||
issuer issuer.ClientCertIssuer,
|
||||
startControllersPostStartHook func(context.Context),
|
||||
apiGroupSuffix string,
|
||||
scheme *runtime.Scheme,
|
||||
loginConciergeGroupVersion, identityConciergeGroupVersion schema.GroupVersion,
|
||||
) (*apiserver.Config, error) {
|
||||
scheme, loginConciergeGroupVersion, identityConciergeGroupVersion := getAggregatedAPIServerScheme(apiGroupSuffix)
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/spf13/cobra"
|
||||
"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 = `
|
||||
@ -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 {
|
||||
missingNames := []string{}
|
||||
if names == nil {
|
||||
missingNames = append(missingNames, "servingCertificateSecret", "credentialIssuer", "apiService")
|
||||
} else {
|
||||
if names.ServingCertificateSecret == "" {
|
||||
missingNames = append(missingNames, "servingCertificateSecret")
|
||||
}
|
||||
if names.CredentialIssuer == "" {
|
||||
missingNames = append(missingNames, "credentialIssuer")
|
||||
}
|
||||
if names.APIService == "" {
|
||||
missingNames = append(missingNames, "apiService")
|
||||
}
|
||||
names = &NamesConfigSpec{}
|
||||
}
|
||||
if names.ServingCertificateSecret == "" {
|
||||
missingNames = append(missingNames, "servingCertificateSecret")
|
||||
}
|
||||
if names.CredentialIssuer == "" {
|
||||
missingNames = append(missingNames, "credentialIssuer")
|
||||
}
|
||||
if names.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 {
|
||||
return constable.Error("missing required names: " + strings.Join(missingNames, ", "))
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
func TestFromPath(t *testing.T) {
|
||||
@ -21,7 +22,7 @@ func TestFromPath(t *testing.T) {
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "Happy",
|
||||
name: "Fully filled out",
|
||||
yaml: here.Doc(`
|
||||
---
|
||||
discovery:
|
||||
@ -36,13 +37,20 @@ func TestFromPath(t *testing.T) {
|
||||
credentialIssuer: pinniped-config
|
||||
apiService: pinniped-api
|
||||
kubeCertAgentPrefix: kube-cert-agent-prefix
|
||||
impersonationConfigMap: impersonationConfigMap-value
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
labels:
|
||||
myLabelKey1: myLabelValue1
|
||||
myLabelKey2: myLabelValue2
|
||||
KubeCertAgent:
|
||||
kubeCertAgent:
|
||||
namePrefix: kube-cert-agent-name-prefix-
|
||||
image: kube-cert-agent-image
|
||||
imagePullSecrets: [kube-cert-agent-image-pull-secret]
|
||||
logLevel: debug
|
||||
`),
|
||||
wantConfig: &Config{
|
||||
DiscoveryInfo: DiscoveryInfoSpec{
|
||||
@ -56,9 +64,14 @@ func TestFromPath(t *testing.T) {
|
||||
},
|
||||
APIGroupSuffix: stringPtr("some.suffix.com"),
|
||||
NamesConfig: NamesConfigSpec{
|
||||
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
|
||||
CredentialIssuer: "pinniped-config",
|
||||
APIService: "pinniped-api",
|
||||
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",
|
||||
ImpersonationSignerSecret: "impersonationSignerSecret-value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"myLabelKey1": "myLabelValue1",
|
||||
@ -69,6 +82,7 @@ func TestFromPath(t *testing.T) {
|
||||
Image: stringPtr("kube-cert-agent-image"),
|
||||
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
|
||||
credentialIssuer: pinniped-config
|
||||
apiService: pinniped-api
|
||||
impersonationConfigMap: impersonationConfigMap-value
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
`),
|
||||
wantConfig: &Config{
|
||||
DiscoveryInfo: DiscoveryInfoSpec{
|
||||
@ -92,9 +111,14 @@ func TestFromPath(t *testing.T) {
|
||||
},
|
||||
},
|
||||
NamesConfig: NamesConfigSpec{
|
||||
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
|
||||
CredentialIssuer: "pinniped-config",
|
||||
APIService: "pinniped-api",
|
||||
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",
|
||||
ImpersonationSignerSecret: "impersonationSignerSecret-value",
|
||||
},
|
||||
Labels: map[string]string{},
|
||||
KubeCertAgentConfig: KubeCertAgentSpec{
|
||||
@ -104,9 +128,12 @@ func TestFromPath(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
yaml: here.Doc(``),
|
||||
wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, apiService",
|
||||
name: "Empty",
|
||||
yaml: here.Doc(``),
|
||||
wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, " +
|
||||
"apiService, impersonationConfigMap, impersonationLoadBalancerService, " +
|
||||
"impersonationTLSCertificateSecret, impersonationCACertificateSecret, " +
|
||||
"impersonationSignerSecret",
|
||||
},
|
||||
{
|
||||
name: "Missing apiService name",
|
||||
@ -115,6 +142,11 @@ func TestFromPath(t *testing.T) {
|
||||
names:
|
||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||
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",
|
||||
},
|
||||
@ -125,6 +157,11 @@ func TestFromPath(t *testing.T) {
|
||||
names:
|
||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||
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",
|
||||
},
|
||||
@ -135,9 +172,103 @@ func TestFromPath(t *testing.T) {
|
||||
names:
|
||||
credentialIssuer: pinniped-config
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
yaml: here.Doc(`
|
||||
@ -150,6 +281,11 @@ func TestFromPath(t *testing.T) {
|
||||
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
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
`),
|
||||
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
|
||||
credentialIssuer: pinniped-config
|
||||
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",
|
||||
},
|
||||
@ -180,6 +321,11 @@ func TestFromPath(t *testing.T) {
|
||||
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
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
`),
|
||||
wantError: "validate api: renewBefore must be positive",
|
||||
},
|
||||
@ -196,6 +342,11 @@ func TestFromPath(t *testing.T) {
|
||||
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
|
||||
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])?)*')",
|
||||
},
|
||||
|
@ -33,9 +33,14 @@ type APIConfigSpec struct {
|
||||
|
||||
// NamesConfigSpec configures the names of some Kubernetes resources for the Concierge.
|
||||
type NamesConfigSpec struct {
|
||||
ServingCertificateSecret string `json:"servingCertificateSecret"`
|
||||
CredentialIssuer string `json:"credentialIssuer"`
|
||||
APIService string `json:"apiService"`
|
||||
ServingCertificateSecret string `json:"servingCertificateSecret"`
|
||||
CredentialIssuer string `json:"credentialIssuer"`
|
||||
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
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package apicerts
|
||||
@ -64,7 +64,7 @@ func (c *apiServiceUpdaterController) Sync(ctx controllerlib.Context) error {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
@ -112,8 +111,8 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
||||
var aggregatorAPIClient *aggregatorfake.Clientset
|
||||
var kubeInformerClient *kubernetesfake.Clientset
|
||||
var kubeInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
|
||||
// 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().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: installedInNamespace,
|
||||
@ -140,14 +139,14 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
kubeInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
@ -155,7 +154,7 @@ func TestAPIServiceUpdaterControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
package apicerts
|
||||
@ -30,6 +30,8 @@ type certsExpirerController struct {
|
||||
// renewBefore is the amount of time after the cert's issuance where
|
||||
// this controller will start to try to rotate it.
|
||||
renewBefore time.Duration
|
||||
|
||||
secretKey string
|
||||
}
|
||||
|
||||
// NewCertsExpirerController returns a controllerlib.Controller that will delete a
|
||||
@ -42,6 +44,7 @@ func NewCertsExpirerController(
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||
renewBefore time.Duration,
|
||||
secretKey string,
|
||||
) controllerlib.Controller {
|
||||
return controllerlib.New(
|
||||
controllerlib.Config{
|
||||
@ -52,6 +55,7 @@ func NewCertsExpirerController(
|
||||
k8sClient: k8sClient,
|
||||
secretInformer: secretInformer,
|
||||
renewBefore: renewBefore,
|
||||
secretKey: secretKey,
|
||||
},
|
||||
},
|
||||
withInformer(
|
||||
@ -74,13 +78,9 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
notBefore, notAfter, err := getCertBounds(secret)
|
||||
notBefore, notAfter, err := c.getCertBounds(secret)
|
||||
if err != nil {
|
||||
// If we can't read the cert, then really all we can do is log something,
|
||||
// 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
|
||||
return fmt.Errorf("failed to get cert bounds for secret %q with key %q: %w", secret.Name, c.secretKey, err)
|
||||
}
|
||||
|
||||
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
|
||||
// provided secret to contain the well-known data keys from this package (see
|
||||
// certs_manager.go).
|
||||
func getCertBounds(secret *corev1.Secret) (time.Time, time.Time, error) {
|
||||
certPEM := secret.Data[tlsCertificateChainSecretKey]
|
||||
func (c *certsExpirerController) getCertBounds(secret *corev1.Secret) (time.Time, time.Time, error) {
|
||||
certPEM := secret.Data[c.secretKey]
|
||||
if certPEM == nil {
|
||||
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
|
||||
|
||||
package apicerts
|
||||
@ -98,7 +98,8 @@ func TestExpirerControllerFilters(t *testing.T) {
|
||||
nil, // k8sClient, not needed
|
||||
secretsInformer,
|
||||
withInformer.WithInformer,
|
||||
0, // renewBefore, not needed
|
||||
0, // renewBefore, not needed
|
||||
"", // not needed
|
||||
)
|
||||
|
||||
unrelated := corev1.Secret{}
|
||||
@ -115,6 +116,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const certsSecretResourceName = "some-resource-name"
|
||||
const fakeTestKey = "some-awesome-key"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -132,6 +134,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
name: "secret missing key",
|
||||
fillSecretData: func(t *testing.T, m map[string][]byte) {},
|
||||
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",
|
||||
@ -143,8 +146,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// See certs_manager.go for this constant.
|
||||
m["tlsCertificateChain"] = certPEM
|
||||
m[fakeTestKey] = certPEM
|
||||
},
|
||||
wantDelete: false,
|
||||
},
|
||||
@ -158,8 +160,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// See certs_manager.go for this constant.
|
||||
m["tlsCertificateChain"] = certPEM
|
||||
m[fakeTestKey] = certPEM
|
||||
},
|
||||
wantDelete: true,
|
||||
},
|
||||
@ -173,8 +174,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// See certs_manager.go for this constant.
|
||||
m["tlsCertificateChain"] = certPEM
|
||||
m[fakeTestKey] = certPEM
|
||||
},
|
||||
wantDelete: true,
|
||||
},
|
||||
@ -188,8 +188,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// See certs_manager.go for this constant.
|
||||
m["tlsCertificateChain"] = certPEM
|
||||
m[fakeTestKey] = certPEM
|
||||
},
|
||||
configKubeAPIClient: func(c *kubernetesfake.Clientset) {
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
// See certs_manager.go for this constant.
|
||||
m["tlsCertificateChain"], err = x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
m[fakeTestKey], err = x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
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 {
|
||||
@ -216,7 +215,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
||||
@ -253,6 +252,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
test.renewBefore,
|
||||
fakeTestKey,
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
package apicerts
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -21,9 +20,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
caCertificateSecretKey = "caCertificate"
|
||||
tlsPrivateKeySecretKey = "tlsPrivateKey"
|
||||
tlsCertificateChainSecretKey = "tlsCertificateChain"
|
||||
CACertificateSecretKey = "caCertificate"
|
||||
CACertificatePrivateKeySecretKey = "caCertificatePrivateKey"
|
||||
tlsPrivateKeySecretKey = "tlsPrivateKey"
|
||||
TLSCertificateChainSecretKey = "tlsCertificateChain"
|
||||
)
|
||||
|
||||
type certsManagerController struct {
|
||||
@ -93,28 +93,16 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
serviceEndpoint := c.serviceNameForGeneratedCertCommonName + "." + c.namespace + ".svc"
|
||||
aggregatedAPIServerTLSCert, err := aggregatedAPIServerCA.Issue(
|
||||
pkix.Name{CommonName: serviceEndpoint},
|
||||
[]string{serviceEndpoint},
|
||||
nil,
|
||||
c.certDuration,
|
||||
)
|
||||
caPrivateKeyPEM, err := ca.PrivateKeyToPEM()
|
||||
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{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@ -123,11 +111,29 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error {
|
||||
Labels: c.certsSecretLabels,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
caCertificateSecretKey: string(aggregatedAPIServerCA.Bundle()),
|
||||
tlsPrivateKeySecretKey: string(tlsPrivateKeyPEM),
|
||||
tlsCertificateChainSecretKey: string(tlsCertChainPEM),
|
||||
CACertificateSecretKey: string(ca.Bundle()),
|
||||
CACertificatePrivateKeySecretKey: string(caPrivateKeyPEM),
|
||||
},
|
||||
}
|
||||
|
||||
// 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{})
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
package apicerts
|
||||
@ -49,7 +49,7 @@ func TestManagerControllerOptions(t *testing.T) {
|
||||
observableWithInitialEventOption.WithInitialEvent,
|
||||
0,
|
||||
"Pinniped CA",
|
||||
"pinniped-api",
|
||||
"ignored",
|
||||
)
|
||||
secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
|
||||
})
|
||||
@ -118,6 +118,7 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
const installedInNamespace = "some-namespace"
|
||||
const certsSecretResourceName = "some-resource-name"
|
||||
const certDuration = 12345678 * time.Second
|
||||
const defaultServiceName = "pinniped-api"
|
||||
|
||||
var r *require.Assertions
|
||||
|
||||
@ -125,13 +126,13 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
var kubeAPIClient *kubernetesfake.Clientset
|
||||
var kubeInformerClient *kubernetesfake.Clientset
|
||||
var kubeInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
|
||||
// Defer starting the informers until the last possible moment so that the
|
||||
// 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.
|
||||
subject = NewCertsManagerController(
|
||||
installedInNamespace,
|
||||
@ -146,12 +147,12 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
controllerlib.WithInitialEvent,
|
||||
certDuration,
|
||||
"Pinniped CA",
|
||||
"pinniped-api",
|
||||
serviceName,
|
||||
)
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: installedInNamespace,
|
||||
@ -160,14 +161,14 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
kubeInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
@ -175,7 +176,7 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
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() {
|
||||
startInformersAndController()
|
||||
startInformersAndController(defaultServiceName)
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
r.NoError(err)
|
||||
|
||||
@ -208,23 +209,54 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
"myLabelKey2": "myLabelValue2",
|
||||
}, actualSecret.Labels)
|
||||
actualCACert := actualSecret.StringData["caCertificate"]
|
||||
actualCAPrivateKey := actualSecret.StringData["caCertificatePrivateKey"]
|
||||
actualPrivateKey := actualSecret.StringData["tlsPrivateKey"]
|
||||
actualCertChain := actualSecret.StringData["tlsCertificateChain"]
|
||||
r.NotEmpty(actualCACert)
|
||||
r.NotEmpty(actualCAPrivateKey)
|
||||
r.NotEmpty(actualPrivateKey)
|
||||
r.NotEmpty(actualCertChain)
|
||||
r.Len(actualSecret.StringData, 4)
|
||||
|
||||
// Validate the created CA's lifetime.
|
||||
validCACert := testutil.ValidateCertificate(t, actualCACert, actualCACert)
|
||||
validCACert := testutil.ValidateServerCertificate(t, actualCACert, actualCACert)
|
||||
validCACert.RequireMatchesPrivateKey(actualCAPrivateKey)
|
||||
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
|
||||
validCert := testutil.ValidateCertificate(t, actualCACert, actualCertChain)
|
||||
validCert := testutil.ValidateServerCertificate(t, actualCACert, actualCertChain)
|
||||
validCert.RequireDNSName("pinniped-api." + installedInNamespace + ".svc")
|
||||
validCert.RequireLifetime(time.Now(), time.Now().Add(certDuration), 6*time.Minute)
|
||||
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() {
|
||||
it.Before(func() {
|
||||
kubeAPIClient.PrependReactor(
|
||||
@ -237,7 +269,7 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it("returns the create error", func() {
|
||||
startInformersAndController()
|
||||
startInformersAndController(defaultServiceName)
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
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() {
|
||||
startInformersAndController()
|
||||
startInformersAndController(defaultServiceName)
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
r.NoError(err)
|
||||
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
|
||||
|
||||
package apicerts
|
||||
@ -18,14 +18,14 @@ import (
|
||||
type certsObserverController struct {
|
||||
namespace string
|
||||
certsSecretResourceName string
|
||||
dynamicCertProvider dynamiccert.Provider
|
||||
dynamicCertProvider dynamiccert.Private
|
||||
secretInformer corev1informers.SecretInformer
|
||||
}
|
||||
|
||||
func NewCertsObserverController(
|
||||
namespace string,
|
||||
certsSecretResourceName string,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||
) controllerlib.Controller {
|
||||
@ -57,12 +57,15 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error {
|
||||
if notFound {
|
||||
klog.Info("certsObserverController Sync found that 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
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
|
||||
|
||||
package apicerts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -16,6 +17,7 @@ import (
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
@ -95,6 +97,7 @@ func TestObserverControllerInformerFilters(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) {
|
||||
const installedInNamespace = "some-namespace"
|
||||
const certsSecretResourceName = "some-resource-name"
|
||||
@ -104,10 +107,10 @@ func TestObserverControllerSync(t *testing.T) {
|
||||
var subject controllerlib.Controller
|
||||
var kubeInformerClient *kubernetesfake.Clientset
|
||||
var kubeInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
var dynamicCertProvider dynamiccert.Provider
|
||||
var dynamicCertProvider dynamiccert.Private
|
||||
|
||||
// Defer starting the informers until the last possible moment so that the
|
||||
// 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().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: installedInNamespace,
|
||||
@ -132,22 +135,22 @@ func TestObserverControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
kubeInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
dynamicCertProvider = dynamiccert.New()
|
||||
dynamicCertProvider = dynamiccert.NewServingCert(name)
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
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)
|
||||
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() {
|
||||
@ -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() {
|
||||
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{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: certsSecretResourceName,
|
||||
@ -184,24 +212,29 @@ func TestObserverControllerSync(t *testing.T) {
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"caCertificate": []byte("fake cert"),
|
||||
"tlsPrivateKey": []byte("fake private key"),
|
||||
"tlsCertificateChain": []byte("fake cert chain"),
|
||||
"tlsPrivateKey": key,
|
||||
"tlsCertificateChain": crt,
|
||||
},
|
||||
}
|
||||
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
||||
err = kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
||||
r.NoError(err)
|
||||
|
||||
dynamicCertProvider.Set(nil, nil)
|
||||
dynamicCertProvider.UnsetCertKeyContent()
|
||||
})
|
||||
|
||||
it("updates the dynamicCertProvider's cert and key", func() {
|
||||
startInformersAndController()
|
||||
|
||||
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||
r.Nil(actualCertChain)
|
||||
r.Nil(actualKey)
|
||||
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
r.NoError(err)
|
||||
|
||||
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||
r.Equal("fake cert chain", string(actualCertChain))
|
||||
r.Equal("fake private key", string(actualKey))
|
||||
actualCertChain, actualKey = dynamicCertProvider.CurrentCertKeyContent()
|
||||
r.True(strings.HasPrefix(string(actualCertChain), `-----BEGIN CERTIFICATE-----`), "not a cert:\n%s", string(actualCertChain))
|
||||
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)
|
||||
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()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
r.NoError(err)
|
||||
|
||||
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||
r.Nil(actualCertChain)
|
||||
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{}))
|
||||
|
@ -6,7 +6,6 @@ package cachecleaner
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -150,7 +149,7 @@ func TestController(t *testing.T) {
|
||||
jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators()
|
||||
controller := New(cache, webhooks, jwtAuthenticators, testLog)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
informers.Start(ctx.Done())
|
||||
|
@ -325,7 +325,7 @@ func TestController(t *testing.T) {
|
||||
|
||||
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()
|
||||
|
||||
informers.Start(ctx.Done())
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
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)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
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
|
||||
|
||||
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) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s sortableStrategies) Len() int { return len(s) }
|
||||
func (s sortableStrategies) Less(i, j int) bool {
|
||||
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
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
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 agentInformers kubeinformers.SharedInformerFactory
|
||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
var controllerManagerPod, agentPod *corev1.Pod
|
||||
var podsGVR schema.GroupVersionResource
|
||||
@ -116,7 +116,7 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: kubeSystemNamespace,
|
||||
@ -125,8 +125,8 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeSystemInformers.Start(timeoutContext.Done())
|
||||
agentInformers.Start(timeoutContext.Done())
|
||||
kubeSystemInformers.Start(cancelContext.Done())
|
||||
agentInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
||||
|
||||
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
|
||||
kubeSystemNamespace, agentPodNamespace, certPath, keyPath,
|
||||
@ -173,7 +173,7 @@ func TestAnnotaterControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
when("there is an agent pod without annotations set", func() {
|
||||
|
@ -94,8 +94,8 @@ func TestCreaterControllerSync(t *testing.T) {
|
||||
var agentInformerClient *kubernetesfake.Clientset
|
||||
var agentInformers kubeinformers.SharedInformerFactory
|
||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
var controllerManagerPod, agentPod *corev1.Pod
|
||||
var podsGVR schema.GroupVersionResource
|
||||
@ -135,7 +135,7 @@ func TestCreaterControllerSync(t *testing.T) {
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: kubeSystemNamespace,
|
||||
@ -144,8 +144,8 @@ func TestCreaterControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeSystemInformers.Start(timeoutContext.Done())
|
||||
agentInformers.Start(timeoutContext.Done())
|
||||
kubeSystemInformers.Start(cancelContext.Done())
|
||||
agentInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ func TestCreaterControllerSync(t *testing.T) {
|
||||
|
||||
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
controllerManagerPod, agentPod = exampleControllerManagerAndAgentPods(
|
||||
kubeSystemNamespace, agentPodNamespace, "ignored for this test", "ignored for this test",
|
||||
@ -201,7 +201,7 @@ func TestCreaterControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
package kubecertagent
|
||||
@ -6,7 +6,6 @@ package kubecertagent
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
@ -57,8 +56,8 @@ func TestDeleterControllerSync(t *testing.T) {
|
||||
var kubeSystemInformers kubeinformers.SharedInformerFactory
|
||||
var agentInformerClient *kubernetesfake.Clientset
|
||||
var agentInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
var controllerManagerPod, agentPod *corev1.Pod
|
||||
var podsGVR schema.GroupVersionResource
|
||||
@ -85,7 +84,7 @@ func TestDeleterControllerSync(t *testing.T) {
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: kubeSystemNamespace,
|
||||
@ -94,8 +93,8 @@ func TestDeleterControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeSystemInformers.Start(timeoutContext.Done())
|
||||
agentInformers.Start(timeoutContext.Done())
|
||||
kubeSystemInformers.Start(cancelContext.Done())
|
||||
agentInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
@ -109,7 +108,7 @@ func TestDeleterControllerSync(t *testing.T) {
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||
|
||||
@ -139,7 +138,7 @@ func TestDeleterControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
when("there is an agent pod", func() {
|
||||
|
@ -11,9 +11,9 @@ import (
|
||||
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"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
@ -33,7 +33,7 @@ type execerController struct {
|
||||
credentialIssuerLocationConfig *CredentialIssuerLocationConfig
|
||||
credentialIssuerLabels map[string]string
|
||||
discoveryURLOverride *string
|
||||
dynamicCertProvider dynamiccert.Provider
|
||||
dynamicCertProvider dynamiccert.Private
|
||||
podCommandExecutor PodCommandExecutor
|
||||
clock clock.Clock
|
||||
pinnipedAPIClient pinnipedclientset.Interface
|
||||
@ -51,7 +51,7 @@ func NewExecerController(
|
||||
credentialIssuerLocationConfig *CredentialIssuerLocationConfig,
|
||||
credentialIssuerLabels map[string]string,
|
||||
discoveryURLOverride *string,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
podCommandExecutor PodCommandExecutor,
|
||||
pinnipedAPIClient pinnipedclientset.Interface,
|
||||
clock clock.Clock,
|
||||
@ -119,8 +119,7 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
|
||||
c.pinnipedAPIClient,
|
||||
strategyError(c.clock, err),
|
||||
)
|
||||
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success")
|
||||
return err
|
||||
return newAggregate(err, strategyResultUpdateErr)
|
||||
}
|
||||
|
||||
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,
|
||||
strategyError(c.clock, err),
|
||||
)
|
||||
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success")
|
||||
return err
|
||||
return newAggregate(err, strategyResultUpdateErr)
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
@ -153,8 +161,7 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
|
||||
LastUpdateTime: metav1.NewTime(c.clock.Now()),
|
||||
},
|
||||
)
|
||||
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success")
|
||||
return err
|
||||
return newAggregate(err, strategyResultUpdateErr)
|
||||
}
|
||||
|
||||
return issuerconfig.UpdateStrategy(
|
||||
@ -219,3 +226,7 @@ func (c *execerController) getKeypairFilePaths(pod *v1.Pod) (string, string) {
|
||||
|
||||
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) {
|
||||
name := t.Name()
|
||||
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
|
||||
const agentPodNamespace = "some-namespace"
|
||||
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 fakeCertPath = "/some/cert/path"
|
||||
const fakeKeyPath = "/some/key/path"
|
||||
const defaultDynamicCertProviderCert = "initial-cert"
|
||||
const defaultDynamicCertProviderKey = "initial-key"
|
||||
const credentialIssuerResourceName = "ci-resource-name"
|
||||
|
||||
var r *require.Assertions
|
||||
|
||||
var subject controllerlib.Controller
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||
var kubeInformerFactory kubeinformers.SharedInformerFactory
|
||||
@ -159,6 +158,8 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
var fakeCertPEM, fakeKeyPEM string
|
||||
var credentialIssuerGVR schema.GroupVersionResource
|
||||
var frozenNow time.Time
|
||||
var defaultDynamicCertProviderCert string
|
||||
var defaultDynamicCertProviderKey string
|
||||
|
||||
// Defer starting the informers until the last possible moment so that the
|
||||
// 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().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: agentPodNamespace,
|
||||
@ -190,7 +191,7 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformerFactory.Start(timeoutContext.Done())
|
||||
kubeInformerFactory.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
@ -228,14 +229,23 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
it.Before(func() {
|
||||
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()
|
||||
kubeClientset = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformerFactory = kubeinformers.NewSharedInformerFactory(kubeClientset, 0)
|
||||
fakeExecutor = &fakePodExecutor{r: r}
|
||||
frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
|
||||
dynamicCertProvider = dynamiccert.New()
|
||||
dynamicCertProvider.Set([]byte(defaultDynamicCertProviderCert), []byte(defaultDynamicCertProviderKey))
|
||||
dynamicCertProvider = dynamiccert.NewCA(name)
|
||||
err = dynamicCertProvider.SetCertKeyContent([]byte(defaultDynamicCertProviderCert), []byte(defaultDynamicCertProviderKey))
|
||||
r.NoError(err)
|
||||
|
||||
loadFile := func(filename string) string {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
@ -253,7 +263,7 @@ func TestManagerControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
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())
|
||||
})
|
||||
})
|
||||
|
||||
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{}))
|
||||
}
|
||||
|
@ -103,8 +103,8 @@ func TestSync(t *testing.T) {
|
||||
var federationDomainInformerClient *pinnipedfake.Clientset
|
||||
var federationDomainInformers pinnipedinformers.SharedInformerFactory
|
||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var cancelContext context.Context
|
||||
var cancelContextCancelFunc context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
var frozenNow time.Time
|
||||
var providersSetter *fakeProvidersSetter
|
||||
@ -124,7 +124,7 @@ func TestSync(t *testing.T) {
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: namespace,
|
||||
@ -133,7 +133,7 @@ func TestSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
federationDomainInformers.Start(timeoutContext.Done())
|
||||
federationDomainInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ func TestSync(t *testing.T) {
|
||||
providersSetter = &fakeProvidersSetter{}
|
||||
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()
|
||||
federationDomainInformers = pinnipedinformers.NewSharedInformerFactory(federationDomainInformerClient, 0)
|
||||
@ -157,7 +157,7 @@ func TestSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
when("there are some valid FederationDomains in the informer", func() {
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -630,7 +629,7 @@ func TestFederationDomainSecretsControllerSync(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
@ -412,7 +411,7 @@ func TestSupervisorSecretsControllerSync(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.
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if test.generateKey != nil {
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
@ -124,16 +123,16 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
||||
const installedInNamespace = "some-namespace"
|
||||
|
||||
var (
|
||||
r *require.Assertions
|
||||
subject controllerlib.Controller
|
||||
pinnipedInformerClient *pinnipedfake.Clientset
|
||||
kubeInformerClient *kubernetesfake.Clientset
|
||||
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||
kubeInformers kubeinformers.SharedInformerFactory
|
||||
timeoutContext context.Context
|
||||
timeoutContextCancel context.CancelFunc
|
||||
syncContext *controllerlib.Context
|
||||
issuerToJWKSSetter *fakeIssuerToJWKSMapSetter
|
||||
r *require.Assertions
|
||||
subject controllerlib.Controller
|
||||
pinnipedInformerClient *pinnipedfake.Clientset
|
||||
kubeInformerClient *kubernetesfake.Clientset
|
||||
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||
kubeInformers kubeinformers.SharedInformerFactory
|
||||
cancelContext context.Context
|
||||
cancelContextCancelFunc context.CancelFunc
|
||||
syncContext *controllerlib.Context
|
||||
issuerToJWKSSetter *fakeIssuerToJWKSMapSetter
|
||||
)
|
||||
|
||||
// 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().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: installedInNamespace,
|
||||
@ -158,15 +157,15 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
pinnipedInformers.Start(timeoutContext.Done())
|
||||
kubeInformers.Start(cancelContext.Done())
|
||||
pinnipedInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
@ -184,7 +183,7 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
when("there are no FederationDomains and no JWKS Secrets yet", func() {
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@ -674,7 +673,7 @@ func TestJWKSWriterControllerSync(t *testing.T) {
|
||||
return goodKey, test.generateKeyErr
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
@ -130,16 +129,16 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
)
|
||||
|
||||
var (
|
||||
r *require.Assertions
|
||||
subject controllerlib.Controller
|
||||
pinnipedInformerClient *pinnipedfake.Clientset
|
||||
kubeInformerClient *kubernetesfake.Clientset
|
||||
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||
kubeInformers kubeinformers.SharedInformerFactory
|
||||
timeoutContext context.Context
|
||||
timeoutContextCancel context.CancelFunc
|
||||
syncContext *controllerlib.Context
|
||||
issuerTLSCertSetter *fakeIssuerTLSCertSetter
|
||||
r *require.Assertions
|
||||
subject controllerlib.Controller
|
||||
pinnipedInformerClient *pinnipedfake.Clientset
|
||||
kubeInformerClient *kubernetesfake.Clientset
|
||||
pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||
kubeInformers kubeinformers.SharedInformerFactory
|
||||
cancelContext context.Context
|
||||
cancelContextCancelFunc context.CancelFunc
|
||||
syncContext *controllerlib.Context
|
||||
issuerTLSCertSetter *fakeIssuerTLSCertSetter
|
||||
)
|
||||
|
||||
// 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().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: installedInNamespace,
|
||||
@ -165,8 +164,8 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
pinnipedInformers.Start(timeoutContext.Done())
|
||||
kubeInformers.Start(cancelContext.Done())
|
||||
pinnipedInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
@ -179,7 +178,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
@ -197,7 +196,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
when("there are no FederationDomains and no TLS Secrets yet", func() {
|
||||
|
@ -624,7 +624,7 @@ func TestController(t *testing.T) {
|
||||
controllerlib.WithInformer,
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
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
|
||||
|
||||
package supervisorstorage
|
||||
@ -108,16 +108,16 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
||||
)
|
||||
|
||||
var (
|
||||
r *require.Assertions
|
||||
subject controllerlib.Controller
|
||||
kubeInformerClient *kubernetesfake.Clientset
|
||||
kubeClient *kubernetesfake.Clientset
|
||||
kubeInformers kubeinformers.SharedInformerFactory
|
||||
timeoutContext context.Context
|
||||
timeoutContextCancel context.CancelFunc
|
||||
syncContext *controllerlib.Context
|
||||
fakeClock *clock.FakeClock
|
||||
frozenNow time.Time
|
||||
r *require.Assertions
|
||||
subject controllerlib.Controller
|
||||
kubeInformerClient *kubernetesfake.Clientset
|
||||
kubeClient *kubernetesfake.Clientset
|
||||
kubeInformers kubeinformers.SharedInformerFactory
|
||||
cancelContext context.Context
|
||||
cancelContextCancelFunc context.CancelFunc
|
||||
syncContext *controllerlib.Context
|
||||
fakeClock *clock.FakeClock
|
||||
frozenNow time.Time
|
||||
)
|
||||
|
||||
// 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().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: "",
|
||||
@ -142,14 +142,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
kubeInformers.Start(cancelContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeClient = kubernetesfake.NewSimpleClientset()
|
||||
@ -168,7 +168,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
cancelContextCancelFunc()
|
||||
})
|
||||
|
||||
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"
|
||||
pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
|
||||
"go.pinniped.dev/internal/apiserviceref"
|
||||
"go.pinniped.dev/internal/concierge/impersonator"
|
||||
"go.pinniped.dev/internal/config/concierge"
|
||||
"go.pinniped.dev/internal/controller/apicerts"
|
||||
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
||||
"go.pinniped.dev/internal/controller/authenticator/cachecleaner"
|
||||
"go.pinniped.dev/internal/controller/authenticator/jwtcachefiller"
|
||||
"go.pinniped.dev/internal/controller/authenticator/webhookcachefiller"
|
||||
"go.pinniped.dev/internal/controller/impersonatorconfig"
|
||||
"go.pinniped.dev/internal/controller/kubecertagent"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/deploymentref"
|
||||
@ -61,13 +63,23 @@ type Config struct {
|
||||
DiscoveryURLOverride *string
|
||||
|
||||
// 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
|
||||
// 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 time.Duration
|
||||
|
||||
// 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
|
||||
// certificate.
|
||||
@ -179,6 +191,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
||||
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
c.ServingCertRenewBefore,
|
||||
apicerts.TLSCertificateChainSecretKey,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
@ -266,6 +279,58 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
||||
klogr.New(),
|
||||
),
|
||||
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.
|
||||
|
@ -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
|
||||
|
||||
package dynamiccert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"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 {
|
||||
dynamiccertificates.CertKeyContentProvider
|
||||
Set(certPEM, keyPEM []byte)
|
||||
Private
|
||||
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 {
|
||||
certPEM []byte
|
||||
keyPEM []byte
|
||||
mutex sync.RWMutex
|
||||
// these fields are constant after struct initialization and thus do not need locking
|
||||
name string
|
||||
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.
|
||||
func New() Provider {
|
||||
return &provider{}
|
||||
// NewServingCert returns a Private that is go routine safe.
|
||||
// It can only hold key pairs that have IsCA=false.
|
||||
func NewServingCert(name string) Private {
|
||||
return &provider{name: name}
|
||||
}
|
||||
|
||||
func (p *provider) Set(certPEM, keyPEM []byte) {
|
||||
p.mutex.Lock() // acquire a write lock
|
||||
defer p.mutex.Unlock()
|
||||
p.certPEM = certPEM
|
||||
p.keyPEM = keyPEM
|
||||
// NewCA returns a Provider that is go routine safe.
|
||||
// It can only hold key pairs that have IsCA=true.
|
||||
func NewCA(name string) Provider {
|
||||
return &provider{name: name, isCA: true}
|
||||
}
|
||||
|
||||
func (p *provider) Name() string {
|
||||
return "DynamicCertProvider"
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *provider) CurrentCertKeyContent() (cert []byte, key []byte) {
|
||||
p.mutex.RLock() // acquire a read lock
|
||||
p.mutex.RLock()
|
||||
defer p.mutex.RUnlock()
|
||||
|
||||
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
|
||||
|
||||
package securityheader
|
||||
@ -22,7 +22,7 @@ func TestWrap(t *testing.T) {
|
||||
})))
|
||||
t.Cleanup(testServer.Close)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
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.
|
||||
// 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
|
||||
|
||||
import (
|
||||
context "context"
|
||||
pkix "crypto/x509/pkix"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
login "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||
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.
|
||||
type MockTokenCredentialRequestAuthenticator struct {
|
||||
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
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -22,20 +21,17 @@ import (
|
||||
"k8s.io/utils/trace"
|
||||
|
||||
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.
|
||||
const clientCertificateTTL = 5 * time.Minute
|
||||
|
||||
type CertIssuer interface {
|
||||
IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error)
|
||||
}
|
||||
|
||||
type TokenCredentialRequestAuthenticator interface {
|
||||
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{
|
||||
authenticator: authenticator,
|
||||
issuer: issuer,
|
||||
@ -45,7 +41,7 @@ func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer CertIssue
|
||||
|
||||
type REST struct {
|
||||
authenticator TokenCredentialRequestAuthenticator
|
||||
issuer CertIssuer
|
||||
issuer issuer.ClientCertIssuer
|
||||
tableConvertor rest.TableConvertor
|
||||
}
|
||||
|
||||
@ -100,30 +96,23 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := r.authenticator.AuthenticateTokenCredentialRequest(ctx, credentialRequest)
|
||||
userInfo, err := r.authenticator.AuthenticateTokenCredentialRequest(ctx, credentialRequest)
|
||||
if err != nil {
|
||||
traceFailureWithError(t, "token authentication", err)
|
||||
return failureResponse(), nil
|
||||
}
|
||||
if user == nil || user.GetName() == "" {
|
||||
traceSuccess(t, user, false)
|
||||
if userInfo == nil || userInfo.GetName() == "" {
|
||||
traceSuccess(t, userInfo, false)
|
||||
return failureResponse(), nil
|
||||
}
|
||||
|
||||
certPEM, keyPEM, err := r.issuer.IssuePEM(
|
||||
pkix.Name{
|
||||
CommonName: user.GetName(),
|
||||
Organization: user.GetGroups(),
|
||||
},
|
||||
[]string{},
|
||||
clientCertificateTTL,
|
||||
)
|
||||
certPEM, keyPEM, err := r.issuer.IssueClientCertPEM(userInfo.GetName(), userInfo.GetGroups(), clientCertificateTTL)
|
||||
if err != nil {
|
||||
traceFailureWithError(t, "cert issuer", err)
|
||||
return failureResponse(), nil
|
||||
}
|
||||
|
||||
traceSuccess(t, user, true)
|
||||
traceSuccess(t, userInfo, true)
|
||||
|
||||
return &loginapi.TokenCredentialRequest{
|
||||
Status: loginapi.TokenCredentialRequestStatus{
|
||||
|
@ -5,7 +5,6 @@ package credentialrequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
@ -24,7 +23,9 @@ import (
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
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/issuermocks"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
@ -88,16 +89,14 @@ func TestCreate(t *testing.T) {
|
||||
Groups: []string{"test-group-1", "test-group-2"},
|
||||
}, nil)
|
||||
|
||||
issuer := credentialrequestmocks.NewMockCertIssuer(ctrl)
|
||||
issuer.EXPECT().IssuePEM(
|
||||
pkix.Name{
|
||||
CommonName: "test-user",
|
||||
Organization: []string{"test-group-1", "test-group-2"}},
|
||||
[]string{},
|
||||
clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl)
|
||||
clientCertIssuer.EXPECT().IssueClientCertPEM(
|
||||
"test-user",
|
||||
[]string{"test-group-1", "test-group-2"},
|
||||
5*time.Minute,
|
||||
).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)
|
||||
|
||||
@ -131,12 +130,12 @@ func TestCreate(t *testing.T) {
|
||||
Groups: []string{"test-group-1", "test-group-2"},
|
||||
}, nil)
|
||||
|
||||
issuer := credentialrequestmocks.NewMockCertIssuer(ctrl)
|
||||
issuer.EXPECT().
|
||||
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl)
|
||||
clientCertIssuer.EXPECT().
|
||||
IssueClientCertPEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
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)
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
@ -353,12 +352,12 @@ func requireSuccessfulResponseWithAuthenticationFailureMessage(t *testing.T, err
|
||||
})
|
||||
}
|
||||
|
||||
func successfulIssuer(ctrl *gomock.Controller) CertIssuer {
|
||||
issuer := credentialrequestmocks.NewMockCertIssuer(ctrl)
|
||||
issuer.EXPECT().
|
||||
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
func successfulIssuer(ctrl *gomock.Controller) issuer.ClientCertIssuer {
|
||||
clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl)
|
||||
clientCertIssuer.EXPECT().
|
||||
IssueClientCertPEM(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return([]byte("test-cert"), []byte("test-key"), nil)
|
||||
return issuer
|
||||
return clientCertIssuer
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
package secret
|
||||
@ -68,7 +68,7 @@ func TestCacheSynchronized(t *testing.T) {
|
||||
c.SetStateEncoderHashKey(issuer, stateEncoderHashKey)
|
||||
c.SetStateEncoderBlockKey(issuer, stateEncoderBlockKey)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
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
|
||||
|
||||
package testutil
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -25,10 +26,18 @@ type ValidCert struct {
|
||||
parsed *x509.Certificate
|
||||
}
|
||||
|
||||
// ValidateCertificate validates a certificate and provides an object for asserting properties of the certificate.
|
||||
func ValidateCertificate(t *testing.T, caPEM string, certPEM string) *ValidCert {
|
||||
// ValidateServerCertificate validates a certificate and provides an object for asserting properties of the certificate.
|
||||
func ValidateServerCertificate(t *testing.T, caPEM string, certPEM string) *ValidCert {
|
||||
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))
|
||||
require.NotNil(t, block)
|
||||
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.
|
||||
roots := x509.NewCertPool()
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
||||
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.
|
||||
func (v *ValidCert) RequireLifetime(expectNotBefore time.Time, expectNotAfter time.Time, delta time.Duration) {
|
||||
v.t.Helper()
|
||||
@ -81,6 +122,11 @@ func (v *ValidCert) RequireCommonName(commonName string) {
|
||||
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
|
||||
// representation of the certificate and its private key. The returned certificate is capable of
|
||||
// signing child certificates.
|
||||
|
@ -6,6 +6,7 @@ package testlogger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/stdr"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Logger implements logr.Logger in a way that captures logs for test assertions.
|
||||
@ -46,6 +48,12 @@ func (l *Logger) Lines() []string {
|
||||
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.
|
||||
type syncBuffer struct {
|
||||
mutex sync.Mutex
|
||||
@ -57,3 +65,18 @@ func (s *syncBuffer) Write(p []byte) (n int, err error) {
|
||||
defer s.mutex.Unlock()
|
||||
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