mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-23 22:26:49 +00:00
hub csr controller
This commit is contained in:
@@ -11,6 +11,7 @@ include $(addprefix ./vendor/github.com/openshift/build-machinery-go/make/, \
|
||||
)
|
||||
|
||||
$(call add-bindata,spokecluster,./pkg/hub/spokecluster/manifests/...,bindata,bindata,./pkg/hub/spokecluster/bindata/bindata.go)
|
||||
$(call add-bindata,csr,./pkg/hub/csr/manifests/...,bindata,bindata,./pkg/hub/csr/bindata/bindata.go)
|
||||
|
||||
clean:
|
||||
$(RM) ./registration
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// Code generated by go-bindata.
|
||||
// sources:
|
||||
// pkg/hub/csr/manifests/clusterrole.yaml
|
||||
// pkg/hub/csr/manifests/clusterrolebinding.yaml
|
||||
// DO NOT EDIT!
|
||||
|
||||
package bindata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type asset struct {
|
||||
bytes []byte
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
type bindataFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (fi bindataFileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
func (fi bindataFileInfo) Size() int64 {
|
||||
return fi.size
|
||||
}
|
||||
func (fi bindataFileInfo) Mode() os.FileMode {
|
||||
return fi.mode
|
||||
}
|
||||
func (fi bindataFileInfo) ModTime() time.Time {
|
||||
return fi.modTime
|
||||
}
|
||||
func (fi bindataFileInfo) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
func (fi bindataFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _pkgHubCsrManifestsClusterroleYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}-csrapproved
|
||||
rules:
|
||||
# Allow spoke agent to create/get/list/watch certificatesigningrequests
|
||||
- apiGroups: ["certificates.k8s.io"]
|
||||
resources: ["certificatesigningrequests"]
|
||||
verbs: ["create", "get", "list", "watch"]
|
||||
# Allow spoke agent to create/get/list/watch spoke cluster
|
||||
- apiGroups: ["cluster.open-cluster-management.io"]
|
||||
resources: ["spokeclusters"]
|
||||
verbs: ["create", "get", "list", "watch"]
|
||||
`)
|
||||
|
||||
func pkgHubCsrManifestsClusterroleYamlBytes() ([]byte, error) {
|
||||
return _pkgHubCsrManifestsClusterroleYaml, nil
|
||||
}
|
||||
|
||||
func pkgHubCsrManifestsClusterroleYaml() (*asset, error) {
|
||||
bytes, err := pkgHubCsrManifestsClusterroleYamlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "pkg/hub/csr/manifests/clusterrole.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var _pkgHubCsrManifestsClusterrolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}-csrapproved
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}-csrapproved
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: system:open-cluster-management:{{ .SpokeClusterName }}
|
||||
`)
|
||||
|
||||
func pkgHubCsrManifestsClusterrolebindingYamlBytes() ([]byte, error) {
|
||||
return _pkgHubCsrManifestsClusterrolebindingYaml, nil
|
||||
}
|
||||
|
||||
func pkgHubCsrManifestsClusterrolebindingYaml() (*asset, error) {
|
||||
bytes, err := pkgHubCsrManifestsClusterrolebindingYamlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "pkg/hub/csr/manifests/clusterrolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.bytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// MustAsset is like Asset but panics when Asset would return an error.
|
||||
// It simplifies safe initialization of global variables.
|
||||
func MustAsset(name string) []byte {
|
||||
a, err := Asset(name)
|
||||
if err != nil {
|
||||
panic("asset: Asset(" + name + "): " + err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// AssetInfo loads and returns the asset info for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func AssetInfo(name string) (os.FileInfo, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"pkg/hub/csr/manifests/clusterrole.yaml": pkgHubCsrManifestsClusterroleYaml,
|
||||
"pkg/hub/csr/manifests/clusterrolebinding.yaml": pkgHubCsrManifestsClusterrolebindingYaml,
|
||||
}
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for childName := range node.Children {
|
||||
rv = append(rv, childName)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type bintree struct {
|
||||
Func func() (*asset, error)
|
||||
Children map[string]*bintree
|
||||
}
|
||||
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"pkg": {nil, map[string]*bintree{
|
||||
"hub": {nil, map[string]*bintree{
|
||||
"csr": {nil, map[string]*bintree{
|
||||
"manifests": {nil, map[string]*bintree{
|
||||
"clusterrole.yaml": {pkgHubCsrManifestsClusterroleYaml, map[string]*bintree{}},
|
||||
"clusterrolebinding.yaml": {pkgHubCsrManifestsClusterrolebindingYaml, map[string]*bintree{}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory
|
||||
func RestoreAsset(dir, name string) error {
|
||||
data, err := Asset(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := AssetInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreAssets restores an asset under the given directory recursively
|
||||
func RestoreAssets(dir, name string) error {
|
||||
children, err := AssetDir(name)
|
||||
// File
|
||||
if err != nil {
|
||||
return RestoreAsset(dir, name)
|
||||
}
|
||||
// Dir
|
||||
for _, child := range children {
|
||||
err = RestoreAssets(dir, filepath.Join(name, child))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _filePath(dir, name string) string {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package csr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/open-cluster-management/registration/pkg/hub/csr/bindata"
|
||||
"github.com/openshift/library-go/pkg/assets"
|
||||
"github.com/openshift/library-go/pkg/controller/factory"
|
||||
"github.com/openshift/library-go/pkg/operator/events"
|
||||
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
|
||||
operatorhelpers "github.com/openshift/library-go/pkg/operator/v1helpers"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
const (
|
||||
manifestDir = "pkg/hub/csr"
|
||||
subjectPrefix = "system:open-cluster-management:"
|
||||
)
|
||||
|
||||
// csrController reconciles instances of spoke cluster CertificateSigningRequests on the hub.
|
||||
type csrController struct {
|
||||
kubeClient kubernetes.Interface
|
||||
eventRecorder events.Recorder
|
||||
}
|
||||
|
||||
// NewCSRController creates a new csr controller
|
||||
func NewCSRController(kubeClient kubernetes.Interface, csrInformer factory.Informer, recorder events.Recorder) factory.Controller {
|
||||
c := &csrController{
|
||||
kubeClient: kubeClient,
|
||||
eventRecorder: recorder.WithComponentSuffix("csr-controller"),
|
||||
}
|
||||
return factory.New().
|
||||
WithInformersQueueKeyFunc(func(obj runtime.Object) string {
|
||||
accessor, _ := meta.Accessor(obj)
|
||||
return accessor.GetName()
|
||||
}, csrInformer).
|
||||
WithSync(c.sync).
|
||||
ToController("CSRController", recorder)
|
||||
}
|
||||
|
||||
func (c *csrController) sync(ctx context.Context, syncCtx factory.SyncContext) error {
|
||||
csrName := syncCtx.QueueKey()
|
||||
klog.V(4).Infof("Reconciling CertificateSigningRequests %q", csrName)
|
||||
csr, err := c.kubeClient.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Current csr is denied, do nothing.
|
||||
approved, denied := getCertApprovalCondition(&csr.Status)
|
||||
if denied {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recognize whether current csr is a spoker clsuter csr.
|
||||
recognized, spokeClusterName := recognize(csr)
|
||||
if !recognized {
|
||||
klog.V(4).Infof("CSR %q was not recognized", csr.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Current spoker clsuter csr is approved, apply the corresponding clusterrole/clusterrolebinding.
|
||||
if approved {
|
||||
resourceResults := resourceapply.ApplyDirectly(
|
||||
resourceapply.NewKubeClientHolder(c.kubeClient),
|
||||
syncCtx.Recorder(),
|
||||
func(name string) ([]byte, error) {
|
||||
config := struct {
|
||||
SpokeClusterName string
|
||||
}{
|
||||
SpokeClusterName: spokeClusterName,
|
||||
}
|
||||
return assets.MustCreateAssetFromTemplate(name, bindata.MustAsset(filepath.Join(manifestDir, name)), config).Data, nil
|
||||
},
|
||||
"manifests/clusterrole.yaml",
|
||||
"manifests/clusterrolebinding.yaml",
|
||||
)
|
||||
errs := []error{}
|
||||
for _, result := range resourceResults {
|
||||
if result.Error != nil {
|
||||
errs = append(errs, fmt.Errorf("%q (%T): %w", result.File, result.Type, result.Error))
|
||||
}
|
||||
}
|
||||
return operatorhelpers.NewMultiLineAggregate(errs)
|
||||
}
|
||||
|
||||
// Using SubjectAccessReview API to authorize whether the current spoke agent has been authorized to renew its csr.
|
||||
// A spoke agent is authorized after its cluster is accepted by hub cluster admin.
|
||||
allowed, err := c.authorize(ctx, csr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
klog.V(4).Infof("Spoke cluster csr %q cannont be auto approved due to subject access review was not approved", csr.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Auto approve the spoker clsuter csr
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1beta1.CertificateApproved,
|
||||
Reason: "AutoApproved",
|
||||
Message: "Auto approving spoke cluster agent certificate after SubjectAccessReview.",
|
||||
})
|
||||
_, err = c.kubeClient.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(ctx, csr, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.eventRecorder.Eventf("SpokeClusterCSRAutoApproved", "spoke cluster csr %q is auto approved by hub csr controller", csr.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *csrController) authorize(ctx context.Context, csr *certificatesv1beta1.CertificateSigningRequest) (bool, error) {
|
||||
extra := make(map[string]authorizationv1.ExtraValue)
|
||||
for k, v := range csr.Spec.Extra {
|
||||
extra[k] = authorizationv1.ExtraValue(v)
|
||||
}
|
||||
|
||||
sar := &authorizationv1.SubjectAccessReview{
|
||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||
User: csr.Spec.Username,
|
||||
UID: csr.Spec.UID,
|
||||
Groups: csr.Spec.Groups,
|
||||
Extra: extra,
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Group: "certificates.k8s.io",
|
||||
Resource: "certificatesigningrequests",
|
||||
Verb: "create",
|
||||
Subresource: "spokeclusteragent",
|
||||
},
|
||||
},
|
||||
}
|
||||
sar, err := c.kubeClient.AuthorizationV1().SubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return sar.Status.Allowed, nil
|
||||
}
|
||||
|
||||
// To recognize a valid spoke cluster csr, we check
|
||||
// 1. if organization field and commonName field in csr request is valid.
|
||||
// 2. if user name in csr is the same as commonName field in csr request.
|
||||
func recognize(csr *certificatesv1beta1.CertificateSigningRequest) (recognized bool, spokeClusterName string) {
|
||||
block, _ := pem.Decode(csr.Spec.Request)
|
||||
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
||||
klog.V(4).Infof("csr %q was not recognized: PEM block type is not CERTIFICATE REQUEST", csr.Name)
|
||||
return false, ""
|
||||
}
|
||||
|
||||
x509cr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||
if err != nil {
|
||||
klog.V(4).Infof("csr %q was not recognized: %w", csr.Name, err)
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if len(x509cr.Subject.Organization) != 1 {
|
||||
return false, ""
|
||||
}
|
||||
organization := x509cr.Subject.Organization[0]
|
||||
if !strings.HasPrefix(organization, subjectPrefix) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(x509cr.Subject.CommonName, organization) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return csr.Spec.Username == x509cr.Subject.CommonName, strings.TrimPrefix(organization, subjectPrefix)
|
||||
}
|
||||
|
||||
func getCertApprovalCondition(status *certificatesv1beta1.CertificateSigningRequestStatus) (approved, denied bool) {
|
||||
for _, c := range status.Conditions {
|
||||
if c.Type == certificatesv1beta1.CertificateApproved {
|
||||
approved = true
|
||||
}
|
||||
if c.Type == certificatesv1beta1.CertificateDenied {
|
||||
denied = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package csr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/openshift/library-go/pkg/operator/events"
|
||||
"github.com/openshift/library-go/pkg/operator/events/eventstesting"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
)
|
||||
|
||||
const testCSRName = "test_csr"
|
||||
|
||||
func TestSync(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
startingCSRs []runtime.Object
|
||||
autoApprovingAllowed bool
|
||||
validateActions func(t *testing.T, actions []clienttesting.Action)
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "sync a deleted csr",
|
||||
startingCSRs: []runtime.Object{},
|
||||
validateActions: func(t *testing.T, actions []clienttesting.Action) {
|
||||
if len(actions) != 1 {
|
||||
t.Errorf("expected 1 call but got: %#v", actions)
|
||||
}
|
||||
assertAction(t, actions[0], "get")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sync a denied csr",
|
||||
startingCSRs: []runtime.Object{newDeniedCSR()},
|
||||
validateActions: func(t *testing.T, actions []clienttesting.Action) {
|
||||
if len(actions) != 1 {
|
||||
t.Errorf("expected 1 call but got: %#v", actions)
|
||||
}
|
||||
assertAction(t, actions[0], "get")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sync an unrecognized csr",
|
||||
startingCSRs: []runtime.Object{newUnrecognizedCSR()},
|
||||
validateActions: func(t *testing.T, actions []clienttesting.Action) {
|
||||
if len(actions) != 1 {
|
||||
t.Errorf("expected 1 call but got: %#v", actions)
|
||||
}
|
||||
assertAction(t, actions[0], "get")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sync a hub cluster admin approved csr",
|
||||
startingCSRs: []runtime.Object{newApprovedCSR()},
|
||||
validateActions: func(t *testing.T, actions []clienttesting.Action) {
|
||||
if len(actions) != 5 {
|
||||
t.Errorf("expected 5 call but got: %#v", actions)
|
||||
}
|
||||
assertAction(t, actions[4], "create")
|
||||
assertClusterRoleBindingCreated(t, actions[4].(clienttesting.CreateActionImpl).Object)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deny an auto approving csr",
|
||||
startingCSRs: []runtime.Object{newRecognizedCSR()},
|
||||
validateActions: func(t *testing.T, actions []clienttesting.Action) {
|
||||
if len(actions) != 2 {
|
||||
t.Errorf("expected 2 call but got: %#v", actions)
|
||||
}
|
||||
assertAction(t, actions[1], "create")
|
||||
assertSubjectAccessReviewCreated(t, actions[1].(clienttesting.CreateActionImpl).Object)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow an auto approving csr",
|
||||
startingCSRs: []runtime.Object{newRecognizedCSR()},
|
||||
autoApprovingAllowed: true,
|
||||
validateActions: func(t *testing.T, actions []clienttesting.Action) {
|
||||
if len(actions) != 3 {
|
||||
t.Errorf("expected 3 call but got: %#v", actions)
|
||||
}
|
||||
assertAction(t, actions[2], "update")
|
||||
assertCondition(t, actions[2].(clienttesting.UpdateActionImpl).Object, certificatesv1beta1.CertificateApproved, "AutoApproved")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
kubeClient := kubefake.NewSimpleClientset(c.startingCSRs...)
|
||||
kubeClient.PrependReactor(
|
||||
"create",
|
||||
"subjectaccessreviews",
|
||||
func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, &authorizationv1.SubjectAccessReview{
|
||||
Status: authorizationv1.SubjectAccessReviewStatus{
|
||||
Allowed: c.autoApprovingAllowed,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
|
||||
ctrl := &csrController{kubeClient, eventstesting.NewTestingEventRecorder(t)}
|
||||
syncErr := ctrl.sync(context.TODO(), newFakeSyncContext(t))
|
||||
if len(c.expectedErr) > 0 && syncErr == nil {
|
||||
t.Errorf("expected %q error", c.expectedErr)
|
||||
return
|
||||
}
|
||||
if len(c.expectedErr) > 0 && syncErr != nil && syncErr.Error() != c.expectedErr {
|
||||
t.Errorf("expected %q error, got %q", c.expectedErr, syncErr.Error())
|
||||
return
|
||||
}
|
||||
if syncErr != nil {
|
||||
t.Errorf("unexpected err: %v", syncErr)
|
||||
}
|
||||
|
||||
c.validateActions(t, kubeClient.Actions())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecognize(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
csr *certificatesv1beta1.CertificateSigningRequest
|
||||
recognized bool
|
||||
expectedClusterName string
|
||||
}{
|
||||
{
|
||||
name: "a wrong block type",
|
||||
csr: newCSR("", []string{}, "", "RSA PRIVATE KEY"),
|
||||
recognized: false,
|
||||
},
|
||||
{
|
||||
name: "an empty organization",
|
||||
csr: newCSR("", []string{}, "", "CERTIFICATE REQUEST"),
|
||||
recognized: false,
|
||||
},
|
||||
{
|
||||
name: "an invalid organization",
|
||||
csr: newCSR("", []string{"test"}, "", "CERTIFICATE REQUEST"),
|
||||
recognized: false,
|
||||
},
|
||||
{
|
||||
name: "an invalid common name",
|
||||
csr: newCSR("", []string{"system:open-cluster-management:spokecluster1"}, "", "CERTIFICATE REQUEST"),
|
||||
recognized: false,
|
||||
},
|
||||
{
|
||||
name: "a recognied csr",
|
||||
csr: newRecognizedCSR(),
|
||||
recognized: true,
|
||||
expectedClusterName: "spokecluster1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
recognized, clusterName := recognize(c.csr)
|
||||
if recognized != c.recognized {
|
||||
t.Errorf("expected %t, but failed", c.recognized)
|
||||
}
|
||||
if clusterName != c.expectedClusterName {
|
||||
t.Errorf("expected %q, but got %q", c.expectedClusterName, clusterName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCSR(cn string, orgs []string, username string, reqBlockType string) *certificatesv1beta1.CertificateSigningRequest {
|
||||
insecureRand := rand.New(rand.NewSource(0))
|
||||
signerName := "tester"
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), insecureRand)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
csrb, err := x509.CreateCertificateRequest(insecureRand, &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: orgs,
|
||||
},
|
||||
DNSNames: []string{},
|
||||
EmailAddresses: []string{},
|
||||
IPAddresses: []net.IP{},
|
||||
}, pk)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &certificatesv1beta1.CertificateSigningRequest{
|
||||
Spec: certificatesv1beta1.CertificateSigningRequestSpec{
|
||||
Username: username,
|
||||
Usages: []certificatesv1beta1.KeyUsage{},
|
||||
SignerName: &signerName,
|
||||
Request: pem.EncodeToMemory(&pem.Block{Type: reqBlockType, Bytes: csrb}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newRecognizedCSR() *certificatesv1beta1.CertificateSigningRequest {
|
||||
csr := newCSR(
|
||||
"system:open-cluster-management:spokecluster1:spokeagent1",
|
||||
[]string{"system:open-cluster-management:spokecluster1"},
|
||||
"system:open-cluster-management:spokecluster1:spokeagent1",
|
||||
"CERTIFICATE REQUEST",
|
||||
)
|
||||
csr.Name = testCSRName
|
||||
return csr
|
||||
}
|
||||
|
||||
func newUnrecognizedCSR() *certificatesv1beta1.CertificateSigningRequest {
|
||||
csr := newCSR(
|
||||
"system:open-cluster-management:spokecluster1:spokeagent2",
|
||||
[]string{"system:open-cluster-management:spokecluster1"},
|
||||
"system:open-cluster-management:spokecluster1:spokeagent1",
|
||||
"CERTIFICATE REQUEST",
|
||||
)
|
||||
csr.Name = testCSRName
|
||||
return csr
|
||||
}
|
||||
|
||||
func newDeniedCSR() *certificatesv1beta1.CertificateSigningRequest {
|
||||
csr := newRecognizedCSR()
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1beta1.CertificateDenied,
|
||||
})
|
||||
return csr
|
||||
}
|
||||
|
||||
func newApprovedCSR() *certificatesv1beta1.CertificateSigningRequest {
|
||||
csr := newRecognizedCSR()
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1beta1.CertificateApproved,
|
||||
})
|
||||
return csr
|
||||
}
|
||||
|
||||
func assertAction(t *testing.T, actual clienttesting.Action, expected string) {
|
||||
if actual.GetVerb() != expected {
|
||||
t.Errorf("expected %s action but got: %#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertClusterRoleBindingCreated(t *testing.T, actual runtime.Object) {
|
||||
_, ok := actual.(*rbacv1.ClusterRoleBinding)
|
||||
if !ok {
|
||||
t.Errorf("expected clusterrolebinding created, but got: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSubjectAccessReviewCreated(t *testing.T, actual runtime.Object) {
|
||||
_, ok := actual.(*authorizationv1.SubjectAccessReview)
|
||||
if !ok {
|
||||
t.Errorf("expected subjectaccessreview created, but got: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCondition(t *testing.T, actual runtime.Object, expectedCondition certificatesv1beta1.RequestConditionType, expectedReason string) {
|
||||
csr := actual.(*certificatesv1beta1.CertificateSigningRequest)
|
||||
conditions := csr.Status.Conditions
|
||||
if len(conditions) != 1 {
|
||||
t.Errorf("expected 1 condition but got: %#v", conditions)
|
||||
}
|
||||
condition := conditions[0]
|
||||
if condition.Type != expectedCondition {
|
||||
t.Errorf("expected %s but got: %s", expectedCondition, condition.Type)
|
||||
}
|
||||
if condition.Reason != expectedReason {
|
||||
t.Errorf("expected %s but got: %s", expectedReason, condition.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeSyncContext struct {
|
||||
csrName string
|
||||
recorder events.Recorder
|
||||
}
|
||||
|
||||
func newFakeSyncContext(t *testing.T) *fakeSyncContext {
|
||||
return &fakeSyncContext{
|
||||
csrName: testCSRName,
|
||||
recorder: eventstesting.NewTestingEventRecorder(t),
|
||||
}
|
||||
}
|
||||
|
||||
func (f fakeSyncContext) Queue() workqueue.RateLimitingInterface { return nil }
|
||||
func (f fakeSyncContext) QueueKey() string { return f.csrName }
|
||||
func (f fakeSyncContext) Recorder() events.Recorder { return f.recorder }
|
||||
@@ -0,0 +1,3 @@
|
||||
// package csr contains the hub-side reconciler for the spoke cluster CertificateSigningRequests
|
||||
// resource.
|
||||
package csr
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}-csrapproved
|
||||
rules:
|
||||
# Allow spoke agent to create/get/list/watch certificatesigningrequests
|
||||
- apiGroups: ["certificates.k8s.io"]
|
||||
resources: ["certificatesigningrequests"]
|
||||
verbs: ["create", "get", "list", "watch"]
|
||||
# Allow spoke agent to create/get/list/watch spoke cluster
|
||||
- apiGroups: ["cluster.open-cluster-management.io"]
|
||||
resources: ["spokeclusters"]
|
||||
verbs: ["create", "get", "list", "watch"]
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}-csrapproved
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}-csrapproved
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: system:open-cluster-management:{{ .SpokeClusterName }}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
|
||||
clusterv1client "github.com/open-cluster-management/api/client/cluster/clientset/versioned"
|
||||
clusterv1informers "github.com/open-cluster-management/api/client/cluster/informers/externalversions"
|
||||
"github.com/open-cluster-management/registration/pkg/hub/csr"
|
||||
"github.com/open-cluster-management/registration/pkg/hub/spokecluster"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
)
|
||||
|
||||
// RunControllerManager starts the controllers on hub to manage spoke cluster registraiton.
|
||||
@@ -23,7 +25,9 @@ func RunControllerManager(ctx context.Context, controllerContext *controllercmd.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clusterInformers := clusterv1informers.NewSharedInformerFactory(clusterClient, 10*time.Minute)
|
||||
csrInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 10*time.Minute)
|
||||
|
||||
spokeClusterController := spokecluster.NewSpokeClusterController(
|
||||
kubeClient,
|
||||
@@ -32,9 +36,17 @@ func RunControllerManager(ctx context.Context, controllerContext *controllercmd.
|
||||
controllerContext.EventRecorder,
|
||||
)
|
||||
|
||||
csrController := csr.NewCSRController(
|
||||
kubeClient,
|
||||
csrInformers.Certificates().V1beta1().CertificateSigningRequests().Informer(),
|
||||
controllerContext.EventRecorder,
|
||||
)
|
||||
|
||||
go clusterInformers.Start(ctx.Done())
|
||||
go csrInformers.Start(ctx.Done())
|
||||
|
||||
go spokeClusterController.Run(ctx, 1)
|
||||
go csrController.Run(ctx, 1)
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
|
||||
@@ -96,7 +96,8 @@ roleRef:
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}`)
|
||||
name: system:open-cluster-management:{{ .SpokeClusterName }}
|
||||
`)
|
||||
|
||||
func pkgHubSpokeclusterManifestsSpokeclusterClusterrolebindingYamlBytes() ([]byte, error) {
|
||||
return _pkgHubSpokeclusterManifestsSpokeclusterClusterrolebindingYaml, nil
|
||||
@@ -116,7 +117,8 @@ func pkgHubSpokeclusterManifestsSpokeclusterClusterrolebindingYaml() (*asset, er
|
||||
var _pkgHubSpokeclusterManifestsSpokeclusterNamespaceYaml = []byte(`apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .SpokeClusterName }}`)
|
||||
name: {{ .SpokeClusterName }}
|
||||
`)
|
||||
|
||||
func pkgHubSpokeclusterManifestsSpokeclusterNamespaceYamlBytes() ([]byte, error) {
|
||||
return _pkgHubSpokeclusterManifestsSpokeclusterNamespaceYaml, nil
|
||||
|
||||
@@ -9,4 +9,4 @@ roleRef:
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: system:open-cluster-management:spokecluster:{{ .SpokeClusterName }}
|
||||
name: system:open-cluster-management:{{ .SpokeClusterName }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .SpokeClusterName }}
|
||||
name: {{ .SpokeClusterName }}
|
||||
|
||||
Reference in New Issue
Block a user