mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-23 22:26:49 +00:00
add spokecluster admission hook
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: spokecluster-admission
|
||||
spec:
|
||||
selector:
|
||||
app: spokecluster-admission
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 6443
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// package webhook contains the spoke cluster admission hook to validate the SpokeCluster create and update operations
|
||||
package webhook
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
+267
@@ -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)
|
||||
}
|
||||
+114
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+45
@@ -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
|
||||
}
|
||||
Vendored
+4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user