add spokecluster admission hook

This commit is contained in:
liuwei
2020-05-18 15:47:18 +08:00
parent b6b71113d5
commit fb20b5005a
19 changed files with 1234 additions and 13 deletions
+2
View File
@@ -15,6 +15,7 @@ import (
"github.com/open-cluster-management/registration/pkg/cmd/hub"
"github.com/open-cluster-management/registration/pkg/cmd/spoke"
"github.com/open-cluster-management/registration/pkg/cmd/webhook"
"github.com/open-cluster-management/registration/pkg/version"
)
@@ -55,6 +56,7 @@ func newRegistrationCommand() *cobra.Command {
cmd.AddCommand(hub.NewController())
cmd.AddCommand(spoke.NewAgent())
cmd.AddCommand(webhook.NewAdmissionHook())
return cmd
}
+7
View File
@@ -18,6 +18,10 @@ rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Allow hub to create subjectaccessreviews
- apiGroups: ["authorization.k8s.io"]
resources: ["subjectaccessreviews"]
verbs: ["create"]
# Allow hub to manage spokeclusters
- apiGroups: ["cluster.open-cluster-management.io"]
resources: ["spokeclusters"]
@@ -28,3 +32,6 @@ rules:
- apiGroups: ["register.open-cluster-management.io"]
resources: ["spokeclusters/clientcertificates"]
verbs: ["renew"]
- apiGroups: ["register.open-cluster-management.io"]
resources: ["spokeclusters/acceptance"]
verbs: ["update"]
+20 -13
View File
@@ -54,19 +54,26 @@ spec:
to grant the permision of access from agent on spoke. When the value
is set false, the namespace representing the spoke cluster is deleted.
type: boolean
spokeClientConfig:
description: SpokeClientConfig represents the apiserver address of the
spoke cluster
type: object
properties:
caBundle:
description: CABundle is the ca bundle to connect to apiserver of
the spoke cluster. System certs are used if it is not set.
type: string
format: byte
url:
description: URL is the url of apiserver endpoint of the spoke cluster.
type: string
spokeClientConfigs:
description: SpokeClientConfigs represents a list of the apiserver address
of the spoke cluster. If it is empty, spoke cluster has no accessible
address to be visited from hub.
type: array
items:
description: ClientConfig represents the apiserver address of the
spoke cluster. TODO include credential to connect to spoke cluster
kube-apiserver
type: object
properties:
caBundle:
description: CABundle is the ca bundle to connect to apiserver
of the spoke cluster. System certs are used if it is not set.
type: string
format: byte
url:
description: URL is the url of apiserver endpoint of the spoke
cluster.
type: string
status:
description: Status represents the current status of joined spoke cluster
type: object
+13
View File
@@ -0,0 +1,13 @@
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1.admission.cluster.open-cluster-management.io
spec:
group: admission.cluster.open-cluster-management.io
version: v1
service:
name: spokecluster-admission
namespace: open-cluster-management
insecureSkipTLSVerify: true
groupPriorityMinimum: 10000
versionPriority: 20
+26
View File
@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spokecluster-admission
labels:
app: spokecluster-admission
spec:
replicas: 1
selector:
matchLabels:
app: spokecluster-admission
template:
metadata:
labels:
app: spokecluster-admission
spec:
serviceAccountName: hub-sa
containers:
- name: spokecluster-admission
image: quay.io/open-cluster-management/registration:latest
imagePullPolicy: IfNotPresent
args:
- "/registration"
- "webhook"
- "--cert-dir=/tmp"
- "--secure-port=6443"
+31
View File
@@ -0,0 +1,31 @@
# Adds namespace to all resources.
namespace: open-cluster-management
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
#namePrefix: multicloud-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
# Each entry in this list must resolve to an existing
# resource definition in YAML. These are the resource
# files that kustomize reads, modifies and emits as a
# YAML string, with resources separated by document
# markers ("---").
#
# General rule here is anything deployed by OLM bundles should go here as well,
# this is used in "make deploy" for developers and should mimic what OLM deploys
# for you. CRDs are an exception to this as we don't want to have to list them all
# here. These are deployed via a "make install" dependency.
resources:
- ./apiservice.yaml
- ./deployment.yaml
- ./service.yaml
- ./webhook.yaml
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: spokecluster-admission
spec:
selector:
app: spokecluster-admission
ports:
- port: 443
targetPort: 6443
+24
View File
@@ -0,0 +1,24 @@
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: spokeclustervalidators.admission.cluster.open-cluster-management.io
webhooks:
- name: spokeclustervalidators.admission.cluster.open-cluster-management.io
clientConfig:
service:
# reach the webhook via the registered aggregated API
namespace: default
name: kubernetes
path: /apis/admission.cluster.open-cluster-management.io/v1/spokeclustervalidators
rules:
- operations:
- CREATE
- UPDATE
apiGroups:
- cluster.open-cluster-management.io
apiVersions:
- v1
resources:
- spokeclusters
failurePolicy: Fail
timeoutSeconds: 3
+2
View File
@@ -8,11 +8,13 @@ require (
github.com/onsi/gomega v1.8.1
github.com/open-cluster-management/api v0.0.0-20200512175145-bed9ce79e17e
github.com/openshift/build-machinery-go v0.0.0-20200211121458-5e3d6e570160
github.com/openshift/generic-admission-server v1.14.1-0.20200514123932-ccc9079d8bdb
github.com/openshift/library-go v0.0.0-20200401114229-ffab8c6e83a9
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
k8s.io/api v0.18.2
k8s.io/apimachinery v0.18.2
k8s.io/apiserver v0.18.2
k8s.io/client-go v0.18.2
k8s.io/component-base v0.18.2
k8s.io/klog v1.0.0
+7
View File
@@ -3,6 +3,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
@@ -275,6 +276,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -297,6 +299,8 @@ github.com/openshift/build-machinery-go v0.0.0-20200211121458-5e3d6e570160 h1:V4
github.com/openshift/build-machinery-go v0.0.0-20200211121458-5e3d6e570160/go.mod h1:1CkcsT3aVebzRBzVTSbiKSkJMsC/CASqxesfqEMfJEc=
github.com/openshift/client-go v0.0.0-20200326155132-2a6cd50aedd0 h1:kMiuiZXH1GdfbiMwsuAQOqGaMxlo9NCUk0wT4XAdfNM=
github.com/openshift/client-go v0.0.0-20200326155132-2a6cd50aedd0/go.mod h1:uUQ4LClRO+fg5MF/P6QxjMCb1C9f7Oh4RKepftDnEJE=
github.com/openshift/generic-admission-server v1.14.1-0.20200514123932-ccc9079d8bdb h1:SfOkJiP0FDdJijQKtA808PpL/i9CkvVAgTE7yOO9ltI=
github.com/openshift/generic-admission-server v1.14.1-0.20200514123932-ccc9079d8bdb/go.mod h1:86YzeUDgE5LpiqJov3uXbAWEkcGBpX3p3t/yGLlKHqA=
github.com/openshift/library-go v0.0.0-20200401114229-ffab8c6e83a9 h1:aEcgb9sKGmHK2IP9IcyUL11XYle0XNVnrG4871MXXIw=
github.com/openshift/library-go v0.0.0-20200401114229-ffab8c6e83a9/go.mod h1:CfydoH0B+RYs22uQZQ36A1mz5m5zhucpMGh8t5s71v4=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
@@ -515,6 +519,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -569,6 +574,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7 h1:uuHDyjllyzRyCI
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM=
sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo=
sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ=
sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
+37
View File
@@ -0,0 +1,37 @@
package webhook
import (
"os"
"github.com/open-cluster-management/registration/pkg/webhook"
admissionserver "github.com/openshift/generic-admission-server/pkg/cmd/server"
"github.com/spf13/cobra"
genericapiserver "k8s.io/apiserver/pkg/server"
)
func NewAdmissionHook() *cobra.Command {
o := admissionserver.NewAdmissionServerOptions(os.Stdout, os.Stderr, &webhook.SpokeClusterAdmissionHook{})
cmd := &cobra.Command{
Use: "webhook",
Short: "Start Spoke Cluster Admission Server",
RunE: func(c *cobra.Command, args []string) error {
stopCh := genericapiserver.SetupSignalHandler()
if err := o.Complete(); err != nil {
return err
}
if err := o.Validate(args); err != nil {
return err
}
if err := o.RunAdmissionServer(stopCh); err != nil {
return err
}
return nil
},
}
o.RecommendedOptions.AddFlags(cmd.Flags())
return cmd
}
+2
View File
@@ -0,0 +1,2 @@
// package webhook contains the spoke cluster admission hook to validate the SpokeCluster create and update operations
package webhook
+203
View File
@@ -0,0 +1,203 @@
package webhook
import (
"context"
"encoding/json"
"fmt"
"net/http"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
clusterv1 "github.com/open-cluster-management/api/cluster/v1"
"github.com/open-cluster-management/registration/pkg/helpers"
operatorhelpers "github.com/openshift/library-go/pkg/operator/v1helpers"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
)
// SpokeClusterAdmissionHook will validate the creating/updating spokeclusters request.
type SpokeClusterAdmissionHook struct {
kubeClient kubernetes.Interface
}
// ValidatingResource is called by generic-admission-server on startup to register the returned REST resource through which the
// webhook is accessed by the kube apiserver.
func (a *SpokeClusterAdmissionHook) ValidatingResource() (plural schema.GroupVersionResource, singular string) {
return schema.GroupVersionResource{
Group: "admission.cluster.open-cluster-management.io",
Version: "v1",
Resource: "spokeclustervalidators",
},
"spokeclustervalidator"
}
// Validate is called by generic-admission-server when the registered REST resource above is called with an admission request.
func (a *SpokeClusterAdmissionHook) Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
klog.V(4).Infof("validate %q operation for object %q", admissionSpec.Operation, admissionSpec.Object)
status := &admissionv1beta1.AdmissionResponse{}
// only validate the request for spokeclusters
if admissionSpec.Resource.Group != "cluster.open-cluster-management.io" ||
admissionSpec.Resource.Version != "v1" ||
admissionSpec.Resource.Resource != "spokeclusters" {
status.Allowed = true
return status
}
switch admissionSpec.Operation {
case admissionv1beta1.Create:
return a.validateCreateRequest(admissionSpec)
case admissionv1beta1.Update:
return a.validateUpdateRequest(admissionSpec)
default:
status.Allowed = true
return status
}
}
// Initialize is called by generic-admission-server on startup to setup initialization that spokeclusters webhook needs.
func (a *SpokeClusterAdmissionHook) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
var err error
a.kubeClient, err = kubernetes.NewForConfig(kubeClientConfig)
return err
}
// validateCreateRequest validates create spoke cluster operation
func (a *SpokeClusterAdmissionHook) validateCreateRequest(request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
status := &admissionv1beta1.AdmissionResponse{}
// validate SpokeCluster object firstly
spokeCluster, err := a.validateSpokeClusterObj(request.Object)
if err != nil {
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
Message: err.Error(),
}
return status
}
// the HubAcceptsClient field is not changed, finish the validation process
if !spokeCluster.Spec.HubAcceptsClient {
status.Allowed = true
return status
}
// the HubAcceptsClient field is changed, we need to check the request user whether
// has been allowed to change the HubAcceptsClient field with SubjectAccessReview api
return a.allowUpdateAcceptField(request.UserInfo)
}
// validateUpdateRequest validates update spoke cluster operation.
func (a *SpokeClusterAdmissionHook) validateUpdateRequest(request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
status := &admissionv1beta1.AdmissionResponse{}
oldSpokeCluster := &clusterv1.SpokeCluster{}
if err := json.Unmarshal(request.OldObject.Raw, oldSpokeCluster); err != nil {
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
Message: err.Error(),
}
return status
}
// validate the updating SpokeCluster object firstly
newSpokeCluster, err := a.validateSpokeClusterObj(request.Object)
if err != nil {
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
Message: err.Error(),
}
return status
}
// the HubAcceptsClient field is not changed, finish the validation process
if newSpokeCluster.Spec.HubAcceptsClient == oldSpokeCluster.Spec.HubAcceptsClient {
status.Allowed = true
return status
}
// the HubAcceptsClient field is changed, we need to check the request user whether
// has been allowed to update the HubAcceptsClient field with SubjectAccessReview api
return a.allowUpdateAcceptField(request.UserInfo)
}
// validateSpokeClusterObj validates the fileds of SpokeCluster object
func (a *SpokeClusterAdmissionHook) validateSpokeClusterObj(requestObj runtime.RawExtension) (*clusterv1.SpokeCluster, error) {
errs := []error{}
spokeCluster := &clusterv1.SpokeCluster{}
if err := json.Unmarshal(requestObj.Raw, spokeCluster); err != nil {
errs = append(errs, err)
}
// there are no spoke client configs, finish the validation process
if len(spokeCluster.Spec.SpokeClientConfigs) == 0 {
return spokeCluster, operatorhelpers.NewMultiLineAggregate(errs)
}
// validate the url in spoke client configs
for _, clientConfig := range spokeCluster.Spec.SpokeClientConfigs {
if !helpers.IsValidHTTPSURL(clientConfig.URL) {
errs = append(errs, fmt.Errorf("url %q is invalid in spoke client configs", clientConfig.URL))
}
}
return spokeCluster, operatorhelpers.NewMultiLineAggregate(errs)
}
// allowUpdateHubAcceptsClientField using SubjectAccessReview API to check whether a request user has been authorized to update
// HubAcceptsClient field
func (a *SpokeClusterAdmissionHook) allowUpdateAcceptField(userInfo authenticationv1.UserInfo) *admissionv1beta1.AdmissionResponse {
status := &admissionv1beta1.AdmissionResponse{}
extra := make(map[string]authorizationv1.ExtraValue)
for k, v := range userInfo.Extra {
extra[k] = authorizationv1.ExtraValue(v)
}
sar := &authorizationv1.SubjectAccessReview{
Spec: authorizationv1.SubjectAccessReviewSpec{
User: userInfo.Username,
UID: userInfo.UID,
Groups: userInfo.Groups,
Extra: extra,
ResourceAttributes: &authorizationv1.ResourceAttributes{
Group: "register.open-cluster-management.io",
Resource: "spokeclusters",
Verb: "update",
Subresource: "acceptance",
},
},
}
sar, err := a.kubeClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
if err != nil {
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusForbidden, Reason: metav1.StatusReasonForbidden,
Message: err.Error(),
}
return status
}
if !sar.Status.Allowed {
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusForbidden, Reason: metav1.StatusReasonForbidden,
Message: fmt.Sprintf("user %q cannot update the HubAcceptsClient field", userInfo.Username),
}
return status
}
status.Allowed = true
return status
}
+219
View File
@@ -0,0 +1,219 @@
package webhook
import (
"encoding/json"
"net/http"
"reflect"
"testing"
clusterv1 "github.com/open-cluster-management/api/cluster/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
authorizationv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
)
var spokeclustersSchema = metav1.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1",
Resource: "spokeclusters",
}
func TestSpokeClusterValidate(t *testing.T) {
cases := []struct {
name string
request *admissionv1beta1.AdmissionRequest
expectedResponse *admissionv1beta1.AdmissionResponse
allowUpdateAcceptField bool
}{
{
name: "validate non-spokeclusters request",
request: &admissionv1beta1.AdmissionRequest{
Resource: metav1.GroupVersionResource{
Group: "test.open-cluster-management.io",
Version: "v1",
Resource: "tests",
},
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: true,
},
},
{
name: "validate deleting operation",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Delete,
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: true,
},
},
{
name: "validate creating SpokeCluster",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Create,
Object: newSpokeClusterObj(),
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: true,
},
},
{
name: "validate creating SpokeCluster with invalid fields",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Create,
Object: newSpokeClusterObjWithClientConfigs(clusterv1.ClientConfig{URL: "http://127.0.0.1:8001"}),
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
Message: "url \"http://127.0.0.1:8001\" is invalid in spoke client configs",
},
},
},
{
name: "validate creating an accepted SpokeCluster without update acceptance permission",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Create,
Object: newSpokeClusterObjWithHubAcceptsClient(true),
UserInfo: authenticationv1.UserInfo{Username: "tester"},
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusForbidden, Reason: metav1.StatusReasonForbidden,
Message: "user \"tester\" cannot update the HubAcceptsClient field",
},
},
},
{
name: "validate creating an accepted SpokeCluster",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Create,
Object: newSpokeClusterObjWithHubAcceptsClient(true),
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: true,
},
allowUpdateAcceptField: true,
},
{
name: "validate update SpokeCluster without HubAcceptsClient field changed",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Update,
OldObject: newSpokeClusterObjWithClientConfigs(clusterv1.ClientConfig{URL: "https://127.0.0.1:6443"}),
Object: newSpokeClusterObjWithClientConfigs(clusterv1.ClientConfig{URL: "https://127.0.0.1:8443"}),
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: true,
},
},
{
name: "validate updating HubAcceptsClient field without update acceptance permission",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Update,
OldObject: newSpokeClusterObjWithHubAcceptsClient(false),
Object: newSpokeClusterObjWithHubAcceptsClient(true),
UserInfo: authenticationv1.UserInfo{Username: "tester"},
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusForbidden, Reason: metav1.StatusReasonForbidden,
Message: "user \"tester\" cannot update the HubAcceptsClient field",
},
},
},
{
name: "validate updating HubAcceptsClient field",
request: &admissionv1beta1.AdmissionRequest{
Resource: spokeclustersSchema,
Operation: admissionv1beta1.Update,
OldObject: newSpokeClusterObjWithHubAcceptsClient(false),
Object: newSpokeClusterObjWithHubAcceptsClient(true),
},
expectedResponse: &admissionv1beta1.AdmissionResponse{
Allowed: true,
},
allowUpdateAcceptField: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
kubeClient := kubefake.NewSimpleClientset()
kubeClient.PrependReactor(
"create",
"subjectaccessreviews",
func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &authorizationv1.SubjectAccessReview{
Status: authorizationv1.SubjectAccessReviewStatus{
Allowed: c.allowUpdateAcceptField,
},
}, nil
},
)
admissionHook := &SpokeClusterAdmissionHook{kubeClient: kubeClient}
actualResponse := admissionHook.Validate(c.request)
if !reflect.DeepEqual(actualResponse, c.expectedResponse) {
t.Errorf("expected %#v but got: %#v", c.expectedResponse.Result, actualResponse.Result)
}
})
}
}
func newSpokeClusterObj() runtime.RawExtension {
spokeCluster := &clusterv1.SpokeCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "testspokecluster",
},
}
clusterObj, _ := json.Marshal(spokeCluster)
return runtime.RawExtension{
Raw: clusterObj,
}
}
func newSpokeClusterObjWithHubAcceptsClient(hubAcceptsClient bool) runtime.RawExtension {
spokeCluster := &clusterv1.SpokeCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "testspokecluster",
},
Spec: clusterv1.SpokeClusterSpec{
HubAcceptsClient: hubAcceptsClient,
},
}
clusterObj, _ := json.Marshal(spokeCluster)
return runtime.RawExtension{
Raw: clusterObj,
}
}
func newSpokeClusterObjWithClientConfigs(clientConfig clusterv1.ClientConfig) runtime.RawExtension {
spokeCluster := &clusterv1.SpokeCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "testspokecluster",
},
Spec: clusterv1.SpokeClusterSpec{
SpokeClientConfigs: []clusterv1.ClientConfig{clientConfig},
},
}
clusterObj, _ := json.Marshal(spokeCluster)
return runtime.RawExtension{
Raw: clusterObj,
}
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,267 @@
package apiserver
import (
"fmt"
"strings"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
restclient "k8s.io/client-go/rest"
"github.com/openshift/generic-admission-server/pkg/registry/admissionreview"
)
var (
Scheme = runtime.NewScheme()
Codecs = serializer.NewCodecFactory(Scheme)
)
type AdmissionHook interface {
// Initialize is called as a post-start hook
Initialize(kubeClientConfig *restclient.Config, stopCh <-chan struct{}) error
}
type ValidatingAdmissionHook interface {
AdmissionHook
// ValidatingResource is the resource to use for hosting your admission webhook. If the hook implements
// MutatingAdmissionHook as well, the two resources for validating and mutating admission must be different.
// Note: this is (usually) not the same as the payload resource!
ValidatingResource() (plural schema.GroupVersionResource, singular string)
// Validate is called to decide whether to accept the admission request. The returned AdmissionResponse
// must not use the Patch field.
Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse
}
type MutatingAdmissionHook interface {
AdmissionHook
// MutatingResource is the resource to use for hosting your admission webhook. If the hook implements
// ValidatingAdmissionHook as well, the two resources for validating and mutating admission must be different.
// Note: this is (usually) not the same as the payload resource!
MutatingResource() (plural schema.GroupVersionResource, singular string)
// Admit is called to decide whether to accept the admission request. The returned AdmissionResponse may
// use the Patch field to mutate the object from the passed AdmissionRequest.
Admit(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse
}
func init() {
admissionv1beta1.AddToScheme(Scheme)
// we need to add the options to empty v1
// TODO fix the server code to avoid this
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
// TODO: keep the generic API server from wanting this
unversioned := schema.GroupVersion{Group: "", Version: "v1"}
Scheme.AddUnversionedTypes(unversioned,
&metav1.Status{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
)
}
type Config struct {
GenericConfig *genericapiserver.RecommendedConfig
ExtraConfig ExtraConfig
}
type ExtraConfig struct {
AdmissionHooks []AdmissionHook
}
// AdmissionServer contains state for a Kubernetes cluster master/api server.
type AdmissionServer struct {
GenericAPIServer *genericapiserver.GenericAPIServer
}
type completedConfig struct {
GenericConfig genericapiserver.CompletedConfig
ExtraConfig *ExtraConfig
}
type CompletedConfig struct {
// Embed a private pointer that cannot be instantiated outside of this package.
*completedConfig
}
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
func (c *Config) Complete() CompletedConfig {
completedCfg := completedConfig{
c.GenericConfig.Complete(),
&c.ExtraConfig,
}
completedCfg.GenericConfig.Version = &version.Info{
Major: "1",
Minor: "1",
}
return CompletedConfig{&completedCfg}
}
// New returns a new instance of AdmissionServer from the given config.
func (c completedConfig) New() (*AdmissionServer, error) {
genericServer, err := c.GenericConfig.New("admission-server", genericapiserver.NewEmptyDelegate()) // completion is done in Complete, no need for a second time
if err != nil {
return nil, err
}
s := &AdmissionServer{
GenericAPIServer: genericServer,
}
inClusterConfig, err := restclient.InClusterConfig()
if err != nil {
return nil, err
}
for _, versionMap := range admissionHooksByGroupThenVersion(c.ExtraConfig.AdmissionHooks...) {
// TODO we're going to need a later k8s.io/apiserver so that we can get discovery to list a different group version for
// our endpoint which we'll use to back some custom storage which will consume the AdmissionReview type and give back the correct response
apiGroupInfo := genericapiserver.APIGroupInfo{
VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
// TODO unhardcode this. It was hardcoded before, but we need to re-evaluate
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
Scheme: Scheme,
ParameterCodec: metav1.ParameterCodec,
NegotiatedSerializer: Codecs,
}
for _, admissionHooks := range versionMap {
for i := range admissionHooks {
admissionHook := admissionHooks[i]
admissionResource, _ := admissionHook.Resource()
admissionVersion := admissionResource.GroupVersion()
// just overwrite the groupversion with a random one. We don't really care or know.
apiGroupInfo.PrioritizedVersions = appendUniqueGroupVersion(apiGroupInfo.PrioritizedVersions, admissionVersion)
admissionReview := admissionreview.NewREST(admissionHook.Admission)
v1alpha1storage, ok := apiGroupInfo.VersionedResourcesStorageMap[admissionVersion.Version]
if !ok {
v1alpha1storage = map[string]rest.Storage{}
}
v1alpha1storage[admissionResource.Resource] = admissionReview
apiGroupInfo.VersionedResourcesStorageMap[admissionVersion.Version] = v1alpha1storage
}
}
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
}
for i := range c.ExtraConfig.AdmissionHooks {
admissionHook := c.ExtraConfig.AdmissionHooks[i]
postStartName := postStartHookName(admissionHook)
if len(postStartName) == 0 {
continue
}
s.GenericAPIServer.AddPostStartHookOrDie(postStartName,
func(context genericapiserver.PostStartHookContext) error {
return admissionHook.Initialize(inClusterConfig, context.StopCh)
},
)
}
return s, nil
}
func appendUniqueGroupVersion(slice []schema.GroupVersion, elems ...schema.GroupVersion) []schema.GroupVersion {
m := map[schema.GroupVersion]bool{}
for _, gv := range slice {
m[gv] = true
}
for _, e := range elems {
m[e] = true
}
out := make([]schema.GroupVersion, 0, len(m))
for gv := range m {
out = append(out, gv)
}
return out
}
func postStartHookName(hook AdmissionHook) string {
var ns []string
if mutatingHook, ok := hook.(MutatingAdmissionHook); ok {
gvr, _ := mutatingHook.MutatingResource()
ns = append(ns, fmt.Sprintf("mutating-%s.%s.%s", gvr.Resource, gvr.Version, gvr.Group))
}
if validatingHook, ok := hook.(ValidatingAdmissionHook); ok {
gvr, _ := validatingHook.ValidatingResource()
ns = append(ns, fmt.Sprintf("validating-%s.%s.%s", gvr.Resource, gvr.Version, gvr.Group))
}
if len(ns) == 0 {
return ""
}
return strings.Join(append(ns, "init"), "-")
}
func admissionHooksByGroupThenVersion(admissionHooks ...AdmissionHook) map[string]map[string][]admissionHookWrapper {
ret := map[string]map[string][]admissionHookWrapper{}
for i := range admissionHooks {
if mutatingHook, ok := admissionHooks[i].(MutatingAdmissionHook); ok {
gvr, _ := mutatingHook.MutatingResource()
group, ok := ret[gvr.Group]
if !ok {
group = map[string][]admissionHookWrapper{}
ret[gvr.Group] = group
}
group[gvr.Version] = append(group[gvr.Version], mutatingAdmissionHookWrapper{mutatingHook})
}
if validatingHook, ok := admissionHooks[i].(ValidatingAdmissionHook); ok {
gvr, _ := validatingHook.ValidatingResource()
group, ok := ret[gvr.Group]
if !ok {
group = map[string][]admissionHookWrapper{}
ret[gvr.Group] = group
}
group[gvr.Version] = append(group[gvr.Version], validatingAdmissionHookWrapper{validatingHook})
}
}
return ret
}
// admissionHookWrapper wraps either a validating or mutating admission hooks, calling the respective resource and admission method.
type admissionHookWrapper interface {
Resource() (plural schema.GroupVersionResource, singular string)
Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse
}
type mutatingAdmissionHookWrapper struct {
hook MutatingAdmissionHook
}
func (h mutatingAdmissionHookWrapper) Resource() (plural schema.GroupVersionResource, singular string) {
return h.hook.MutatingResource()
}
func (h mutatingAdmissionHookWrapper) Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
return h.hook.Admit(admissionSpec)
}
type validatingAdmissionHookWrapper struct {
hook ValidatingAdmissionHook
}
func (h validatingAdmissionHookWrapper) Resource() (plural schema.GroupVersionResource, singular string) {
return h.hook.ValidatingResource()
}
func (h validatingAdmissionHookWrapper) Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
return h.hook.Validate(admissionSpec)
}
@@ -0,0 +1,114 @@
package server
import (
"fmt"
"io"
"net"
"github.com/spf13/cobra"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options"
"github.com/openshift/generic-admission-server/pkg/apiserver"
)
const defaultEtcdPathPrefix = "/registry/online.openshift.io"
type AdmissionServerOptions struct {
RecommendedOptions *genericoptions.RecommendedOptions
AdmissionHooks []apiserver.AdmissionHook
StdOut io.Writer
StdErr io.Writer
}
func NewAdmissionServerOptions(out, errOut io.Writer, admissionHooks ...apiserver.AdmissionHook) *AdmissionServerOptions {
o := &AdmissionServerOptions{
// TODO we will nil out the etcd storage options. This requires a later level of k8s.io/apiserver
RecommendedOptions: genericoptions.NewRecommendedOptions(
defaultEtcdPathPrefix,
apiserver.Codecs.LegacyCodec(admissionv1beta1.SchemeGroupVersion),
nil,
),
AdmissionHooks: admissionHooks,
StdOut: out,
StdErr: errOut,
}
o.RecommendedOptions.Etcd = nil
o.RecommendedOptions.Admission = nil
return o
}
// NewCommandStartMaster provides a CLI handler for 'start master' command
func NewCommandStartAdmissionServer(out, errOut io.Writer, stopCh <-chan struct{}, admissionHooks ...apiserver.AdmissionHook) *cobra.Command {
o := NewAdmissionServerOptions(out, errOut, admissionHooks...)
cmd := &cobra.Command{
Short: "Launch a namespace reservation API server",
Long: "Launch a namespace reservation API server",
RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(); err != nil {
return err
}
if err := o.Validate(args); err != nil {
return err
}
if err := o.RunAdmissionServer(stopCh); err != nil {
return err
}
return nil
},
}
flags := cmd.Flags()
o.RecommendedOptions.AddFlags(flags)
return cmd
}
func (o AdmissionServerOptions) Validate(args []string) error {
return nil
}
func (o *AdmissionServerOptions) Complete() error {
return nil
}
func (o AdmissionServerOptions) Config() (*apiserver.Config, error) {
// TODO have a "real" external address
if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1")}); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
}
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil {
return nil, err
}
config := &apiserver.Config{
GenericConfig: serverConfig,
ExtraConfig: apiserver.ExtraConfig{
AdmissionHooks: o.AdmissionHooks,
},
}
return config, nil
}
func (o AdmissionServerOptions) RunAdmissionServer(stopCh <-chan struct{}) error {
config, err := o.Config()
if err != nil {
return err
}
server, err := config.Complete().New()
if err != nil {
return err
}
return server.GenericAPIServer.PrepareRun().Run(stopCh)
}
@@ -0,0 +1,45 @@
package admissionreview
import (
"context"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
)
type AdmissionHookFunc func(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse
type REST struct {
hookFn AdmissionHookFunc
}
var _ rest.Creater = &REST{}
var _ rest.Scoper = &REST{}
var _ rest.GroupVersionKindProvider = &REST{}
func NewREST(hookFn AdmissionHookFunc) *REST {
return &REST{
hookFn: hookFn,
}
}
func (r *REST) New() runtime.Object {
return &admissionv1beta1.AdmissionReview{}
}
func (r *REST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
return admissionv1beta1.SchemeGroupVersion.WithKind("AdmissionReview")
}
func (r *REST) NamespaceScoped() bool {
return false
}
func (r *REST) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) {
admissionReview := obj.(*admissionv1beta1.AdmissionReview)
admissionReview.Response = r.hookFn(admissionReview.Request)
return admissionReview, nil
}
+4
View File
@@ -201,6 +201,10 @@ github.com/openshift/build-machinery-go/make/targets/golang
github.com/openshift/build-machinery-go/make/targets/openshift
github.com/openshift/build-machinery-go/make/targets/openshift/operator
github.com/openshift/build-machinery-go/scripts
# github.com/openshift/generic-admission-server v1.14.1-0.20200514123932-ccc9079d8bdb
github.com/openshift/generic-admission-server/pkg/apiserver
github.com/openshift/generic-admission-server/pkg/cmd/server
github.com/openshift/generic-admission-server/pkg/registry/admissionreview
# github.com/openshift/library-go v0.0.0-20200401114229-ffab8c6e83a9
github.com/openshift/library-go/pkg/assets
github.com/openshift/library-go/pkg/config/client