Add WhoAmIRequest Aggregated Virtual REST API
This change adds a new virtual aggregated API that can be used by any user to echo back who they are currently authenticated as. This has general utility to end users and can be used in tests to validate if authentication was successful. Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
parent
62630d6449
commit
abc941097c
8
apis/concierge/identity/doc.go.tmpl
Normal file
8
apis/concierge/identity/doc.go.tmpl
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package identity is the internal version of the Pinniped identity API.
|
||||
package identity
|
38
apis/concierge/identity/register.go.tmpl
Normal file
38
apis/concierge/identity/register.go.tmpl
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "identity.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
// Kind takes an unqualified kind and returns back a Group qualified GroupKind.
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns back a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WhoAmIRequest{},
|
||||
&WhoAmIRequestList{},
|
||||
)
|
||||
return nil
|
||||
}
|
37
apis/concierge/identity/types_userinfo.go.tmpl
Normal file
37
apis/concierge/identity/types_userinfo.go.tmpl
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import "fmt"
|
||||
|
||||
// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it.
|
||||
// Copied from the Kubernetes token review API.
|
||||
type KubernetesUserInfo struct {
|
||||
// User is the UserInfo associated with the current user.
|
||||
User UserInfo
|
||||
// Audiences are audience identifiers chosen by the authenticator.
|
||||
Audiences []string
|
||||
}
|
||||
|
||||
// UserInfo holds the information about the user needed to implement the
|
||||
// user.Info interface.
|
||||
type UserInfo struct {
|
||||
// The name that uniquely identifies this user among all active users.
|
||||
Username string
|
||||
// A unique value that identifies this user across time. If this user is
|
||||
// deleted and another user by the same name is added, they will have
|
||||
// different UIDs.
|
||||
UID string
|
||||
// The names of groups this user is a part of.
|
||||
Groups []string
|
||||
// Any additional information provided by the authenticator.
|
||||
Extra map[string]ExtraValue
|
||||
}
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
type ExtraValue []string
|
||||
|
||||
func (t ExtraValue) String() string {
|
||||
return fmt.Sprintf("%v", []string(t))
|
||||
}
|
40
apis/concierge/identity/types_whoami.go.tmpl
Normal file
40
apis/concierge/identity/types_whoami.go.tmpl
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequest struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ObjectMeta
|
||||
|
||||
Spec WhoAmIRequestSpec
|
||||
Status WhoAmIRequestStatus
|
||||
}
|
||||
|
||||
type WhoAmIRequestSpec struct {
|
||||
// empty for now but we may add some config here in the future
|
||||
// any such config must be safe in the context of an unauthenticated user
|
||||
}
|
||||
|
||||
type WhoAmIRequestStatus struct {
|
||||
// The current authenticated user, exactly as Kubernetes understands it.
|
||||
KubernetesUserInfo KubernetesUserInfo
|
||||
|
||||
// We may add concierge specific information here in the future.
|
||||
}
|
||||
|
||||
// WhoAmIRequestList is a list of WhoAmIRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of WhoAmIRequest
|
||||
Items []WhoAmIRequest
|
||||
}
|
4
apis/concierge/identity/v1alpha1/conversion.go.tmpl
Normal file
4
apis/concierge/identity/v1alpha1/conversion.go.tmpl
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
12
apis/concierge/identity/v1alpha1/defaults.go.tmpl
Normal file
12
apis/concierge/identity/v1alpha1/defaults.go.tmpl
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
11
apis/concierge/identity/v1alpha1/doc.go.tmpl
Normal file
11
apis/concierge/identity/v1alpha1/doc.go.tmpl
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/identity
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped identity API.
|
||||
package v1alpha1
|
43
apis/concierge/identity/v1alpha1/register.go.tmpl
Normal file
43
apis/concierge/identity/v1alpha1/register.go.tmpl
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "identity.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WhoAmIRequest{},
|
||||
&WhoAmIRequestList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
41
apis/concierge/identity/v1alpha1/types_userinfo.go.tmpl
Normal file
41
apis/concierge/identity/v1alpha1/types_userinfo.go.tmpl
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import "fmt"
|
||||
|
||||
// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it.
|
||||
// Copied from the Kubernetes token review API.
|
||||
type KubernetesUserInfo struct {
|
||||
// User is the UserInfo associated with the current user.
|
||||
User UserInfo `json:"user"`
|
||||
// Audiences are audience identifiers chosen by the authenticator.
|
||||
// +optional
|
||||
Audiences []string `json:"audiences,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo holds the information about the user needed to implement the
|
||||
// user.Info interface.
|
||||
type UserInfo struct {
|
||||
// The name that uniquely identifies this user among all active users.
|
||||
Username string `json:"username"`
|
||||
// A unique value that identifies this user across time. If this user is
|
||||
// deleted and another user by the same name is added, they will have
|
||||
// different UIDs.
|
||||
// +optional
|
||||
UID string `json:"uid,omitempty"`
|
||||
// The names of groups this user is a part of.
|
||||
// +optional
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// Any additional information provided by the authenticator.
|
||||
// +optional
|
||||
Extra map[string]ExtraValue `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
type ExtraValue []string
|
||||
|
||||
func (t ExtraValue) String() string {
|
||||
return fmt.Sprintf("%v", []string(t))
|
||||
}
|
43
apis/concierge/identity/v1alpha1/types_whoami.go.tmpl
Normal file
43
apis/concierge/identity/v1alpha1/types_whoami.go.tmpl
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +genclient:onlyVerbs=create
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec WhoAmIRequestSpec `json:"spec,omitempty"`
|
||||
Status WhoAmIRequestStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type WhoAmIRequestSpec struct {
|
||||
// empty for now but we may add some config here in the future
|
||||
// any such config must be safe in the context of an unauthenticated user
|
||||
}
|
||||
|
||||
type WhoAmIRequestStatus struct {
|
||||
// The current authenticated user, exactly as Kubernetes understands it.
|
||||
KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"`
|
||||
|
||||
// We may add concierge specific information here in the future.
|
||||
}
|
||||
|
||||
// WhoAmIRequestList is a list of WhoAmIRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequestList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Items is a list of WhoAmIRequest
|
||||
Items []WhoAmIRequest `json:"items"`
|
||||
}
|
14
apis/concierge/identity/validation/validation.go.tmpl
Normal file
14
apis/concierge/identity/validation/validation.go.tmpl
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
identityapi "go.pinniped.dev/GENERATED_PKG/apis/concierge/identity"
|
||||
)
|
||||
|
||||
func ValidateWhoAmIRequest(whoAmIRequest *identityapi.WhoAmIRequest) field.ErrorList {
|
||||
return nil // add validation for spec here if we expand it
|
||||
}
|
@ -31,6 +31,7 @@ type TokenCredentialRequestStatus struct {
|
||||
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +genclient:onlyVerbs=create
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
@ -108,7 +108,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
f.StringVar(&namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
||||
f.StringVar(&flags.concierge.authenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)")
|
||||
f.StringVar(&flags.concierge.authenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name (default: autodiscover)")
|
||||
f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix")
|
||||
f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
|
||||
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
|
||||
f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)")
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||
@ -93,7 +94,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
|
||||
mustMarkHidden(cmd, "debug-session-cache")
|
||||
mustMarkRequired(cmd, "issuer")
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
@ -67,7 +68,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix")
|
||||
cmd.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")
|
||||
|
@ -204,3 +204,19 @@ spec:
|
||||
name: #@ defaultResourceNameWithSuffix("api")
|
||||
namespace: #@ namespace()
|
||||
port: 443
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("v1alpha1.identity.concierge")
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
version: v1alpha1
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("identity.concierge")
|
||||
groupPriorityMinimum: 9900
|
||||
versionPriority: 15
|
||||
#! caBundle: Do not include this key here. Starts out null, will be updated/owned by the golang code.
|
||||
service:
|
||||
name: #@ defaultResourceNameWithSuffix("api")
|
||||
namespace: #@ namespace()
|
||||
port: 443
|
||||
|
@ -133,18 +133,22 @@ roleRef:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
name: #@ defaultResourceNameWithSuffix("pre-authn-apis")
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("login.concierge")
|
||||
resources: [ tokencredentialrequests ]
|
||||
verbs: [ create ]
|
||||
verbs: [ create, list ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("identity.concierge")
|
||||
resources: [ whoamirequests ]
|
||||
verbs: [ create, list ]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
name: #@ defaultResourceNameWithSuffix("pre-authn-apis")
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: Group
|
||||
@ -155,7 +159,7 @@ subjects:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
name: #@ defaultResourceNameWithSuffix("pre-authn-apis")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permissions for subjectaccessreviews, tokenreview that is needed by aggregated api servers
|
||||
|
@ -112,7 +112,7 @@ echo "generating API-related code for our public API groups..."
|
||||
deepcopy \
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \
|
||||
"supervisor/config:v1alpha1 supervisor/idp:v1alpha1 concierge/config:v1alpha1 concierge/authentication:v1alpha1 concierge/login:v1alpha1" \
|
||||
"supervisor/config:v1alpha1 supervisor/idp:v1alpha1 concierge/config:v1alpha1 concierge/authentication:v1alpha1 concierge/login:v1alpha1 concierge/identity:v1alpha1" \
|
||||
--go-header-file "${ROOT}/hack/boilerplate.go.txt" 2>&1 | sed "s|^|gen-api > |"
|
||||
)
|
||||
|
||||
@ -124,7 +124,7 @@ echo "generating API-related code for our internal API groups..."
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/client/concierge" \
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \
|
||||
"concierge/login:v1alpha1" \
|
||||
"concierge/login:v1alpha1 concierge/identity:v1alpha1" \
|
||||
--go-header-file "${ROOT}/hack/boilerplate.go.txt" 2>&1 | sed "s|^|gen-int-api > |"
|
||||
)
|
||||
|
||||
@ -140,7 +140,7 @@ echo "generating client code for our public API groups..."
|
||||
client,lister,informer \
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/client/concierge" \
|
||||
"${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \
|
||||
"concierge/config:v1alpha1 concierge/authentication:v1alpha1 concierge/login:v1alpha1" \
|
||||
"concierge/config:v1alpha1 concierge/authentication:v1alpha1 concierge/login:v1alpha1 concierge/identity:v1alpha1" \
|
||||
--go-header-file "${ROOT}/hack/boilerplate.go.txt" 2>&1 | sed "s|^|gen-client > |"
|
||||
)
|
||||
(cd client &&
|
||||
|
@ -10,12 +10,14 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/registry/credentialrequest"
|
||||
"go.pinniped.dev/internal/registry/whoamirequest"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@ -29,7 +31,8 @@ type ExtraConfig struct {
|
||||
StartControllersPostStartHook func(ctx context.Context)
|
||||
Scheme *runtime.Scheme
|
||||
NegotiatedSerializer runtime.NegotiatedSerializer
|
||||
GroupVersion schema.GroupVersion
|
||||
LoginConciergeGroupVersion schema.GroupVersion
|
||||
IdentityConciergeGroupVersion schema.GroupVersion
|
||||
}
|
||||
|
||||
type PinnipedServer struct {
|
||||
@ -70,17 +73,35 @@ func (c completedConfig) New() (*PinnipedServer, error) {
|
||||
GenericAPIServer: genericServer,
|
||||
}
|
||||
|
||||
gvr := c.ExtraConfig.GroupVersion.WithResource("tokencredentialrequests")
|
||||
storage := credentialrequest.NewREST(c.ExtraConfig.Authenticator, c.ExtraConfig.Issuer, gvr.GroupResource())
|
||||
if err := s.GenericAPIServer.InstallAPIGroup(&genericapiserver.APIGroupInfo{
|
||||
PrioritizedVersions: []schema.GroupVersion{gvr.GroupVersion()},
|
||||
VersionedResourcesStorageMap: map[string]map[string]rest.Storage{gvr.Version: {gvr.Resource: storage}},
|
||||
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
|
||||
Scheme: c.ExtraConfig.Scheme,
|
||||
ParameterCodec: metav1.ParameterCodec,
|
||||
NegotiatedSerializer: c.ExtraConfig.NegotiatedSerializer,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("could not install API group %s: %w", gvr.String(), err)
|
||||
var errs []error //nolint: prealloc
|
||||
for _, f := range []func() (schema.GroupVersionResource, rest.Storage){
|
||||
func() (schema.GroupVersionResource, rest.Storage) {
|
||||
tokenCredReqGVR := c.ExtraConfig.LoginConciergeGroupVersion.WithResource("tokencredentialrequests")
|
||||
tokenCredStorage := credentialrequest.NewREST(c.ExtraConfig.Authenticator, c.ExtraConfig.Issuer, tokenCredReqGVR.GroupResource())
|
||||
return tokenCredReqGVR, tokenCredStorage
|
||||
},
|
||||
func() (schema.GroupVersionResource, rest.Storage) {
|
||||
whoAmIReqGVR := c.ExtraConfig.IdentityConciergeGroupVersion.WithResource("whoamirequests")
|
||||
whoAmIStorage := whoamirequest.NewREST(whoAmIReqGVR.GroupResource())
|
||||
return whoAmIReqGVR, whoAmIStorage
|
||||
},
|
||||
} {
|
||||
gvr, storage := f()
|
||||
errs = append(errs,
|
||||
s.GenericAPIServer.InstallAPIGroup(
|
||||
&genericapiserver.APIGroupInfo{
|
||||
PrioritizedVersions: []schema.GroupVersion{gvr.GroupVersion()},
|
||||
VersionedResourcesStorageMap: map[string]map[string]rest.Storage{gvr.Version: {gvr.Resource: storage}},
|
||||
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
|
||||
Scheme: c.ExtraConfig.Scheme,
|
||||
ParameterCodec: metav1.ParameterCodec,
|
||||
NegotiatedSerializer: c.ExtraConfig.NegotiatedSerializer,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
if err := errors.NewAggregate(errs); err != nil {
|
||||
return nil, fmt.Errorf("could not install API groups: %w", err)
|
||||
}
|
||||
|
||||
s.GenericAPIServer.AddPostStartHookOrDie("start-controllers",
|
||||
|
@ -19,6 +19,8 @@ import (
|
||||
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"
|
||||
@ -174,7 +176,7 @@ func getAggregatedAPIServerConfig(
|
||||
startControllersPostStartHook func(context.Context),
|
||||
apiGroupSuffix string,
|
||||
) (*apiserver.Config, error) {
|
||||
scheme, groupVersion := getAggregatedAPIServerScheme(apiGroupSuffix)
|
||||
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
|
||||
@ -182,7 +184,7 @@ func getAggregatedAPIServerConfig(
|
||||
|
||||
recommendedOptions := genericoptions.NewRecommendedOptions(
|
||||
defaultEtcdPathPrefix,
|
||||
codecs.LegacyCodec(groupVersion),
|
||||
codecs.LegacyCodec(loginConciergeGroupVersion, identityConciergeGroupVersion),
|
||||
)
|
||||
recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet
|
||||
recommendedOptions.SecureServing.ServerCert.GeneratedCert = dynamicCertProvider
|
||||
@ -210,13 +212,14 @@ func getAggregatedAPIServerConfig(
|
||||
StartControllersPostStartHook: startControllersPostStartHook,
|
||||
Scheme: scheme,
|
||||
NegotiatedSerializer: codecs,
|
||||
GroupVersion: groupVersion,
|
||||
LoginConciergeGroupVersion: loginConciergeGroupVersion,
|
||||
IdentityConciergeGroupVersion: identityConciergeGroupVersion,
|
||||
},
|
||||
}
|
||||
return apiServerConfig, nil
|
||||
}
|
||||
|
||||
func getAggregatedAPIServerScheme(apiGroupSuffix string) (*runtime.Scheme, schema.GroupVersion) {
|
||||
func getAggregatedAPIServerScheme(apiGroupSuffix string) (_ *runtime.Scheme, login, identity schema.GroupVersion) {
|
||||
// standard set up of the server side scheme
|
||||
scheme := runtime.NewScheme()
|
||||
|
||||
@ -224,48 +227,30 @@ func getAggregatedAPIServerScheme(apiGroupSuffix string) (*runtime.Scheme, schem
|
||||
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
|
||||
|
||||
// nothing fancy is required if using the standard group suffix
|
||||
if apiGroupSuffix == "pinniped.dev" {
|
||||
utilruntime.Must(loginv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(loginapi.AddToScheme(scheme))
|
||||
return scheme, loginv1alpha1.SchemeGroupVersion
|
||||
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
|
||||
}
|
||||
|
||||
loginConciergeAPIGroup, ok := groupsuffix.Replace(loginv1alpha1.GroupName, apiGroupSuffix)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("cannot make api group from %s/%s", loginv1alpha1.GroupName, apiGroupSuffix)) // static input, impossible case
|
||||
}
|
||||
loginConciergeGroupData, identityConciergeGroupData := groupsuffix.ConciergeAggregatedGroups(apiGroupSuffix)
|
||||
|
||||
// we need a temporary place to register our types to avoid double registering them
|
||||
tmpScheme := runtime.NewScheme()
|
||||
utilruntime.Must(loginv1alpha1.AddToScheme(tmpScheme))
|
||||
utilruntime.Must(loginapi.AddToScheme(tmpScheme))
|
||||
addToSchemeAtNewGroup(scheme, loginv1alpha1.GroupName, loginConciergeGroupData.Group, loginv1alpha1.AddToScheme, loginapi.AddToScheme)
|
||||
addToSchemeAtNewGroup(scheme, identityv1alpha1.GroupName, identityConciergeGroupData.Group, identityv1alpha1.AddToScheme, identityapi.AddToScheme)
|
||||
|
||||
for gvk := range tmpScheme.AllKnownTypes() {
|
||||
if gvk.GroupVersion() == metav1.Unversioned {
|
||||
continue // metav1.AddToGroupVersion registers types outside of our aggregated API group that we need to ignore
|
||||
}
|
||||
|
||||
if gvk.Group != loginv1alpha1.GroupName {
|
||||
panic("tmp scheme has types not in the aggregated API group: " + gvk.Group) // programmer error
|
||||
}
|
||||
|
||||
obj, err := tmpScheme.New(gvk)
|
||||
if err != nil {
|
||||
panic(err) // programmer error, scheme internal code is broken
|
||||
}
|
||||
newGVK := schema.GroupVersionKind{
|
||||
Group: loginConciergeAPIGroup,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
|
||||
// register the existing type but with the new group in the correct scheme
|
||||
scheme.AddKnownTypeWithName(newGVK, obj)
|
||||
}
|
||||
|
||||
// manually register conversions and defaulting into the correct scheme since we cannot directly call loginv1alpha1.AddToScheme
|
||||
utilruntime.Must(loginv1alpha1.RegisterConversions(scheme))
|
||||
utilruntime.Must(loginv1alpha1.RegisterDefaults(scheme))
|
||||
// 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
|
||||
@ -306,5 +291,35 @@ func getAggregatedAPIServerScheme(apiGroupSuffix string) (*runtime.Scheme, schem
|
||||
credentialRequest.Spec.Authenticator.APIGroup = &restoredGroup
|
||||
})
|
||||
|
||||
return scheme, schema.GroupVersion{Group: loginConciergeAPIGroup, Version: loginv1alpha1.SchemeGroupVersion.Version}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ import (
|
||||
"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"
|
||||
)
|
||||
@ -99,24 +101,40 @@ func TestCommand(t *testing.T) {
|
||||
|
||||
func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
||||
// the standard group
|
||||
regularGV := schema.GroupVersion{
|
||||
regularLoginGV := schema.GroupVersion{
|
||||
Group: "login.concierge.pinniped.dev",
|
||||
Version: "v1alpha1",
|
||||
}
|
||||
regularGVInternal := schema.GroupVersion{
|
||||
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
|
||||
otherGV := schema.GroupVersion{
|
||||
otherLoginGV := schema.GroupVersion{
|
||||
Group: "login.concierge.walrus.tld",
|
||||
Version: "v1alpha1",
|
||||
}
|
||||
otherGVInternal := schema.GroupVersion{
|
||||
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{
|
||||
@ -125,10 +143,11 @@ func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
apiGroupSuffix string
|
||||
want map[schema.GroupVersionKind]reflect.Type
|
||||
wantGroupVersion schema.GroupVersion
|
||||
name string
|
||||
apiGroupSuffix string
|
||||
want map[schema.GroupVersionKind]reflect.Type
|
||||
wantLoginGroupVersion schema.GroupVersion
|
||||
wantIdentityGroupVersion schema.GroupVersion
|
||||
}{
|
||||
{
|
||||
name: "regular api group",
|
||||
@ -136,22 +155,39 @@ func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
||||
want: map[schema.GroupVersionKind]reflect.Type{
|
||||
// all the types that are in the aggregated API group
|
||||
|
||||
regularGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
||||
regularGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
||||
regularLoginGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
||||
regularLoginGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
||||
|
||||
regularGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
||||
regularGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
||||
regularLoginGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
||||
regularLoginGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
||||
|
||||
regularGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
||||
regularGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
||||
regularGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
||||
regularGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
||||
regularGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
||||
regularGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
||||
regularGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||
regularGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||
regularIdentityGV.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequest{}).Elem(),
|
||||
regularIdentityGV.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequestList{}).Elem(),
|
||||
|
||||
regularGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).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
|
||||
|
||||
@ -171,7 +207,8 @@ func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
||||
metav1.Unversioned.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||
metav1.Unversioned.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||
},
|
||||
wantGroupVersion: regularGV,
|
||||
wantLoginGroupVersion: regularLoginGV,
|
||||
wantIdentityGroupVersion: regularIdentityGV,
|
||||
},
|
||||
{
|
||||
name: "other api group",
|
||||
@ -179,22 +216,39 @@ func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
||||
want: map[schema.GroupVersionKind]reflect.Type{
|
||||
// all the types that are in the aggregated API group
|
||||
|
||||
otherGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
||||
otherGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
||||
otherLoginGV.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequest{}).Elem(),
|
||||
otherLoginGV.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginv1alpha1.TokenCredentialRequestList{}).Elem(),
|
||||
|
||||
otherGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
||||
otherGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
||||
otherLoginGVInternal.WithKind("TokenCredentialRequest"): reflect.TypeOf(&loginapi.TokenCredentialRequest{}).Elem(),
|
||||
otherLoginGVInternal.WithKind("TokenCredentialRequestList"): reflect.TypeOf(&loginapi.TokenCredentialRequestList{}).Elem(),
|
||||
|
||||
otherGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(),
|
||||
otherGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(),
|
||||
otherGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(),
|
||||
otherGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(),
|
||||
otherGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(),
|
||||
otherGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(),
|
||||
otherGV.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||
otherGV.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||
otherIdentityGV.WithKind("WhoAmIRequest"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequest{}).Elem(),
|
||||
otherIdentityGV.WithKind("WhoAmIRequestList"): reflect.TypeOf(&identityv1alpha1.WhoAmIRequestList{}).Elem(),
|
||||
|
||||
otherGVInternal.WithKind("WatchEvent"): reflect.TypeOf(&metav1.InternalEvent{}).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
|
||||
|
||||
@ -214,15 +268,17 @@ func Test_getAggregatedAPIServerScheme(t *testing.T) {
|
||||
metav1.Unversioned.WithKind("UpdateOptions"): reflect.TypeOf(&metav1.UpdateOptions{}).Elem(),
|
||||
metav1.Unversioned.WithKind("WatchEvent"): reflect.TypeOf(&metav1.WatchEvent{}).Elem(),
|
||||
},
|
||||
wantGroupVersion: otherGV,
|
||||
wantLoginGroupVersion: otherLoginGV,
|
||||
wantIdentityGroupVersion: otherIdentityGV,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scheme, gv := getAggregatedAPIServerScheme(tt.apiGroupSuffix)
|
||||
scheme, loginGV, identityGV := getAggregatedAPIServerScheme(tt.apiGroupSuffix)
|
||||
require.Equal(t, tt.want, scheme.AllKnownTypes())
|
||||
require.Equal(t, tt.wantGroupVersion, gv)
|
||||
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
|
||||
|
@ -79,7 +79,7 @@ func maybeSetAPIDefaults(apiConfig *APIConfigSpec) {
|
||||
|
||||
func maybeSetAPIGroupSuffixDefault(apiGroupSuffix **string) {
|
||||
if *apiGroupSuffix == nil {
|
||||
*apiGroupSuffix = stringPtr("pinniped.dev")
|
||||
*apiGroupSuffix = stringPtr(groupsuffix.PinnipedDefaultSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ func FromPath(path string) (*Config, error) {
|
||||
|
||||
func maybeSetAPIGroupSuffixDefault(apiGroupSuffix **string) {
|
||||
if *apiGroupSuffix == nil {
|
||||
*apiGroupSuffix = stringPtr("pinniped.dev")
|
||||
*apiGroupSuffix = stringPtr(groupsuffix.PinnipedDefaultSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||
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"
|
||||
@ -85,18 +84,14 @@ type Config struct {
|
||||
// Prepare the controllers and their informers and return a function that will start them when called.
|
||||
//nolint:funlen // Eh, fair, it is a really long function...but it is wiring the world...so...
|
||||
func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
||||
groupName, ok := groupsuffix.Replace(loginv1alpha1.GroupName, c.APIGroupSuffix)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot make api group from %s/%s", loginv1alpha1.GroupName, c.APIGroupSuffix)
|
||||
}
|
||||
apiServiceName := loginv1alpha1.SchemeGroupVersion.Version + "." + groupName
|
||||
loginConciergeGroupData, identityConciergeGroupData := groupsuffix.ConciergeAggregatedGroups(c.APIGroupSuffix)
|
||||
|
||||
dref, _, err := deploymentref.New(c.ServerInstallationInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create deployment ref: %w", err)
|
||||
}
|
||||
|
||||
apiServiceRef, err := apiserviceref.New(apiServiceName)
|
||||
apiServiceRef, err := apiserviceref.New(loginConciergeGroupData.APIServiceName())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create API service ref: %w", err)
|
||||
}
|
||||
@ -163,7 +158,18 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
||||
apicerts.NewAPIServiceUpdaterController(
|
||||
c.ServerInstallationInfo.Namespace,
|
||||
c.NamesConfig.ServingCertificateSecret,
|
||||
apiServiceName,
|
||||
loginConciergeGroupData.APIServiceName(),
|
||||
client.Aggregation,
|
||||
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
apicerts.NewAPIServiceUpdaterController(
|
||||
c.ServerInstallationInfo.Namespace,
|
||||
c.NamesConfig.ServingCertificateSecret,
|
||||
identityConciergeGroupData.APIServiceName(),
|
||||
client.Aggregation,
|
||||
informers.installationNamespaceK8s.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
|
34
internal/groupsuffix/groupdata.go
Normal file
34
internal/groupsuffix/groupdata.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package groupsuffix
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||
)
|
||||
|
||||
type GroupData schema.GroupVersion
|
||||
|
||||
func (d GroupData) APIServiceName() string {
|
||||
return d.Version + "." + d.Group
|
||||
}
|
||||
|
||||
func ConciergeAggregatedGroups(apiGroupSuffix string) (login, identity GroupData) {
|
||||
loginConciergeAPIGroup, ok1 := Replace(loginv1alpha1.GroupName, apiGroupSuffix)
|
||||
identityConciergeAPIGroup, ok2 := Replace(identityv1alpha1.GroupName, apiGroupSuffix)
|
||||
|
||||
if valid := ok1 && ok2; !valid {
|
||||
panic("static group input is invalid")
|
||||
}
|
||||
|
||||
return GroupData{
|
||||
Group: loginConciergeAPIGroup,
|
||||
Version: loginv1alpha1.SchemeGroupVersion.Version,
|
||||
}, GroupData{
|
||||
Group: identityConciergeAPIGroup,
|
||||
Version: identityv1alpha1.SchemeGroupVersion.Version,
|
||||
}
|
||||
}
|
@ -20,13 +20,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
pinnipedDefaultSuffix = "pinniped.dev"
|
||||
PinnipedDefaultSuffix = "pinniped.dev"
|
||||
pinnipedDefaultSuffixWithDot = ".pinniped.dev"
|
||||
)
|
||||
|
||||
func New(apiGroupSuffix string) kubeclient.Middleware {
|
||||
// return a no-op middleware by default
|
||||
if len(apiGroupSuffix) == 0 || apiGroupSuffix == pinnipedDefaultSuffix {
|
||||
if len(apiGroupSuffix) == 0 || apiGroupSuffix == PinnipedDefaultSuffix {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ func Replace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
|
||||
if !strings.HasSuffix(baseAPIGroup, pinnipedDefaultSuffixWithDot) {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSuffix(baseAPIGroup, pinnipedDefaultSuffix) + apiGroupSuffix, true
|
||||
return strings.TrimSuffix(baseAPIGroup, PinnipedDefaultSuffix) + apiGroupSuffix, true
|
||||
}
|
||||
|
||||
// Unreplace is like performing an undo of Replace().
|
||||
@ -169,7 +169,7 @@ func Unreplace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
|
||||
if !strings.HasSuffix(baseAPIGroup, "."+apiGroupSuffix) {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSuffix(baseAPIGroup, apiGroupSuffix) + pinnipedDefaultSuffix, true
|
||||
return strings.TrimSuffix(baseAPIGroup, apiGroupSuffix) + PinnipedDefaultSuffix, true
|
||||
}
|
||||
|
||||
// Validate validates the provided apiGroupSuffix is usable as an API group suffix. Specifically, it
|
||||
|
@ -21,8 +21,8 @@ import (
|
||||
"k8s.io/client-go/transport"
|
||||
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||
|
||||
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
"go.pinniped.dev/internal/testutil/fakekubeapi"
|
||||
)
|
||||
|
||||
@ -46,16 +46,15 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
tokenCredentialRequestGVK = loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest")
|
||||
goodTokenCredentialRequest = &loginv1alpha1.TokenCredentialRequest{
|
||||
credentialIssuerGVK = conciergeconfigv1alpha1.SchemeGroupVersion.WithKind("CredentialIssuer")
|
||||
goodCredentialIssuer = &conciergeconfigv1alpha1.CredentialIssuer{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "good-token-credential-request",
|
||||
Namespace: "good-namespace",
|
||||
Name: "good-credential-issuer",
|
||||
},
|
||||
}
|
||||
|
||||
federationDomainGVK = configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain")
|
||||
goodFederationDomain = &configv1alpha1.FederationDomain{
|
||||
federationDomainGVK = supervisorconfigv1alpha1.SchemeGroupVersion.WithKind("FederationDomain")
|
||||
goodFederationDomain = &supervisorconfigv1alpha1.FederationDomain{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "good-federation-domain",
|
||||
Namespace: "good-namespace",
|
||||
@ -258,60 +257,60 @@ func TestKubeclient(t *testing.T) {
|
||||
reallyRunTest: func(t *testing.T, c *Client) {
|
||||
// create
|
||||
tokenCredentialRequest, err := c.PinnipedConcierge.
|
||||
LoginV1alpha1().
|
||||
TokenCredentialRequests().
|
||||
Create(context.Background(), goodTokenCredentialRequest, metav1.CreateOptions{})
|
||||
ConfigV1alpha1().
|
||||
CredentialIssuers().
|
||||
Create(context.Background(), goodCredentialIssuer, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, goodTokenCredentialRequest, tokenCredentialRequest)
|
||||
require.Equal(t, goodCredentialIssuer, tokenCredentialRequest)
|
||||
|
||||
// read
|
||||
tokenCredentialRequest, err = c.PinnipedConcierge.
|
||||
LoginV1alpha1().
|
||||
TokenCredentialRequests().
|
||||
ConfigV1alpha1().
|
||||
CredentialIssuers().
|
||||
Get(context.Background(), tokenCredentialRequest.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, with(goodTokenCredentialRequest, annotations(), labels()), tokenCredentialRequest)
|
||||
require.Equal(t, with(goodCredentialIssuer, annotations(), labels()), tokenCredentialRequest)
|
||||
|
||||
// update
|
||||
goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName := with(goodTokenCredentialRequest, annotations(), labels(), clusterName()).(*loginv1alpha1.TokenCredentialRequest)
|
||||
goodCredentialIssuerWithAnnotationsAndLabelsAndClusterName := with(goodCredentialIssuer, annotations(), labels(), clusterName()).(*conciergeconfigv1alpha1.CredentialIssuer)
|
||||
tokenCredentialRequest, err = c.PinnipedConcierge.
|
||||
LoginV1alpha1().
|
||||
TokenCredentialRequests().
|
||||
Update(context.Background(), goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
|
||||
ConfigV1alpha1().
|
||||
CredentialIssuers().
|
||||
Update(context.Background(), goodCredentialIssuerWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName, tokenCredentialRequest)
|
||||
require.Equal(t, goodCredentialIssuerWithAnnotationsAndLabelsAndClusterName, tokenCredentialRequest)
|
||||
|
||||
// delete
|
||||
err = c.PinnipedConcierge.
|
||||
LoginV1alpha1().
|
||||
TokenCredentialRequests().
|
||||
ConfigV1alpha1().
|
||||
CredentialIssuers().
|
||||
Delete(context.Background(), tokenCredentialRequest.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
wantMiddlewareReqs: [][]Object{
|
||||
{
|
||||
with(goodTokenCredentialRequest, gvk(tokenCredentialRequestGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
||||
with(goodCredentialIssuer, gvk(credentialIssuerGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)),
|
||||
},
|
||||
{
|
||||
with(goodTokenCredentialRequest, annotations(), gvk(tokenCredentialRequestGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
||||
with(goodCredentialIssuer, annotations(), gvk(credentialIssuerGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)),
|
||||
with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)),
|
||||
},
|
||||
},
|
||||
wantMiddlewareResps: [][]Object{
|
||||
{
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), gvk(credentialIssuerGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), gvk(credentialIssuerGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)),
|
||||
},
|
||||
{
|
||||
with(goodTokenCredentialRequest, emptyAnnotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
||||
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
||||
with(goodCredentialIssuer, emptyAnnotations(), labels(), gvk(credentialIssuerGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), gvk(credentialIssuerGVK)),
|
||||
with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -338,7 +337,7 @@ func TestKubeclient(t *testing.T) {
|
||||
require.Equal(t, with(goodFederationDomain, annotations(), labels()), federationDomain)
|
||||
|
||||
// update
|
||||
goodFederationDomainWithAnnotationsAndLabelsAndClusterName := with(goodFederationDomain, annotations(), labels(), clusterName()).(*configv1alpha1.FederationDomain)
|
||||
goodFederationDomainWithAnnotationsAndLabelsAndClusterName := with(goodFederationDomain, annotations(), labels(), clusterName()).(*supervisorconfigv1alpha1.FederationDomain)
|
||||
federationDomain, err = c.PinnipedSupervisor.
|
||||
ConfigV1alpha1().
|
||||
FederationDomains(federationDomain.Namespace).
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/utils/trace"
|
||||
|
||||
@ -157,6 +158,11 @@ func validateRequest(ctx context.Context, obj runtime.Object, createValidation r
|
||||
}
|
||||
}
|
||||
|
||||
if namespace := genericapirequest.NamespaceValue(ctx); len(namespace) != 0 {
|
||||
traceValidationFailure(t, "namespace is not allowed")
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("namespace is not allowed on TokenCredentialRequest: %v", namespace))
|
||||
}
|
||||
|
||||
// let dynamic admission webhooks have a chance to validate (but not mutate) as well
|
||||
// TODO Since we are an aggregated API, we should investigate to see if the kube API server is already invoking admission hooks for us.
|
||||
// Even if it is, its okay to call it again here. However, if the kube API server is already calling the webhooks and passing
|
||||
|
@ -284,6 +284,17 @@ func TestCreate(t *testing.T) {
|
||||
`.pinniped.dev "request name" is invalid: dryRun: Unsupported value: []string{"some dry run flag"}`)
|
||||
requireOneLogStatement(r, logger, `"failure" failureType:request validation,msg:dryRun not supported`)
|
||||
})
|
||||
|
||||
it("CreateFailsWhenNamespaceIsNotEmpty", func() {
|
||||
response, err := NewREST(nil, nil, schema.GroupResource{}).Create(
|
||||
genericapirequest.WithNamespace(genericapirequest.NewContext(), "some-ns"),
|
||||
validCredentialRequest(),
|
||||
rest.ValidateAllObjectFunc,
|
||||
&metav1.CreateOptions{})
|
||||
|
||||
requireAPIError(t, response, err, apierrors.IsBadRequest, `namespace is not allowed on TokenCredentialRequest: some-ns`)
|
||||
requireOneLogStatement(r, logger, `"failure" failureType:request validation,msg:namespace is not allowed`)
|
||||
})
|
||||
}, spec.Sequential())
|
||||
}
|
||||
|
||||
|
131
internal/registry/whoamirequest/rest.go
Normal file
131
internal/registry/whoamirequest/rest.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package whoamirequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
||||
identityapivalidation "go.pinniped.dev/generated/latest/apis/concierge/identity/validation"
|
||||
)
|
||||
|
||||
func NewREST(resource schema.GroupResource) *REST {
|
||||
return &REST{
|
||||
tableConvertor: rest.NewDefaultTableConvertor(resource),
|
||||
}
|
||||
}
|
||||
|
||||
type REST struct {
|
||||
tableConvertor rest.TableConvertor
|
||||
}
|
||||
|
||||
// Assert that our *REST implements all the optional interfaces that we expect it to implement.
|
||||
var _ interface {
|
||||
rest.Creater
|
||||
rest.NamespaceScopedStrategy
|
||||
rest.Scoper
|
||||
rest.Storage
|
||||
rest.CategoriesProvider
|
||||
rest.Lister
|
||||
} = (*REST)(nil)
|
||||
|
||||
func (*REST) New() runtime.Object {
|
||||
return &identityapi.WhoAmIRequest{}
|
||||
}
|
||||
|
||||
func (*REST) NewList() runtime.Object {
|
||||
return &identityapi.WhoAmIRequestList{}
|
||||
}
|
||||
|
||||
func (*REST) List(_ context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
return &identityapi.WhoAmIRequestList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: "0", // this resource version means "from the API server cache"
|
||||
},
|
||||
Items: []identityapi.WhoAmIRequest{}, // avoid sending nil items list
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *REST) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return r.tableConvertor.ConvertToTable(ctx, obj, tableOptions)
|
||||
}
|
||||
|
||||
func (*REST) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (*REST) Categories() []string {
|
||||
return []string{"pinniped"}
|
||||
}
|
||||
|
||||
func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
whoAmIRequest, ok := obj.(*identityapi.WhoAmIRequest)
|
||||
if !ok {
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a WhoAmIRequest: %#v", obj))
|
||||
}
|
||||
|
||||
if errs := identityapivalidation.ValidateWhoAmIRequest(whoAmIRequest); len(errs) > 0 {
|
||||
return nil, apierrors.NewInvalid(identityapi.Kind(whoAmIRequest.Kind), whoAmIRequest.Name, errs)
|
||||
}
|
||||
|
||||
// just a sanity check, not sure how to honor a dry run on a virtual API
|
||||
if options != nil {
|
||||
if len(options.DryRun) != 0 {
|
||||
errs := field.ErrorList{field.NotSupported(field.NewPath("dryRun"), options.DryRun, nil)}
|
||||
return nil, apierrors.NewInvalid(identityapi.Kind(whoAmIRequest.Kind), whoAmIRequest.Name, errs)
|
||||
}
|
||||
}
|
||||
|
||||
if namespace := genericapirequest.NamespaceValue(ctx); len(namespace) != 0 {
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("namespace is not allowed on WhoAmIRequest: %v", namespace))
|
||||
}
|
||||
|
||||
if createValidation != nil {
|
||||
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
userInfo, ok := genericapirequest.UserFrom(ctx)
|
||||
if !ok {
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("no user info on request"))
|
||||
}
|
||||
|
||||
auds, _ := authenticator.AudiencesFrom(ctx)
|
||||
|
||||
out := &identityapi.WhoAmIRequest{
|
||||
Status: identityapi.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityapi.KubernetesUserInfo{
|
||||
User: identityapi.UserInfo{
|
||||
Username: userInfo.GetName(),
|
||||
UID: userInfo.GetUID(),
|
||||
Groups: userInfo.GetGroups(),
|
||||
},
|
||||
Audiences: auds,
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, v := range userInfo.GetExtra() {
|
||||
if out.Status.KubernetesUserInfo.User.Extra == nil {
|
||||
out.Status.KubernetesUserInfo.User.Extra = map[string]identityapi.ExtraValue{}
|
||||
}
|
||||
|
||||
// this assumes no one is putting secret data in the extra field
|
||||
// I think this is a safe assumption since it would leak into audit logs
|
||||
out.Status.KubernetesUserInfo.User.Extra[k] = v
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
211
internal/registry/whoamirequest/rest_test.go
Normal file
211
internal/registry/whoamirequest/rest_test.go
Normal file
@ -0,0 +1,211 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package whoamirequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
r := NewREST(schema.GroupResource{Group: "bears", Resource: "panda"})
|
||||
require.NotNil(t, r)
|
||||
require.False(t, r.NamespaceScoped())
|
||||
require.Equal(t, []string{"pinniped"}, r.Categories())
|
||||
require.IsType(t, &identityapi.WhoAmIRequest{}, r.New())
|
||||
require.IsType(t, &identityapi.WhoAmIRequestList{}, r.NewList())
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// check the simple invariants of our no-op list
|
||||
list, err := r.List(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, list)
|
||||
require.IsType(t, &identityapi.WhoAmIRequestList{}, list)
|
||||
require.Equal(t, "0", list.(*identityapi.WhoAmIRequestList).ResourceVersion)
|
||||
require.NotNil(t, list.(*identityapi.WhoAmIRequestList).Items)
|
||||
require.Len(t, list.(*identityapi.WhoAmIRequestList).Items, 0)
|
||||
|
||||
// make sure we can turn lists into tables if needed
|
||||
table, err := r.ConvertToTable(ctx, list, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, table)
|
||||
require.Equal(t, "0", table.ResourceVersion)
|
||||
require.Nil(t, table.Rows)
|
||||
|
||||
// exercise group resource - force error by passing a runtime.Object that does not have an embedded object meta
|
||||
_, err = r.ConvertToTable(ctx, &metav1.APIGroup{}, nil)
|
||||
require.Error(t, err, "the resource panda.bears does not support being converted to a Table")
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
obj runtime.Object
|
||||
createValidation rest.ValidateObjectFunc
|
||||
options *metav1.CreateOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want runtime.Object
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "wrong type",
|
||||
args: args{
|
||||
ctx: genericapirequest.NewContext(),
|
||||
obj: &metav1.Status{},
|
||||
createValidation: nil,
|
||||
options: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: `not a WhoAmIRequest: &v1.Status{TypeMeta:v1.TypeMeta{Kind:"", APIVersion:""}, ListMeta:v1.ListMeta{SelfLink:"", ResourceVersion:"", Continue:"", RemainingItemCount:(*int64)(nil)}, Status:"", Message:"", Reason:"", Details:(*v1.StatusDetails)(nil), Code:0}`,
|
||||
},
|
||||
{
|
||||
name: "bad options",
|
||||
args: args{
|
||||
ctx: genericapirequest.NewContext(),
|
||||
obj: &identityapi.WhoAmIRequest{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "SomeKind",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "some-name",
|
||||
},
|
||||
},
|
||||
createValidation: nil,
|
||||
options: &metav1.CreateOptions{DryRun: []string{"stuff"}},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: `SomeKind.identity.concierge.pinniped.dev "some-name" is invalid: dryRun: Unsupported value: []string{"stuff"}`,
|
||||
},
|
||||
{
|
||||
name: "bad namespace",
|
||||
args: args{
|
||||
ctx: genericapirequest.WithNamespace(genericapirequest.NewContext(), "some-ns"),
|
||||
obj: &identityapi.WhoAmIRequest{},
|
||||
createValidation: nil,
|
||||
options: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: `namespace is not allowed on WhoAmIRequest: some-ns`,
|
||||
},
|
||||
{
|
||||
// if we add fields to spec, we need additional tests to:
|
||||
// - make sure admission cannot mutate it
|
||||
// - the input spec fields are validated correctly
|
||||
name: "create validation failure",
|
||||
args: args{
|
||||
ctx: genericapirequest.NewContext(),
|
||||
obj: &identityapi.WhoAmIRequest{},
|
||||
createValidation: func(ctx context.Context, obj runtime.Object) error {
|
||||
return errors.New("some-error-here")
|
||||
},
|
||||
options: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: `some-error-here`,
|
||||
},
|
||||
{
|
||||
name: "no user info",
|
||||
args: args{
|
||||
ctx: genericapirequest.NewContext(),
|
||||
obj: &identityapi.WhoAmIRequest{},
|
||||
createValidation: nil,
|
||||
options: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: `Internal error occurred: no user info on request`,
|
||||
},
|
||||
{
|
||||
name: "with user info, no auds",
|
||||
args: args{
|
||||
ctx: genericapirequest.WithUser(genericapirequest.NewContext(), &user.DefaultInfo{
|
||||
Name: "bond",
|
||||
UID: "007",
|
||||
Groups: []string{"agents", "ops"},
|
||||
Extra: map[string][]string{
|
||||
"fan-of": {"pandas", "twizzlers"},
|
||||
"needs": {"sleep"},
|
||||
},
|
||||
}),
|
||||
obj: &identityapi.WhoAmIRequest{},
|
||||
createValidation: nil,
|
||||
options: nil,
|
||||
},
|
||||
want: &identityapi.WhoAmIRequest{
|
||||
Status: identityapi.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityapi.KubernetesUserInfo{
|
||||
User: identityapi.UserInfo{
|
||||
Username: "bond",
|
||||
UID: "007",
|
||||
Groups: []string{"agents", "ops"},
|
||||
Extra: map[string]identityapi.ExtraValue{
|
||||
"fan-of": {"pandas", "twizzlers"},
|
||||
"needs": {"sleep"},
|
||||
},
|
||||
},
|
||||
Audiences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ``,
|
||||
},
|
||||
{
|
||||
name: "with user info and auds",
|
||||
args: args{
|
||||
ctx: authenticator.WithAudiences(
|
||||
genericapirequest.WithUser(genericapirequest.NewContext(), &user.DefaultInfo{
|
||||
Name: "panda",
|
||||
}),
|
||||
authenticator.Audiences{"gitlab", "aws"},
|
||||
),
|
||||
obj: &identityapi.WhoAmIRequest{},
|
||||
createValidation: nil,
|
||||
options: nil,
|
||||
},
|
||||
want: &identityapi.WhoAmIRequest{
|
||||
Status: identityapi.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityapi.KubernetesUserInfo{
|
||||
User: identityapi.UserInfo{
|
||||
Username: "panda",
|
||||
},
|
||||
Audiences: []string{"gitlab", "aws"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ``,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &REST{}
|
||||
got, err := r.Create(tt.args.ctx, tt.args.obj, tt.args.createValidation, tt.args.options)
|
||||
require.Equal(t, tt.wantErr, errString(err))
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return err.Error()
|
||||
}
|
@ -118,7 +118,7 @@ func WithAPIGroupSuffix(apiGroupSuffix string) Option {
|
||||
|
||||
// New validates the specified options and returns a newly initialized *Client.
|
||||
func New(opts ...Option) (*Client, error) {
|
||||
c := Client{apiGroupSuffix: "pinniped.dev"}
|
||||
c := Client{apiGroupSuffix: groupsuffix.PinnipedDefaultSuffix}
|
||||
for _, opt := range opts {
|
||||
if err := opt(&c); err != nil {
|
||||
return nil, err
|
||||
|
@ -45,6 +45,8 @@ func TestGetPinnipedCategory(t *testing.T) {
|
||||
|
||||
require.Contains(t, stdErr.String(), `"kind":"Table"`)
|
||||
require.Contains(t, stdErr.String(), `"resourceVersion":"0"`)
|
||||
require.Contains(t, stdErr.String(), `/v1alpha1/tokencredentialrequests`)
|
||||
require.Contains(t, stdErr.String(), `/v1alpha1/whoamirequests`)
|
||||
})
|
||||
|
||||
t.Run("list, no special params", func(t *testing.T) {
|
||||
@ -78,7 +80,7 @@ func TestGetPinnipedCategory(t *testing.T) {
|
||||
require.Contains(t, stdErr.String(), `"resourceVersion":"0"`)
|
||||
})
|
||||
|
||||
t.Run("raw request to see body", func(t *testing.T) {
|
||||
t.Run("raw request to see body, token cred", func(t *testing.T) {
|
||||
var stdOut, stdErr bytes.Buffer
|
||||
|
||||
//nolint: gosec // input is part of test env
|
||||
@ -93,4 +95,20 @@ func TestGetPinnipedCategory(t *testing.T) {
|
||||
require.Contains(t, stdOut.String(), `{"kind":"TokenCredentialRequestList","apiVersion":"login.concierge`+
|
||||
dotSuffix+`/v1alpha1","metadata":{"resourceVersion":"0"},"items":[]}`)
|
||||
})
|
||||
|
||||
t.Run("raw request to see body, whoami", func(t *testing.T) {
|
||||
var stdOut, stdErr bytes.Buffer
|
||||
|
||||
//nolint: gosec // input is part of test env
|
||||
cmd := exec.Command("kubectl", "get", "--raw", "/apis/identity.concierge"+dotSuffix+"/v1alpha1/whoamirequests")
|
||||
cmd.Stdout = &stdOut
|
||||
cmd.Stderr = &stdErr
|
||||
err := cmd.Run()
|
||||
require.NoError(t, err, stdErr.String(), stdOut.String())
|
||||
require.Empty(t, stdErr.String())
|
||||
|
||||
require.NotContains(t, stdOut.String(), "MethodNotAllowed")
|
||||
require.Contains(t, stdOut.String(), `{"kind":"WhoAmIRequestList","apiVersion":"identity.concierge`+
|
||||
dotSuffix+`/v1alpha1","metadata":{"resourceVersion":"0"},"items":[]}`)
|
||||
})
|
||||
}
|
||||
|
@ -303,4 +303,37 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
expectedGroups = append(expectedGroups, g)
|
||||
}
|
||||
require.Equal(t, expectedGroups, idTokenClaims[oidc.DownstreamGroupsClaim])
|
||||
|
||||
// confirm we are the right user according to Kube
|
||||
expectedYAMLGroups := func() string {
|
||||
var b strings.Builder
|
||||
for _, g := range env.SupervisorTestUpstream.ExpectedGroups {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(` - `)
|
||||
b.WriteString(g)
|
||||
}
|
||||
return b.String()
|
||||
}()
|
||||
kubectlCmd3 := exec.CommandContext(ctx, "kubectl", "create", "-f", "-", "-o", "yaml", "--kubeconfig", kubeconfigPath)
|
||||
kubectlCmd3.Env = append(os.Environ(), env.ProxyEnv()...)
|
||||
kubectlCmd3.Stdin = strings.NewReader(`
|
||||
apiVersion: identity.concierge.` + env.APIGroupSuffix + `/v1alpha1
|
||||
kind: WhoAmIRequest
|
||||
`)
|
||||
kubectlOutput3, err := kubectlCmd3.CombinedOutput()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
`apiVersion: identity.concierge.`+env.APIGroupSuffix+`/v1alpha1
|
||||
kind: WhoAmIRequest
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
spec: {}
|
||||
status:
|
||||
kubernetesUserInfo:
|
||||
user:
|
||||
groups:`+expectedYAMLGroups+`
|
||||
- system:authenticated
|
||||
username: `+env.SupervisorTestUpstream.Username+`
|
||||
`,
|
||||
string(kubectlOutput3))
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/discovery"
|
||||
|
||||
"go.pinniped.dev/test/library"
|
||||
@ -44,6 +45,7 @@ func TestGetAPIResourceList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
loginConciergeGV := makeGV("login", "concierge")
|
||||
identityConciergeGV := makeGV("identity", "concierge")
|
||||
authenticationConciergeGV := makeGV("authentication", "concierge")
|
||||
configConciergeGV := makeGV("config", "concierge")
|
||||
idpSupervisorGV := makeGV("idp", "supervisor")
|
||||
@ -79,6 +81,32 @@ func TestGetAPIResourceList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
group: metav1.APIGroup{
|
||||
Name: identityConciergeGV.Group,
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{
|
||||
GroupVersion: identityConciergeGV.String(),
|
||||
Version: identityConciergeGV.Version,
|
||||
},
|
||||
},
|
||||
PreferredVersion: metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: identityConciergeGV.String(),
|
||||
Version: identityConciergeGV.Version,
|
||||
},
|
||||
},
|
||||
resourceByVersion: map[string][]metav1.APIResource{
|
||||
identityConciergeGV.String(): {
|
||||
{
|
||||
Name: "whoamirequests",
|
||||
Kind: "WhoAmIRequest",
|
||||
Verbs: []string{"create", "list"},
|
||||
Namespaced: false,
|
||||
Categories: []string{"pinniped"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
group: metav1.APIGroup{
|
||||
Name: configSupervisorGV.Group,
|
||||
@ -280,6 +308,8 @@ func TestGetAPIResourceList(t *testing.T) {
|
||||
t.Run("every API has a status subresource", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
aggregatedAPIs := sets.NewString("tokencredentialrequests", "whoamirequests")
|
||||
|
||||
var regular, status []string
|
||||
|
||||
for _, r := range resources {
|
||||
@ -288,8 +318,8 @@ func TestGetAPIResourceList(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, a := range r.APIResources {
|
||||
if a.Name == "tokencredentialrequests" {
|
||||
continue // our special aggregated API with its own magical properties
|
||||
if aggregatedAPIs.Has(a.Name) {
|
||||
continue // skip our special aggregated APIs with their own magical properties
|
||||
}
|
||||
|
||||
if strings.HasSuffix(a.Name, "/status") {
|
||||
|
448
test/integration/whoami_test.go
Normal file
448
test/integration/whoami_test.go
Normal file
@ -0,0 +1,448 @@
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/util/cert"
|
||||
"k8s.io/client-go/util/certificate/csr"
|
||||
"k8s.io/client-go/util/keyutil"
|
||||
|
||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||
"go.pinniped.dev/test/library"
|
||||
)
|
||||
|
||||
func TestWhoAmI_Kubeadm(t *testing.T) {
|
||||
// use the cluster signing key being available as a proxy for this being a kubeadm cluster
|
||||
// we should add more robust logic around skipping clusters based on vendor
|
||||
_ = library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
whoAmI, err := library.NewConciergeClientset(t).IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// this user info is based off of the bootstrap cert user created by kubeadm
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "kubernetes-admin",
|
||||
Groups: []string{
|
||||
"system:masters",
|
||||
"system:authenticated",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmI,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhoAmI_ServiceAccount_Legacy(t *testing.T) {
|
||||
_ = library.IntegrationEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
kubeClient := library.NewKubernetesClientset(t).CoreV1()
|
||||
|
||||
ns, err := kubeClient.Namespaces().Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "test-whoami-",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
err := kubeClient.Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
sa, err := kubeClient.ServiceAccounts(ns.Name).Create(ctx, &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "test-whoami-",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
secret, err := kubeClient.Secrets(ns.Name).Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "test-whoami-",
|
||||
Annotations: map[string]string{
|
||||
corev1.ServiceAccountNameKey: sa.Name,
|
||||
},
|
||||
},
|
||||
Type: corev1.SecretTypeServiceAccountToken,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
||||
secret, err = kubeClient.Secrets(ns.Name).Get(ctx, secret.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(secret.Data[corev1.ServiceAccountTokenKey]) > 0, nil
|
||||
}, 30*time.Second, time.Second)
|
||||
|
||||
saConfig := library.NewAnonymousClientRestConfig(t)
|
||||
saConfig.BearerToken = string(secret.Data[corev1.ServiceAccountTokenKey])
|
||||
|
||||
whoAmI, err := library.NewKubeclient(t, saConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// legacy service account tokens do not have any extra info
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "system:serviceaccount:" + ns.Name + ":" + sa.Name,
|
||||
UID: "", // aggregation drops UID: https://github.com/kubernetes/kubernetes/issues/93699
|
||||
Groups: []string{
|
||||
"system:serviceaccounts",
|
||||
"system:serviceaccounts:" + ns.Name,
|
||||
"system:authenticated",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmI,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhoAmI_ServiceAccount_TokenRequest(t *testing.T) {
|
||||
_ = library.IntegrationEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
kubeClient := library.NewKubernetesClientset(t).CoreV1()
|
||||
|
||||
ns, err := kubeClient.Namespaces().Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "test-whoami-",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
err := kubeClient.Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
sa, err := kubeClient.ServiceAccounts(ns.Name).Create(ctx, &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "test-whoami-",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, tokenRequestProbeErr := kubeClient.ServiceAccounts(ns.Name).CreateToken(ctx, sa.Name, &authenticationv1.TokenRequest{}, metav1.CreateOptions{})
|
||||
if errors.IsNotFound(tokenRequestProbeErr) && tokenRequestProbeErr.Error() == "the server could not find the requested resource" {
|
||||
return // stop test early since the token request API is not enabled on this cluster - other errors are caught below
|
||||
}
|
||||
|
||||
pod, err := kubeClient.Pods(ns.Name).Create(ctx, &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "test-whoami-",
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "ignored-but-required",
|
||||
Image: "does-not-matter",
|
||||
},
|
||||
},
|
||||
ServiceAccountName: sa.Name,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
tokenRequestBadAudience, err := kubeClient.ServiceAccounts(ns.Name).CreateToken(ctx, sa.Name, &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{"should-fail-because-wrong-audience"}, // anything that is not an API server audience
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Pod",
|
||||
APIVersion: "",
|
||||
Name: pod.Name,
|
||||
UID: pod.UID,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
saBadAudConfig := library.NewAnonymousClientRestConfig(t)
|
||||
saBadAudConfig.BearerToken = tokenRequestBadAudience.Status.Token
|
||||
|
||||
_, badAudErr := library.NewKubeclient(t, saBadAudConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.True(t, errors.IsUnauthorized(badAudErr), library.Sdump(badAudErr))
|
||||
|
||||
tokenRequest, err := kubeClient.ServiceAccounts(ns.Name).CreateToken(ctx, sa.Name, &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{},
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Pod",
|
||||
APIVersion: "",
|
||||
Name: pod.Name,
|
||||
UID: pod.UID,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
saTokenReqConfig := library.NewAnonymousClientRestConfig(t)
|
||||
saTokenReqConfig.BearerToken = tokenRequest.Status.Token
|
||||
|
||||
whoAmITokenReq, err := library.NewKubeclient(t, saTokenReqConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// new service account tokens include the pod info in the extra fields
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "system:serviceaccount:" + ns.Name + ":" + sa.Name,
|
||||
UID: "", // aggregation drops UID: https://github.com/kubernetes/kubernetes/issues/93699
|
||||
Groups: []string{
|
||||
"system:serviceaccounts",
|
||||
"system:serviceaccounts:" + ns.Name,
|
||||
"system:authenticated",
|
||||
},
|
||||
Extra: map[string]identityv1alpha1.ExtraValue{
|
||||
"authentication.kubernetes.io/pod-name": {pod.Name},
|
||||
"authentication.kubernetes.io/pod-uid": {string(pod.UID)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmITokenReq,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhoAmI_CSR(t *testing.T) {
|
||||
// use the cluster signing key being available as a proxy for this not being an EKS cluster
|
||||
// we should add more robust logic around skipping clusters based on vendor
|
||||
_ = library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
kubeClient := library.NewKubernetesClientset(t)
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
der, err := x509.MarshalECPrivateKey(privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: keyutil.ECPrivateKeyBlockType, Bytes: der})
|
||||
|
||||
csrPEM, err := cert.MakeCSR(privateKey, &pkix.Name{
|
||||
CommonName: "panda-man",
|
||||
Organization: []string{"living-the-dream", "need-more-sleep"},
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
csrName, csrUID, err := csr.RequestCertificate(
|
||||
kubeClient,
|
||||
csrPEM,
|
||||
"",
|
||||
certificatesv1.KubeAPIServerClientSignerName,
|
||||
[]certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
|
||||
privateKey,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
err := kubeClient.CertificatesV1().CertificateSigningRequests().Delete(ctx, csrName, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
// this is a blind update with no resource version checks, which is only safe during tests
|
||||
_, err = kubeClient.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, csrName, &certificatesv1.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: csrName,
|
||||
},
|
||||
Status: certificatesv1.CertificateSigningRequestStatus{
|
||||
Conditions: []certificatesv1.CertificateSigningRequestCondition{
|
||||
{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "WhoAmICSRTest",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
crtPEM, err := csr.WaitForCertificate(ctx, kubeClient, csrName, csrUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
csrConfig := library.NewAnonymousClientRestConfig(t)
|
||||
csrConfig.CertData = crtPEM
|
||||
csrConfig.KeyData = keyPEM
|
||||
|
||||
whoAmI, err := library.NewKubeclient(t, csrConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "panda-man",
|
||||
Groups: []string{
|
||||
"need-more-sleep",
|
||||
"living-the-dream",
|
||||
"system:authenticated",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmI,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhoAmI_Anonymous(t *testing.T) {
|
||||
_ = library.IntegrationEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
anonymousConfig := library.NewAnonymousClientRestConfig(t)
|
||||
|
||||
whoAmI, err := library.NewKubeclient(t, anonymousConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// this also asserts that all users, even unauthenticated ones, can call this API when anonymous is enabled
|
||||
// this test will need to be skipped when we start running the integration tests against AKS clusters
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "system:anonymous",
|
||||
Groups: []string{
|
||||
"system:unauthenticated",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmI,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhoAmI_ImpersonateDirectly(t *testing.T) {
|
||||
_ = library.IntegrationEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
impersonationConfig := library.NewClientConfig(t)
|
||||
impersonationConfig.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: "solaire",
|
||||
Groups: []string{"astora", "lordran"},
|
||||
Extra: map[string][]string{
|
||||
"covenant": {"warrior-of-sunlight"},
|
||||
"loves": {"sun", "co-op"},
|
||||
},
|
||||
}
|
||||
|
||||
whoAmI, err := library.NewKubeclient(t, impersonationConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "solaire",
|
||||
UID: "", // no way to impersonate UID: https://github.com/kubernetes/kubernetes/issues/93699
|
||||
Groups: []string{
|
||||
"astora",
|
||||
"lordran",
|
||||
"system:authenticated", // impersonation will add this implicitly
|
||||
},
|
||||
Extra: map[string]identityv1alpha1.ExtraValue{
|
||||
"covenant": {"warrior-of-sunlight"},
|
||||
"loves": {"sun", "co-op"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmI,
|
||||
)
|
||||
|
||||
impersonationAnonymousConfig := library.NewClientConfig(t)
|
||||
impersonationAnonymousConfig.Impersonate.UserName = "system:anonymous"
|
||||
|
||||
whoAmIAnonymous, err := library.NewKubeclient(t, impersonationAnonymousConfig).PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t,
|
||||
&identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "system:anonymous",
|
||||
Groups: []string{
|
||||
"system:unauthenticated", // impersonation will add this implicitly
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whoAmIAnonymous,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhoAmI_ImpersonateViaProxy(t *testing.T) {
|
||||
_ = library.IntegrationEnv(t)
|
||||
|
||||
// TODO: add this test after the impersonation proxy is done
|
||||
// this should test all forms of auth understood by the proxy (certs, SA token, token cred req, anonymous, etc)
|
||||
// remember that impersonation does not support UID: https://github.com/kubernetes/kubernetes/issues/93699
|
||||
}
|
@ -72,25 +72,25 @@ func NewClientsetWithCertAndKey(t *testing.T, clientCertificateData, clientKeyDa
|
||||
func NewKubernetesClientset(t *testing.T) kubernetes.Interface {
|
||||
t.Helper()
|
||||
|
||||
return newKubeclient(t, NewClientConfig(t)).Kubernetes
|
||||
return NewKubeclient(t, NewClientConfig(t)).Kubernetes
|
||||
}
|
||||
|
||||
func NewSupervisorClientset(t *testing.T) supervisorclientset.Interface {
|
||||
t.Helper()
|
||||
|
||||
return newKubeclient(t, NewClientConfig(t)).PinnipedSupervisor
|
||||
return NewKubeclient(t, NewClientConfig(t)).PinnipedSupervisor
|
||||
}
|
||||
|
||||
func NewConciergeClientset(t *testing.T) conciergeclientset.Interface {
|
||||
t.Helper()
|
||||
|
||||
return newKubeclient(t, NewClientConfig(t)).PinnipedConcierge
|
||||
return NewKubeclient(t, NewClientConfig(t)).PinnipedConcierge
|
||||
}
|
||||
|
||||
func NewAnonymousConciergeClientset(t *testing.T) conciergeclientset.Interface {
|
||||
t.Helper()
|
||||
|
||||
return newKubeclient(t, newAnonymousClientRestConfig(t)).PinnipedConcierge
|
||||
return NewKubeclient(t, NewAnonymousClientRestConfig(t)).PinnipedConcierge
|
||||
}
|
||||
|
||||
func NewAggregatedClientset(t *testing.T) aggregatorclient.Interface {
|
||||
@ -118,7 +118,7 @@ func newClientsetWithConfig(t *testing.T, config *rest.Config) kubernetes.Interf
|
||||
}
|
||||
|
||||
// Returns a rest.Config without any user authentication info.
|
||||
func newAnonymousClientRestConfig(t *testing.T) *rest.Config {
|
||||
func NewAnonymousClientRestConfig(t *testing.T) *rest.Config {
|
||||
t.Helper()
|
||||
|
||||
return rest.AnonymousClientConfig(NewClientConfig(t))
|
||||
@ -128,13 +128,13 @@ func newAnonymousClientRestConfig(t *testing.T) *rest.Config {
|
||||
func newAnonymousClientRestConfigWithCertAndKeyAdded(t *testing.T, clientCertificateData, clientKeyData string) *rest.Config {
|
||||
t.Helper()
|
||||
|
||||
config := newAnonymousClientRestConfig(t)
|
||||
config := NewAnonymousClientRestConfig(t)
|
||||
config.CertData = []byte(clientCertificateData)
|
||||
config.KeyData = []byte(clientKeyData)
|
||||
return config
|
||||
}
|
||||
|
||||
func newKubeclient(t *testing.T, config *rest.Config) *kubeclient.Client {
|
||||
func NewKubeclient(t *testing.T, config *rest.Config) *kubeclient.Client {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
client, err := kubeclient.New(
|
||||
|
Loading…
Reference in New Issue
Block a user