diff options
| author | KubeEdge Bot <48982446+kubeedge-bot@users.noreply.github.com> | 2023-07-01 02:31:14 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-01 02:31:14 +0800 |
| commit | 817cb6738af7a5bec3d3402303158dfb5b035cb9 (patch) | |
| tree | 1ba1f8f89c949416385a8bee956fa3882e2afd32 | |
| parent | Merge pull request #4809 from JiaweiGithub/feat/updateversion (diff) | |
| parent | reconcile rules (diff) | |
| download | kubeedge-817cb6738af7a5bec3d3402303158dfb5b035cb9.tar.gz | |
Merge pull request #4802 from vincentgoat/rbac-offline-sql
authentication and authorization on the edge side
73 files changed, 8379 insertions, 450 deletions
diff --git a/build/crds/policy/policy_v1alpha1_serviceaccountaccess.yaml b/build/crds/policy/policy_v1alpha1_serviceaccountaccess.yaml new file mode 100644 index 000000000..a6a2c27e0 --- /dev/null +++ b/build/crds/policy/policy_v1alpha1_serviceaccountaccess.yaml @@ -0,0 +1,501 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.2 + creationTimestamp: null + name: serviceaccountaccesses.policy.kubeedge.io +spec: + group: policy.kubeedge.io + names: + kind: ServiceAccountAccess + listKind: ServiceAccountAccessList + plural: serviceaccountaccesses + shortNames: + - saaccess + singular: serviceaccountaccess + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServiceAccountAccess is the Schema for the ServiceAccountAccess + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec represents the specification of rbac. + properties: + accessClusterRoleBinding: + description: AccessClusterRoleBinding represents rbac ClusterRoleBinding + plus detailed ClusterRole info. + items: + description: AccessClusterRoleBinding represents rbac ClusterRoleBinding + plus detailed ClusterRole info. + properties: + clusterRoleBinding: + description: ClusterRoleBinding represents rbac ClusterRoleBinding. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of + this representation of an object. Servers should convert + recognized schemas to the latest internal value, and may + reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST + resource this object represents. Servers may infer this + from the endpoint the client submits requests to. Cannot + be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + roleRef: + description: RoleRef can only reference a ClusterRole in + the global namespace. If the RoleRef cannot be resolved, + the Authorizer must return an error. + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - apiGroup + - kind + - name + type: object + subjects: + description: Subjects holds references to the objects the + role applies to. + items: + description: Subject contains a reference to the object + or user identities a role binding applies to. This + can either hold a direct API object reference, or a + value for non-objects such as user and group names. + properties: + apiGroup: + description: APIGroup holds the API group of the referenced + subject. Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User + and Group subjects. + type: string + kind: + description: Kind of object being referenced. Values + defined by this API group are "User", "Group", and + "ServiceAccount". If the Authorizer does not recognized + the kind value, the Authorizer should report an + error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: Namespace of the referenced object. If + the object kind is non-namespace, such as "User" + or "Group", and this value is not empty the Authorizer + should report an error. + type: string + required: + - kind + - name + type: object + type: array + required: + - roleRef + type: object + rules: + description: Rules contains role rules. + items: + description: PolicyRule holds information that describes a + policy rule, but does not contain information about who + the rule applies to or which namespace the rule applies + to. + properties: + apiGroups: + description: APIGroups is the name of the APIGroup that + contains the resources. If multiple API groups are + specified, any action requested against one of the enumerated + resources in any API group will be allowed. + items: + type: string + type: array + nonResourceURLs: + description: NonResourceURLs is a set of partial urls + that a user should have access to. *s are allowed, + but only as the full, final step in the path Since non-resource + URLs are not namespaced, this field is only applicable + for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" + or "secrets") or non-resource URL paths (such as "/api"), but + not both. + items: + type: string + type: array + resourceNames: + description: ResourceNames is an optional white list of + names that the rule applies to. An empty set means + that everything is allowed. + items: + type: string + type: array + resources: + description: Resources is a list of resources this rule + applies to. '*' represents all resources. + items: + type: string + type: array + verbs: + description: Verbs is a list of Verbs that apply to ALL + the ResourceKinds contained in this rule. '*' represents + all verbs. + items: + type: string + type: array + required: + - verbs + type: object + type: array + type: object + type: array + accessRoleBinding: + description: AccessRoleBinding represents rbac rolebinding plus detailed + role info. + items: + description: AccessRoleBinding represents rbac rolebinding plus + detailed role info. + properties: + roleBinding: + description: RoleBinding represents rbac rolebinding. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of + this representation of an object. Servers should convert + recognized schemas to the latest internal value, and may + reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST + resource this object represents. Servers may infer this + from the endpoint the client submits requests to. Cannot + be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + roleRef: + description: RoleRef can reference a Role in the current + namespace or a ClusterRole in the global namespace. If + the RoleRef cannot be resolved, the Authorizer must return + an error. + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - apiGroup + - kind + - name + type: object + subjects: + description: Subjects holds references to the objects the + role applies to. + items: + description: Subject contains a reference to the object + or user identities a role binding applies to. This + can either hold a direct API object reference, or a + value for non-objects such as user and group names. + properties: + apiGroup: + description: APIGroup holds the API group of the referenced + subject. Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User + and Group subjects. + type: string + kind: + description: Kind of object being referenced. Values + defined by this API group are "User", "Group", and + "ServiceAccount". If the Authorizer does not recognized + the kind value, the Authorizer should report an + error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: Namespace of the referenced object. If + the object kind is non-namespace, such as "User" + or "Group", and this value is not empty the Authorizer + should report an error. + type: string + required: + - kind + - name + type: object + type: array + required: + - roleRef + type: object + rules: + description: Rules contains role rules. + items: + description: PolicyRule holds information that describes a + policy rule, but does not contain information about who + the rule applies to or which namespace the rule applies + to. + properties: + apiGroups: + description: APIGroups is the name of the APIGroup that + contains the resources. If multiple API groups are + specified, any action requested against one of the enumerated + resources in any API group will be allowed. + items: + type: string + type: array + nonResourceURLs: + description: NonResourceURLs is a set of partial urls + that a user should have access to. *s are allowed, + but only as the full, final step in the path Since non-resource + URLs are not namespaced, this field is only applicable + for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" + or "secrets") or non-resource URL paths (such as "/api"), but + not both. + items: + type: string + type: array + resourceNames: + description: ResourceNames is an optional white list of + names that the rule applies to. An empty set means + that everything is allowed. + items: + type: string + type: array + resources: + description: Resources is a list of resources this rule + applies to. '*' represents all resources. + items: + type: string + type: array + verbs: + description: Verbs is a list of Verbs that apply to ALL + the ResourceKinds contained in this rule. '*' represents + all verbs. + items: + type: string + type: array + required: + - verbs + type: object + type: array + type: object + type: array + serviceAccount: + description: ServiceAccount is one-to-one corresponding relations + with the serviceaccountaccess. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this + representation of an object. Servers should convert recognized + schemas to the latest internal value, and may reject unrecognized + values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + automountServiceAccountToken: + description: AutomountServiceAccountToken indicates whether pods + running as this service account should have an API token automatically + mounted. Can be overridden at the pod level. + type: boolean + imagePullSecrets: + description: 'ImagePullSecrets is a list of references to secrets + in the same namespace to use for pulling any images in pods + that reference this ServiceAccount. ImagePullSecrets are distinct + from Secrets because Secrets can be mounted in the pod, but + ImagePullSecrets are only accessed by the kubelet. More info: + https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod' + items: + description: LocalObjectReference contains enough information + to let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: array + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + secrets: + description: 'Secrets is the list of secrets allowed to be used + by pods running using this ServiceAccount. More info: https://kubernetes.io/docs/concepts/configuration/secret' + items: + description: 'ObjectReference contains enough information to + let you inspect or modify the referred object. --- New uses + of this type are discouraged because of difficulty describing + its usage when embedded in APIs. 1. Ignored fields. It includes + many fields which are not generally honored. For instance, + ResourceVersion and FieldPath are both very rarely valid in + actual usage. 2. Invalid usage help. It is impossible to + add specific help for individual usage. In most embedded + usages, there are particular restrictions like, "must + refer only to types A and B" or "UID not honored" or "name + must be restricted". Those cannot be well described when + embedded. 3. Inconsistent validation. Because the usages + are different, the validation rules are different by usage, + which makes it hard for users to predict what will happen. 4. + The fields are both imprecise and overly precise. Kind is + not a precise mapping to a URL. This can produce ambiguity during + interpretation and require a REST mapping. In most cases, + the dependency is on the group,resource tuple and the + version of the actual struct is irrelevant. 5. We cannot + easily change it. Because this type is embedded in many locations, + updates to this type will affect numerous schemas. Don''t + make new APIs embed an underspecified API type they do not + control. Instead of using this type, create a locally provided + and used type that is well-focused on your reference. For + example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + .' + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this pod). + This syntax is chosen only to have some well-defined way + of referencing a part of an object. TODO: this design + is not final and this field is subject to change in the + future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + type: array + type: object + serviceAccountUid: + description: ServiceAccountUID is the uid of serviceaccount. + type: string + type: object + status: + description: Status represents the node list which store the rules. + properties: + nodeList: + description: NodeList represents the node name which store the rules. + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/cloud/cmd/cloudcore/app/server.go b/cloud/cmd/cloudcore/app/server.go index 68da4ae06..e42f6cf3b 100644 --- a/cloud/cmd/cloudcore/app/server.go +++ b/cloud/cmd/cloudcore/app/server.go @@ -49,6 +49,7 @@ import ( "github.com/kubeedge/kubeedge/cloud/pkg/dynamiccontroller" "github.com/kubeedge/kubeedge/cloud/pkg/edgecontroller" "github.com/kubeedge/kubeedge/cloud/pkg/nodeupgradejobcontroller" + "github.com/kubeedge/kubeedge/cloud/pkg/policycontroller" "github.com/kubeedge/kubeedge/cloud/pkg/router" "github.com/kubeedge/kubeedge/cloud/pkg/synccontroller" "github.com/kubeedge/kubeedge/common/constants" @@ -162,6 +163,7 @@ func registerModules(c *v1alpha1.CloudCoreConfig) { cloudstream.Register(c.Modules.CloudStream, c.CommonConfig) router.Register(c.Modules.Router) dynamiccontroller.Register(c.Modules.DynamicController) + policycontroller.Register(client.CrdConfig) } func NegotiateTunnelPort() (*int, error) { diff --git a/cloud/pkg/cloudhub/dispatcher/message_dispatcher.go b/cloud/pkg/cloudhub/dispatcher/message_dispatcher.go index 1f63c975f..408f05afe 100644 --- a/cloud/pkg/cloudhub/dispatcher/message_dispatcher.go +++ b/cloud/pkg/cloudhub/dispatcher/message_dispatcher.go @@ -175,6 +175,9 @@ func (md *messageDispatcher) DispatchUpstream(message *beehivemodel.Message, inf klog.Errorf("node %s receive message ack err: %v", info.NodeID, err) } + case message.GetOperation() == beehivemodel.ResponseErrorOperation: + klog.Errorf("node %s receive message %s error response: %v", info.NodeID, message.GetID(), message.GetContent()) + case message.GetOperation() == beehivemodel.UploadOperation && message.GetGroup() == modules.UserGroup: message.Router.Resource = fmt.Sprintf("node/%s/%s", info.NodeID, message.Router.Resource) beehivecontext.Send(modules.RouterModuleName, *message) @@ -219,7 +222,7 @@ func (md *messageDispatcher) enqueueAckMessage(nodeID string, msg *beehivemodel. messageKey, err := common.AckMessageKeyFunc(msg) if err != nil { - klog.Errorf("fail to get key for message: %s", msg.String()) + klog.Errorf("fail to get key for message: %s, err: %v", msg.String(), err) return } diff --git a/cloud/pkg/common/client/client.go b/cloud/pkg/common/client/client.go index a3daba4d5..4fc9e32bf 100644 --- a/cloud/pkg/common/client/client.go +++ b/cloud/pkg/common/client/client.go @@ -43,6 +43,7 @@ var ( authKubeConfig *rest.Config KubeConfig *rest.Config + CrdConfig *rest.Config ) func InitKubeEdgeClient(config *cloudcoreConfig.KubeAPIConfig) { @@ -64,6 +65,7 @@ func InitKubeEdgeClient(config *cloudcoreConfig.KubeAPIConfig) { crdKubeConfig := rest.CopyConfig(kubeConfig) crdKubeConfig.ContentType = runtime.ContentTypeJSON + CrdConfig = crdKubeConfig crdClient = crdClientset.NewForConfigOrDie(crdKubeConfig) authKubeConfig, err = clientcmd.BuildConfigFromFlags(kubeConfig.Host, "") diff --git a/cloud/pkg/common/messagelayer/context.go b/cloud/pkg/common/messagelayer/context.go index 21cc57009..7aeb2afdc 100644 --- a/cloud/pkg/common/messagelayer/context.go +++ b/cloud/pkg/common/messagelayer/context.go @@ -103,3 +103,11 @@ func NodeUpgradeJobControllerMessageLayer() MessageLayer { ResponseModuleName: modules.CloudHubModuleName, } } + +func PolicyControllerMessageLayer() MessageLayer { + return &ContextMessageLayer{ + SendModuleName: modules.CloudHubModuleName, + ReceiveModuleName: modules.PolicyControllerModuleName, + ResponseModuleName: modules.CloudHubModuleName, + } +} diff --git a/cloud/pkg/common/modules/modules.go b/cloud/pkg/common/modules/modules.go index 8d9a471e3..76f748251 100644 --- a/cloud/pkg/common/modules/modules.go +++ b/cloud/pkg/common/modules/modules.go @@ -7,6 +7,9 @@ const ( EdgeControllerModuleName = "edgecontroller" EdgeControllerGroupName = "edgecontroller" + PolicyControllerModuleName = "policycontroller" + PolicyControllerGroupName = "policycontroller" + DeviceControllerModuleName = "devicecontroller" DeviceControllerModuleGroup = "devicecontroller" diff --git a/cloud/pkg/dynamiccontroller/application/application.go b/cloud/pkg/dynamiccontroller/application/application.go index 08deca76e..a13882702 100644 --- a/cloud/pkg/dynamiccontroller/application/application.go +++ b/cloud/pkg/dynamiccontroller/application/application.go @@ -5,16 +5,12 @@ import ( "fmt" "strings" - authorizationv1 "k8s.io/api/authorization/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" - "k8s.io/client-go/rest" "k8s.io/klog/v2" "github.com/kubeedge/beehive/pkg/core/model" @@ -23,20 +19,19 @@ import ( "github.com/kubeedge/kubeedge/cloud/pkg/common/modules" "github.com/kubeedge/kubeedge/cloud/pkg/dynamiccontroller/filter" "github.com/kubeedge/kubeedge/edge/pkg/common/message" - kefeatures "github.com/kubeedge/kubeedge/pkg/features" "github.com/kubeedge/kubeedge/pkg/metaserver" ) type Center struct { HandlerCenter messageLayer messagelayer.MessageLayer - authConfig *rest.Config + kubeclient dynamic.Interface } func NewApplicationCenter(dynamicSharedInformerFactory dynamicinformer.DynamicSharedInformerFactory) *Center { a := &Center{ HandlerCenter: NewHandlerCenter(dynamicSharedInformerFactory), - authConfig: client.GetAuthConfig(), + kubeclient: client.GetDynamicClient(), messageLayer: messagelayer.DynamicControllerMessageLayer(), } return a @@ -70,72 +65,6 @@ func (c *Center) Process(msg model.Message) { klog.Infof("[metaserver/applicationCenter]successfully to process Application(%+v)", app) } -func (c *Center) generateNewConfig(raw string) (*rest.Config, error) { - parts := strings.SplitN(raw, " ", 3) - if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" || len(parts[1]) <= 0 { - return nil, fmt.Errorf("invalid request token format or length: %v", len(parts)) - } - authConfig := rest.CopyConfig(c.authConfig) - authConfig.BearerToken = parts[1] - return authConfig, nil -} - -func (c *Center) createAuthClient(app *metaserver.Application) (authorizationv1client.AuthorizationV1Interface, error) { - authConfig, err := c.generateNewConfig(app.Token) - if err != nil { - return nil, err - } - return authorizationv1client.NewForConfigOrDie(authConfig), nil -} - -func (c *Center) createKubeClient(app *metaserver.Application) (dynamic.Interface, error) { - if !kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) { - return client.GetDynamicClient(), nil - } - authConfig, err := c.generateNewConfig(app.Token) - if err != nil { - return nil, err - } - return dynamic.NewForConfigOrDie(authConfig), nil -} - -func (c *Center) authorizeApplication(app *metaserver.Application, gvr schema.GroupVersionResource, namespace string, name string) error { - if !kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) { - return nil - } - tmpAuthClient, err := c.createAuthClient(app) - if err != nil { - return err - } - sar := &authorizationv1.SelfSubjectAccessReview{ - Spec: authorizationv1.SelfSubjectAccessReviewSpec{ - ResourceAttributes: &authorizationv1.ResourceAttributes{ - Namespace: namespace, - Verb: string(app.Verb), - Group: gvr.Group, - Resource: gvr.Resource, - Name: name, - Subresource: app.Subresource, - }, - }, - } - response, err := tmpAuthClient.SelfSubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{}) - if err != nil { - return err - } - if response.Status.Allowed { - return nil - } - var errMsg = fmt.Sprintf("resource %v authorize failed.", gvr) - if len(response.Status.Reason) > 0 { - errMsg += fmt.Sprintf("reason: %v.", response.Status.Reason) - } - if len(response.Status.EvaluationError) > 0 { - errMsg += fmt.Sprintf("evaluation error: %v.", response.Status.EvaluationError) - } - return fmt.Errorf(errMsg) -} - // ProcessApplication processes application by re-translating it to kube-api request with kube client, // which will be processed and responded by apiserver eventually. // Specially if app.verb == watch, it transforms app to a listener and register it to HandlerCenter, rather @@ -144,21 +73,6 @@ func (c *Center) authorizeApplication(app *metaserver.Application, gvr schema.Gr func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, error) { app.Status = metaserver.InProcessing gvr, ns, name := metaserver.ParseKey(app.Key) - var kubeClient dynamic.Interface - var err error - if app.Verb != metaserver.Watch { - kubeClient, err = c.createKubeClient(app) - if err != nil { - klog.Errorf("create kube client error: %v", err) - return nil, err - } - } else { - err := c.authorizeApplication(app, gvr, ns, name) - if err != nil { - klog.Errorf("authorize application error: %v", err) - return nil, err - } - } switch app.Verb { case metaserver.List: @@ -166,7 +80,7 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e if err := app.OptionTo(option); err != nil { return nil, err } - list, err := kubeClient.Resource(gvr).Namespace(ns).List(context.TODO(), *option) + list, err := c.kubeclient.Resource(gvr).Namespace(ns).List(context.TODO(), *option) if err != nil { return nil, fmt.Errorf("get current list error: %v", err) } @@ -186,7 +100,7 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e if err := app.OptionTo(option); err != nil { return nil, err } - retObj, err := kubeClient.Resource(gvr).Namespace(ns).Get(context.TODO(), name, *option) + retObj, err := c.kubeclient.Resource(gvr).Namespace(ns).Get(context.TODO(), name, *option) if err != nil { return nil, err } @@ -203,9 +117,9 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e var retObj interface{} var err error if app.Subresource == "" { - retObj, err = kubeClient.Resource(gvr).Namespace(ns).Create(context.TODO(), obj, *option) + retObj, err = c.kubeclient.Resource(gvr).Namespace(ns).Create(context.TODO(), obj, *option) } else { - retObj, err = kubeClient.Resource(gvr).Namespace(ns).Create(context.TODO(), obj, *option, app.Subresource) + retObj, err = c.kubeclient.Resource(gvr).Namespace(ns).Create(context.TODO(), obj, *option, app.Subresource) } if err != nil { return nil, err @@ -216,7 +130,7 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e if err := app.OptionTo(&option); err != nil { return nil, err } - if err := kubeClient.Resource(gvr).Namespace(ns).Delete(context.TODO(), name, *option); err != nil { + if err := c.kubeclient.Resource(gvr).Namespace(ns).Delete(context.TODO(), name, *option); err != nil { return nil, err } return nil, nil @@ -232,9 +146,9 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e var retObj interface{} var err error if app.Subresource == "" { - retObj, err = kubeClient.Resource(gvr).Namespace(ns).Update(context.TODO(), obj, *option) + retObj, err = c.kubeclient.Resource(gvr).Namespace(ns).Update(context.TODO(), obj, *option) } else { - retObj, err = kubeClient.Resource(gvr).Namespace(ns).Update(context.TODO(), obj, *option, app.Subresource) + retObj, err = c.kubeclient.Resource(gvr).Namespace(ns).Update(context.TODO(), obj, *option, app.Subresource) } if err != nil { return nil, err @@ -249,7 +163,7 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e if err := app.ReqBodyTo(obj); err != nil { return nil, err } - retObj, err := kubeClient.Resource(gvr).Namespace(ns).UpdateStatus(context.TODO(), obj, *option) + retObj, err := c.kubeclient.Resource(gvr).Namespace(ns).UpdateStatus(context.TODO(), obj, *option) if err != nil { return nil, err } @@ -259,7 +173,7 @@ func (c *Center) ProcessApplication(app *metaserver.Application) (interface{}, e if err := app.OptionTo(pi); err != nil { return nil, err } - retObj, err := kubeClient.Resource(gvr).Namespace(ns).Patch(context.TODO(), pi.Name, pi.PatchType, pi.Data, pi.Options, pi.Subresources...) + retObj, err := c.kubeclient.Resource(gvr).Namespace(ns).Patch(context.TODO(), pi.Name, pi.PatchType, pi.Data, pi.Options, pi.Subresources...) if err != nil { return nil, err } @@ -377,13 +291,6 @@ func (c *Center) getWatchDiff(allWatchAppInEdge map[string]metaserver.Applicatio func (c *Center) processWatchApp(watchApp *metaserver.Application) error { watchApp.Status = metaserver.InProcessing - gvr, ns, name := metaserver.ParseKey(watchApp.Key) - - err := c.authorizeApplication(watchApp, gvr, ns, name) - if err != nil { - return fmt.Errorf("authorize application error: %v", err) - } - listener, err := applicationToListener(watchApp) if err != nil { return err diff --git a/cloud/pkg/edgecontroller/controller/upstream.go b/cloud/pkg/edgecontroller/controller/upstream.go index 07a133741..338117bcc 100644 --- a/cloud/pkg/edgecontroller/controller/upstream.go +++ b/cloud/pkg/edgecontroller/controller/upstream.go @@ -90,12 +90,6 @@ func SortInitContainerStatuses(p *v1.Pod, statuses []v1.ContainerStatus) { } } -// ObjectResp is the object that api-server response -type ObjectResp struct { - Object metaV1.Object - Err error -} - // UpstreamController subscribe messages from edge and sync to k8s api server type UpstreamController struct { kubeClient kubernetes.Interface @@ -848,7 +842,7 @@ func (uc *UpstreamController) registerNode() { } resMsg := model.NewMessage(msg.GetID()). - FillBody(&ObjectResp{Object: resp, Err: err}). + FillBody(&edgeapi.ObjectResp{Object: resp, Err: err}). BuildRouter(modules.EdgeControllerModuleName, constants.GroupResource, msg.GetResource(), model.ResponseOperation) if err = uc.messageLayer.Response(*resMsg); err != nil { klog.Warningf("Response message: %s failed, response failed with error: %v", msg.GetID(), err) @@ -893,7 +887,7 @@ func (uc *UpstreamController) patchNode() { resMsg := model.NewMessage(msg.GetID()). SetResourceVersion(node.ResourceVersion). - FillBody(&ObjectResp{Object: node, Err: err}). + FillBody(&edgeapi.ObjectResp{Object: node, Err: err}). BuildRouter(modules.EdgeControllerModuleName, constants.GroupResource, msg.GetResource(), model.ResponseOperation) if err = uc.messageLayer.Response(*resMsg); err != nil { klog.Warningf("Message: %s process failure, response failed with error: %v", msg.GetID(), err) @@ -1036,7 +1030,7 @@ func (uc *UpstreamController) patchPod() { resMsg := model.NewMessage(msg.GetID()). SetResourceVersion(updatedPod.ResourceVersion). - FillBody(&ObjectResp{Object: updatedPod, Err: err}). + FillBody(&edgeapi.ObjectResp{Object: updatedPod, Err: err}). BuildRouter(modules.EdgeControllerModuleName, constants.GroupResource, msg.GetResource(), model.ResponseOperation) if err = uc.messageLayer.Response(*resMsg); err != nil { klog.Errorf("Message: %s process failure, response failed with error: %v", msg.GetID(), err) @@ -1163,7 +1157,7 @@ func (uc *UpstreamController) createOrUpdateLease() { } resMsg := model.NewMessage(msg.GetID()). - FillBody(&ObjectResp{Object: resp, Err: err}). + FillBody(&edgeapi.ObjectResp{Object: resp, Err: err}). BuildRouter(modules.EdgeControllerModuleName, constants.GroupResource, msg.GetResource(), model.ResponseOperation) if err = uc.messageLayer.Response(*resMsg); err != nil { klog.Warningf("Response message: %s failed, response failed with error: %v", msg.GetID(), err) @@ -1179,7 +1173,7 @@ func (uc *UpstreamController) createOrUpdateLease() { } resMsg := model.NewMessage(msg.GetID()). - FillBody(&ObjectResp{Object: resp, Err: err}). + FillBody(&edgeapi.ObjectResp{Object: resp, Err: err}). BuildRouter(modules.EdgeControllerModuleName, constants.GroupResource, msg.GetResource(), model.ResponseOperation) if err = uc.messageLayer.Response(*resMsg); err != nil { klog.Warningf("Response message: %s failed, response failed with error: %v", msg.GetID(), err) @@ -1220,7 +1214,7 @@ func (uc *UpstreamController) queryLease() { } resMsg := model.NewMessage(msg.GetID()). - FillBody(&ObjectResp{Object: object, Err: err}). + FillBody(&edgeapi.ObjectResp{Object: object, Err: err}). BuildRouter(modules.EdgeControllerModuleName, constants.GroupResource, msg.GetResource(), model.ResponseOperation) if err = uc.messageLayer.Response(*resMsg); err != nil { klog.Warningf("Response message: %s failed, response failed with error: %v", msg.GetID(), err) diff --git a/cloud/pkg/policycontroller/manager/reconcile.go b/cloud/pkg/policycontroller/manager/reconcile.go new file mode 100644 index 000000000..c0610a184 --- /dev/null +++ b/cloud/pkg/policycontroller/manager/reconcile.go @@ -0,0 +1,620 @@ +package controller + +import ( + "context" + "fmt" + "reflect" + "sort" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/klog/v2" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/kubeedge/beehive/pkg/core/model" + "github.com/kubeedge/kubeedge/cloud/pkg/common/messagelayer" + "github.com/kubeedge/kubeedge/cloud/pkg/common/modules" + "github.com/kubeedge/kubeedge/cloud/pkg/edgecontroller/constants" + commonconstants "github.com/kubeedge/kubeedge/common/constants" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" +) + +type Controller struct { + client.Client + MessageLayer messagelayer.MessageLayer +} + +func (c *Controller) Reconcile(ctx context.Context, request controllerruntime.Request) (controllerruntime.Result, error) { + acc := &policyv1alpha1.ServiceAccountAccess{} + if err := c.Client.Get(ctx, request.NamespacedName, acc); err != nil { + if apierrors.IsNotFound(err) { + return controllerruntime.Result{}, nil + } + klog.Errorf("failed to get serviceaccountaccess %s/%s, %v", request.Namespace, request.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + if !acc.GetDeletionTimestamp().IsZero() { + return controllerruntime.Result{}, nil + } + return c.syncRules(ctx, acc) +} + +func (c *Controller) filterResource(ctx context.Context, object client.Object) bool { + var p = &PolicyMatcher{} + matchTarget(ctx, c.Client, object, p.isMatchServiceAccount) + klog.V(4).Infof("filter resource %s/%s, %v", object.GetNamespace(), object.GetName(), p.match) + return p.match +} + +func isMatchedRoleRef(roleRef rbacv1.RoleRef, bindingNamespace string, object client.Object) bool { + if reflect.TypeOf(object).Elem().Name() != roleRef.Kind { + return false + } + if roleRef.Kind == "ClusterRole" { + return object.GetName() == roleRef.Name + } else if roleRef.Kind == "Role" { + return object.GetName() == roleRef.Name && bindingNamespace == object.GetNamespace() + } + return false +} + +type PolicyMatcher struct { + match bool +} + +func (pm *PolicyMatcher) isMatchServiceAccount(acc *policyv1alpha1.ServiceAccountAccess) bool { + pm.match = true + return false +} + +type PolicyRequestVisitor struct { + AuthPolicy []controllerruntime.Request +} + +func (p *PolicyRequestVisitor) matchRuntimeRequest(acc *policyv1alpha1.ServiceAccountAccess) bool { + p.AuthPolicy = append(p.AuthPolicy, controllerruntime.Request{NamespacedName: client.ObjectKey{Namespace: acc.Namespace, Name: acc.Name}}) + return true +} + +func matchTarget(ctx context.Context, cli client.Client, object client.Object, visitor func(*policyv1alpha1.ServiceAccountAccess) bool) { + accList := &policyv1alpha1.ServiceAccountAccessList{} + if err := cli.List(ctx, accList); err != nil { + klog.Errorf("failed to list serviceaccountaccess, %v", err) + return + } + + crbl := &rbacv1.ClusterRoleBindingList{} + if err := cli.List(ctx, crbl); err != nil { + klog.Errorf("failed to list clusterrolebindings, %v", err) + return + } + + for _, am := range accList.Items { + userInfo := serviceaccount.UserInfo(am.Spec.ServiceAccount.Namespace, am.Spec.ServiceAccount.Name, string(am.Spec.ServiceAccount.UID)) + switch obj := object.(type) { + case *rbacv1.ClusterRoleBinding: + _, applies := appliesTo(userInfo, obj.Subjects, "") + if applies && !visitor(&am) { + return + } + case *rbacv1.RoleBinding: + _, applies := appliesTo(userInfo, obj.Subjects, obj.Namespace) + if applies && !visitor(&am) { + return + } + case *rbacv1.ClusterRole: + for _, crb := range crbl.Items { + if !isMatchedRoleRef(crb.RoleRef, "", obj) { + continue + } + _, applies := appliesTo(userInfo, crb.Subjects, "") + if applies && !visitor(&am) { + return + } + } + var roleBindingList = &rbacv1.RoleBindingList{} + if err := cli.List(ctx, roleBindingList, &client.ListOptions{Namespace: am.Spec.ServiceAccount.Namespace}); err != nil { + klog.Errorf("failed to list rolebindings, %v", err) + return + } + for _, rb := range roleBindingList.Items { + if !isMatchedRoleRef(rb.RoleRef, rb.Namespace, obj) { + continue + } + _, applies := appliesTo(userInfo, rb.Subjects, rb.Namespace) + if applies && !visitor(&am) { + return + } + } + case *rbacv1.Role: + var roleBindingList = &rbacv1.RoleBindingList{} + if err := cli.List(ctx, roleBindingList, &client.ListOptions{Namespace: am.Spec.ServiceAccount.Namespace}); err != nil { + klog.Errorf("failed to list rolebindings, %v", err) + return + } + for _, rb := range roleBindingList.Items { + if !isMatchedRoleRef(rb.RoleRef, rb.Namespace, obj) { + continue + } + _, applies := appliesTo(userInfo, rb.Subjects, rb.Namespace) + if applies && !visitor(&am) { + return + } + } + } + } +} + +func (c *Controller) mapRolesFunc(object client.Object) []controllerruntime.Request { + var p = PolicyRequestVisitor{} + matchTarget(context.Background(), c.Client, object, p.matchRuntimeRequest) + klog.V(4).Infof("filter resource %s/%s, %v", object.GetNamespace(), object.GetName(), p.AuthPolicy) + return p.AuthPolicy +} + +func newSaAccessObject(sa corev1.ServiceAccount) *policyv1alpha1.ServiceAccountAccess { + return &policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: sa.GetName(), + Namespace: sa.GetNamespace(), + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa, + }, + } +} + +func (c *Controller) mapObjectFunc(object client.Object) []controllerruntime.Request { + accList := &policyv1alpha1.ServiceAccountAccessList{} + if err := c.Client.List(context.Background(), accList, &client.ListOptions{Namespace: object.GetNamespace()}); err != nil { + klog.Errorf("failed to list serviceaccountaccess, %v", err) + return nil + } + switch obj := object.(type) { + case *corev1.Pod: + sa := obj.Spec.ServiceAccountName + for _, am := range accList.Items { + if am.Spec.ServiceAccount.Name == sa && am.Spec.ServiceAccount.Namespace == object.GetNamespace() { + if obj.GetDeletionTimestamp() == nil { + // won't reconcile pod update event + return []controllerruntime.Request{} + } + klog.V(4).Infof("reconcile pod deleting %s/%s", obj.Namespace, obj.Name) + return []controllerruntime.Request{{NamespacedName: client.ObjectKey{Namespace: am.Namespace, Name: am.Name}}} + } + } + // already deleted serviceaccountaccess + if obj.GetDeletionTimestamp() != nil { + return []controllerruntime.Request{} + } + // create serviceaccountaccess if not exist when pod event triggered + newSaa := newSaAccessObject(corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: sa, + Namespace: object.GetNamespace(), + }, + }) + klog.V(4).Infof("create serviceaccountaccess %s/%s for pod %s/%s", newSaa.Namespace, newSaa.Name, obj.Namespace, obj.Name) + if err := c.Client.Create(context.Background(), newSaa); err != nil { + klog.Errorf("failed to create serviceaccountaccess, %v", err) + return nil + } + // return empty request to avoid reconcile conflict with serviceaccountaccess resource + return []controllerruntime.Request{} + case *corev1.ServiceAccount: + return []controllerruntime.Request{{NamespacedName: client.ObjectKey{Namespace: object.GetNamespace(), Name: object.GetName()}}} + } + + return []controllerruntime.Request{} +} + +func (c *Controller) filterObject(ctx context.Context, object client.Object) bool { + switch obj := object.(type) { + case *corev1.Pod: + node := obj.Spec.NodeName + if obj.Spec.ServiceAccountName == "" || node == "" || !isEdgeNode(ctx, c.Client, node) { + return false + } + return true + case *corev1.ServiceAccount: + accList := &policyv1alpha1.ServiceAccountAccessList{} + if err := c.Client.List(ctx, accList, &client.ListOptions{Namespace: object.GetNamespace()}); err != nil { + klog.Errorf("failed to list serviceaccountaccess, %v", err) + return false + } + for _, am := range accList.Items { + if am.Spec.ServiceAccount.Name == object.GetName() && am.Spec.ServiceAccount.Namespace == object.GetNamespace() { + return true + } + } + } + return false +} + +// SetupWithManager creates a controller and register to controller manager. +func (c *Controller) SetupWithManager(ctx context.Context, mgr controllerruntime.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.Pod{}, "spec.serviceAccountName", func(o client.Object) []string { + pod := o.(*corev1.Pod) + return []string{pod.Spec.ServiceAccountName} + }); err != nil { + return fmt.Errorf("failed to set ServiceAccountName field selector for manager, %v", err) + } + return controllerruntime.NewControllerManagedBy(mgr). + For(&policyv1alpha1.ServiceAccountAccess{}). + Watches(&source.Kind{Type: &rbacv1.ClusterRoleBinding{}}, handler.EnqueueRequestsFromMapFunc(c.mapRolesFunc), builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return c.filterResource(ctx, object) + }))). + Watches(&source.Kind{Type: &rbacv1.RoleBinding{}}, handler.EnqueueRequestsFromMapFunc(c.mapRolesFunc), builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return c.filterResource(ctx, object) + }))). + Watches(&source.Kind{Type: &rbacv1.ClusterRole{}}, handler.EnqueueRequestsFromMapFunc(c.mapRolesFunc), builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return c.filterResource(ctx, object) + }))). + Watches(&source.Kind{Type: &rbacv1.Role{}}, handler.EnqueueRequestsFromMapFunc(c.mapRolesFunc), builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return c.filterResource(ctx, object) + }))). + Watches(&source.Kind{Type: &corev1.ServiceAccount{}}, handler.EnqueueRequestsFromMapFunc(c.mapObjectFunc), builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return c.filterObject(ctx, object) + }))). + Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(c.mapObjectFunc), builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return c.filterObject(ctx, object) + }))). + Complete(c) +} + +func isEdgeNode(ctx context.Context, cli client.Client, name string) bool { + set := labels.Set{commonconstants.EdgeNodeRoleKey: commonconstants.EdgeNodeRoleValue} + selector := labels.SelectorFromSet(set) + var edgeNodeList = &corev1.NodeList{} + if err := cli.List(ctx, edgeNodeList, &client.ListOptions{LabelSelector: selector}); err != nil { + klog.Errorf("failed to list edge nodes, %v", err) + return false + } + for _, node := range edgeNodeList.Items { + if node.Name == name { + return true + } + } + return false +} + +func getNodeListOfServiceAccountAccess(ctx context.Context, cli client.Client, acc *policyv1alpha1.ServiceAccountAccess) ([]string, error) { + var nodeList []string + podList := &corev1.PodList{} + saNameSelector := fields.OneTermEqualSelector("spec.serviceAccountName", acc.Spec.ServiceAccount.Name) + if err := cli.List(ctx, podList, client.MatchingFieldsSelector{Selector: saNameSelector}, &client.ListOptions{Namespace: acc.Namespace}); err != nil { + klog.Errorf("failed to list pods through field selector serviceAccountName, %v", err) + return nil, err + } + + var nodeMap = make(map[string]bool) + for _, pod := range podList.Items { + if pod.Spec.NodeName == "" { + continue + } + if nodeMap[pod.Spec.NodeName] { + continue + } + if !isEdgeNode(ctx, cli, pod.Spec.NodeName) { + continue + } + nodeMap[pod.Spec.NodeName] = true + nodeList = append(nodeList, pod.Spec.NodeName) + } + return nodeList, nil +} + +func intersectSlice(old, new []string) []string { + var intersect = []string{} + var oldMap = make(map[string]bool) + for _, oldItem := range old { + oldMap[oldItem] = true + } + for _, newItem := range new { + if oldMap[newItem] { + intersect = append(intersect, newItem) + } + } + return intersect +} + +func subtractSlice(source, subTarget []string) []string { + var subtract = []string{} + var oldMap = make(map[string]bool) + for _, oldItem := range source { + oldMap[oldItem] = true + } + for _, newItem := range subTarget { + if !oldMap[newItem] { + subtract = append(subtract, newItem) + } + } + return subtract +} + +func (c *Controller) send2Edge(acc *policyv1alpha1.ServiceAccountAccess, targets []string, opr string) { + sendObj := acc.DeepCopy() + for _, node := range targets { + resource, err := messagelayer.BuildResource(node, sendObj.Namespace, model.ResourceTypeSaAccess, sendObj.Name) + if err != nil { + klog.Warningf("built message resource failed with error: %s", err) + continue + } + // filter out the node list data + sendObj.Status.NodeList = []string{} + msg := model.NewMessage(""). + SetResourceVersion(sendObj.ResourceVersion). + FillBody(sendObj).BuildRouter(modules.PolicyControllerModuleName, constants.GroupResource, resource, opr) + if err := c.MessageLayer.Send(*msg); err != nil { + klog.Warningf("send message %s failed with error: %s", resource, err) + continue + } + } +} + +func (c *Controller) syncRules(ctx context.Context, acc *policyv1alpha1.ServiceAccountAccess) (controllerruntime.Result, error) { + var newSA = &corev1.ServiceAccount{} + err := c.Client.Get(ctx, types.NamespacedName{Namespace: acc.Namespace, Name: acc.Spec.ServiceAccount.Name}, newSA) + if (err != nil && apierrors.IsNotFound(err)) || (err == nil && newSA.DeletionTimestamp != nil) { + klog.V(4).Infof("serviceaccount %s/%s is removed and delete the policy resource", acc.Namespace, acc.Spec.ServiceAccount.Name) + copyObj := acc.DeepCopy() + if err := c.Client.Delete(ctx, copyObj); err != nil { + klog.Errorf("failed to delete serviceaccountaccess %s/%s, %v", copyObj.Namespace, copyObj.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + c.send2Edge(copyObj, copyObj.Status.NodeList, model.DeleteOperation) + return controllerruntime.Result{}, nil + } else if err != nil { + klog.Errorf("failed to get serviceaccount %s/%s, %v", acc.Namespace, acc.Spec.ServiceAccount.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + userInfo := serviceaccount.UserInfo(newSA.Namespace, newSA.Name, string(newSA.UID)) + var currentAcc = &policyv1alpha1.ServiceAccountAccess{} + c.VisitRulesFor(ctx, userInfo, acc.Namespace, currentAcc) + nodes, err := getNodeListOfServiceAccountAccess(ctx, c.Client, acc) + if err != nil { + klog.Errorf("failed to get node list of serviceaccountaccess %s/%s, %v", acc.Namespace, acc.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + currentAcc.Spec.ServiceAccount = *newSA + currentAcc.Spec.ServiceAccountUID = newSA.UID + if len(nodes) == 0 && len(acc.Status.NodeList) == 0 { + klog.Warningf("no nodes found for serviceaccountaccess %s/%s", acc.Namespace, acc.Name) + return controllerruntime.Result{}, nil + } + deleteNodes := subtractSlice(nodes, acc.Status.NodeList) + if len(deleteNodes) != 0 { + // no nodes in the current acc status, delete the acc + if len(nodes) == 0 { + if err = c.Client.Delete(ctx, acc); err != nil { + klog.Errorf("failed to delete serviceaccountaccess %s/%s, %v", acc.Namespace, acc.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + klog.V(4).Infof("delete serviceaccountaccess %s/%s", acc.Namespace, acc.Name) + c.send2Edge(acc, deleteNodes, model.DeleteOperation) + return controllerruntime.Result{}, nil + } + c.send2Edge(acc, deleteNodes, model.DeleteOperation) + } + sort.Slice(currentAcc.Spec.AccessRoleBinding, func(i, j int) bool { + return currentAcc.Spec.AccessRoleBinding[i].RoleBinding.Name < currentAcc.Spec.AccessRoleBinding[j].RoleBinding.Name + }) + sort.Slice(currentAcc.Spec.AccessClusterRoleBinding, func(i, j int) bool { + return currentAcc.Spec.AccessClusterRoleBinding[i].ClusterRoleBinding.Name < currentAcc.Spec.AccessClusterRoleBinding[j].ClusterRoleBinding.Name + }) + sort.Slice(nodes, func(i, j int) bool { + return nodes[i] < nodes[j] + }) + if !equalAccessBindingSlice(acc.Spec.AccessClusterRoleBinding, currentAcc.Spec.AccessClusterRoleBinding) || + !equalAccessBindingSlice(acc.Spec.AccessRoleBinding, currentAcc.Spec.AccessRoleBinding) || + !equalServiceAccount(&acc.Spec.ServiceAccount, ¤tAcc.Spec.ServiceAccount) || + acc.Spec.ServiceAccountUID != currentAcc.Spec.ServiceAccountUID { + acc.Spec = *currentAcc.Spec.DeepCopy() + if err := c.Client.Update(ctx, acc); err != nil { + klog.Errorf("failed to update serviceaccountaccess %s/%s, %v", acc.Namespace, acc.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + if !equality.Semantic.DeepEqual(acc.Status.NodeList, nodes) { + acc.Status.NodeList = append([]string{}, nodes...) + if err := c.Client.Status().Update(ctx, acc); err != nil { + klog.Errorf("failed to update serviceaccountaccess status %s/%s, %v", acc.Namespace, acc.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + } + c.send2Edge(acc, nodes, model.UpdateOperation) + } else { + addNodes := subtractSlice(acc.Status.NodeList, nodes) + klog.V(4).Infof("serviceaccountaccess spec %s/%s is up to date", acc.Namespace, acc.Name) + if len(addNodes) != 0 { + acc.Status.NodeList = append([]string{}, nodes...) + if err := c.Client.Status().Update(ctx, acc); err != nil { + klog.Errorf("failed to update serviceaccountaccess status %s/%s, %v", acc.Namespace, acc.Name, err) + return controllerruntime.Result{Requeue: true}, err + } + c.send2Edge(acc, addNodes, model.InsertOperation) + } + } + return controllerruntime.Result{}, nil +} + +func equalAccessBindingSlice(a, b interface{}) bool { + aBindings, aOk := a.([]policyv1alpha1.AccessRoleBinding) + bBindings, bOk := b.([]policyv1alpha1.AccessRoleBinding) + if !aOk || !bOk { + aClusterBindings, aClusterOk := a.([]policyv1alpha1.AccessClusterRoleBinding) + bClusterBindings, bClusterOk := b.([]policyv1alpha1.AccessClusterRoleBinding) + if !aClusterOk || !bClusterOk { + return false + } + if len(aClusterBindings) != len(bClusterBindings) { + return false + } + for i := range aClusterBindings { + if aClusterBindings[i].ClusterRoleBinding.Name != bClusterBindings[i].ClusterRoleBinding.Name || + !equality.Semantic.DeepEqual(aClusterBindings[i].Rules, bClusterBindings[i].Rules) || + !equality.Semantic.DeepEqual(aClusterBindings[i].ClusterRoleBinding.Labels, bClusterBindings[i].ClusterRoleBinding.Labels) || + !equality.Semantic.DeepEqual(aClusterBindings[i].ClusterRoleBinding.Annotations, bClusterBindings[i].ClusterRoleBinding.Annotations) || + !equality.Semantic.DeepEqual(aClusterBindings[i].ClusterRoleBinding.Subjects, bClusterBindings[i].ClusterRoleBinding.Subjects) || + !equality.Semantic.DeepEqual(aClusterBindings[i].ClusterRoleBinding.RoleRef, bClusterBindings[i].ClusterRoleBinding.RoleRef) { + return false + } + } + } else { + if len(aBindings) != len(bBindings) { + return false + } + for i := range aBindings { + if aBindings[i].RoleBinding.Name != bBindings[i].RoleBinding.Name || + aBindings[i].RoleBinding.Namespace != bBindings[i].RoleBinding.Namespace || + !equality.Semantic.DeepEqual(aBindings[i].Rules, bBindings[i].Rules) || + !equality.Semantic.DeepEqual(aBindings[i].RoleBinding.Labels, bBindings[i].RoleBinding.Labels) || + !equality.Semantic.DeepEqual(aBindings[i].RoleBinding.Annotations, bBindings[i].RoleBinding.Annotations) || + !equality.Semantic.DeepEqual(aBindings[i].RoleBinding.RoleRef, bBindings[i].RoleBinding.RoleRef) || + !equality.Semantic.DeepEqual(aBindings[i].RoleBinding.Subjects, bBindings[i].RoleBinding.Subjects) { + return false + } + } + } + return true +} + +func equalServiceAccount(a, b *corev1.ServiceAccount) bool { + if a == nil || b == nil { + return false + } + aCopy := a.DeepCopy() + bCopy := b.DeepCopy() + // ignore metadata because it is not be allowed to update in crd + aCopy.ObjectMeta = bCopy.ObjectMeta + return equality.Semantic.DeepEqual(aCopy, bCopy) +} + +func appliesToUser(user user.Info, subject rbacv1.Subject, namespace string) bool { + switch subject.Kind { + case rbacv1.UserKind: + return user.GetName() == subject.Name + + case rbacv1.GroupKind: + return has(user.GetGroups(), subject.Name) + + case rbacv1.ServiceAccountKind: + // default the namespace to namespace we're working in if its available. This allows rolebindings that reference + // SAs in th local namespace to avoid having to qualify them. + saNamespace := namespace + if len(subject.Namespace) > 0 { + saNamespace = subject.Namespace + } + if len(saNamespace) == 0 { + return false + } + // use a more efficient comparison for RBAC checking + return serviceaccount.MatchesUsername(saNamespace, subject.Name, user.GetName()) + default: + return false + } +} + +// appliesTo returns whether any of the bindingSubjects applies to the specified subject, +// and if true, the index of the first subject that applies +func appliesTo(user user.Info, bindingSubjects []rbacv1.Subject, namespace string) (int, bool) { + for i, bindingSubject := range bindingSubjects { + if appliesToUser(user, bindingSubject, namespace) { + return i, true + } + } + return 0, false +} + +func has(set []string, ele string) bool { + for _, s := range set { + if s == ele { + return true + } + } + return false +} + +// GetRoleReferenceRules attempts to resolve the RoleBinding or ClusterRoleBinding. +func (c *Controller) GetRoleReferenceRules(ctx context.Context, roleRef rbacv1.RoleRef, bindingNamespace string) ([]rbacv1.PolicyRule, error) { + switch roleRef.Kind { + case "Role": + var role = &rbacv1.Role{} + err := c.Client.Get(ctx, types.NamespacedName{Namespace: bindingNamespace, Name: roleRef.Name}, role) + if err != nil { + return nil, err + } + return role.Rules, nil + + case "ClusterRole": + var clusterRole = &rbacv1.ClusterRole{} + err := c.Client.Get(ctx, types.NamespacedName{Name: roleRef.Name}, clusterRole) + if err != nil { + return nil, err + } + return clusterRole.Rules, nil + + default: + return nil, fmt.Errorf("unsupported role reference kind: %q", roleRef.Kind) + } +} + +func (c *Controller) VisitRulesFor(ctx context.Context, user user.Info, namespace string, acc *policyv1alpha1.ServiceAccountAccess) { + crbl := &rbacv1.ClusterRoleBindingList{} + if err := c.Client.List(ctx, crbl); err != nil { + klog.Errorf("failed to list clusterrolebindings, %v", err) + return + } + for _, crb := range crbl.Items { + _, applies := appliesTo(user, crb.Subjects, "") + if !applies { + continue + } + rules, err := c.GetRoleReferenceRules(ctx, crb.RoleRef, "") + if err != nil { + klog.Errorf("failed to get rules for clusterrolebinding %s, %v", crb.Name, err) + return + } + var accessClusterRoleBinding = policyv1alpha1.AccessClusterRoleBinding{ + ClusterRoleBinding: crb, + Rules: rules, + } + acc.Spec.AccessClusterRoleBinding = append(acc.Spec.AccessClusterRoleBinding, accessClusterRoleBinding) + } + + if len(namespace) > 0 { + var roleBindingList = &rbacv1.RoleBindingList{} + if err := c.Client.List(ctx, roleBindingList, &client.ListOptions{Namespace: namespace}); err != nil { + klog.Errorf("failed to list rolebindings, %v", err) + return + } + for _, roleBinding := range roleBindingList.Items { + _, applies := appliesTo(user, roleBinding.Subjects, namespace) + if !applies { + continue + } + rules, err := c.GetRoleReferenceRules(ctx, roleBinding.RoleRef, namespace) + if err != nil { + klog.Errorf("failed to get rules for rolebinding %s, %v", roleBinding.Name, err) + return + } + var accessRoleBinding = policyv1alpha1.AccessRoleBinding{ + RoleBinding: roleBinding, + Rules: rules, + } + acc.Spec.AccessRoleBinding = append(acc.Spec.AccessRoleBinding, accessRoleBinding) + } + } +} diff --git a/cloud/pkg/policycontroller/manager/reconcile_test.go b/cloud/pkg/policycontroller/manager/reconcile_test.go new file mode 100644 index 000000000..cee178392 --- /dev/null +++ b/cloud/pkg/policycontroller/manager/reconcile_test.go @@ -0,0 +1,1781 @@ +package controller + +import ( + "context" + "encoding/json" + "reflect" + "sort" + "sync" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierror "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/authentication/user" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/kubeedge/beehive/pkg/common" + beehiveContext "github.com/kubeedge/beehive/pkg/core/context" + "github.com/kubeedge/beehive/pkg/core/model" + "github.com/kubeedge/kubeedge/cloud/pkg/common/messagelayer" + "github.com/kubeedge/kubeedge/cloud/pkg/common/modules" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" +) + +func TestIntersectSlice(t *testing.T) { + tests := []struct { + name string + a []string + b []string + want []string + }{ + { + name: "test1", + a: []string{"a", "b", "c"}, + b: []string{"b", "c", "d"}, + want: []string{"b", "c"}, + }, + { + name: "test2", + a: []string{"a", "b", "c"}, + b: []string{"d", "e", "f"}, + want: []string{}, + }, + { + name: "test3", + a: []string{"a", "b", "c"}, + b: []string{"a", "b", "c"}, + want: []string{"a", "b", "c"}, + }, + { + name: "test4", + a: []string{}, + b: []string{"a", "b", "c"}, + want: []string{}, + }, + { + name: "test5", + a: []string{"a", "b", "c"}, + b: []string{}, + want: []string{}, + }, + { + name: "test6", + a: []string{}, + b: []string{}, + want: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := intersectSlice(tt.a, tt.b); !reflect.DeepEqual(got, tt.want) { + t.Errorf("intersectSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSubtractSlice(t *testing.T) { + tests := []struct { + name string + a []string + b []string + want []string + }{ + { + name: "test1", + a: []string{"a", "b", "c"}, + b: []string{"b", "c", "d"}, + want: []string{"a"}, + }, + { + name: "test2", + a: []string{"a", "b", "c"}, + b: []string{"d", "e", "f"}, + want: []string{"a", "b", "c"}, + }, + { + name: "test3", + a: []string{"a", "b", "c"}, + b: []string{"a", "b", "c"}, + want: []string{}, + }, + { + name: "test4", + a: []string{}, + b: []string{"a", "b", "c"}, + want: []string{}, + }, + { + name: "test5", + a: []string{"a", "b", "c"}, + b: []string{}, + want: []string{"a", "b", "c"}, + }, + { + name: "test6", + a: []string{}, + b: []string{}, + want: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := subtractSlice(tt.b, tt.a); !reflect.DeepEqual(got, tt.want) { + t.Errorf("subtractSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAppliesTo(t *testing.T) { + tests := []struct { + subjects []rbacv1.Subject + user user.Info + namespace string + appliesTo bool + index int + testCase string + }{ + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "foobar"}, + }, + user: &user.DefaultInfo{Name: "foobar"}, + appliesTo: true, + index: 0, + testCase: "single subject that matches username", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "barfoo"}, + {Kind: rbacv1.UserKind, Name: "foobar"}, + }, + user: &user.DefaultInfo{Name: "foobar"}, + appliesTo: true, + index: 1, + testCase: "multiple subjects, one that matches username", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "barfoo"}, + {Kind: rbacv1.UserKind, Name: "foobar"}, + }, + user: &user.DefaultInfo{Name: "zimzam"}, + appliesTo: false, + testCase: "multiple subjects, none that match username", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "barfoo"}, + {Kind: rbacv1.GroupKind, Name: "foobar"}, + }, + user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}}, + appliesTo: true, + index: 1, + testCase: "multiple subjects, one that match group", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "barfoo"}, + {Kind: rbacv1.GroupKind, Name: "foobar"}, + }, + user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}}, + namespace: "namespace1", + appliesTo: true, + index: 1, + testCase: "multiple subjects, one that match group, should ignore namespace", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "barfoo"}, + {Kind: rbacv1.GroupKind, Name: "foobar"}, + {Kind: rbacv1.ServiceAccountKind, Namespace: "kube-system", Name: "default"}, + }, + user: &user.DefaultInfo{Name: "system:serviceaccount:kube-system:default"}, + namespace: "default", + appliesTo: true, + index: 2, + testCase: "multiple subjects with a service account that matches", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.UserKind, Name: "*"}, + }, + user: &user.DefaultInfo{Name: "foobar"}, + namespace: "default", + appliesTo: false, + testCase: "* user subject name doesn't match all users", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.GroupKind, Name: user.AllAuthenticated}, + {Kind: rbacv1.GroupKind, Name: user.AllUnauthenticated}, + }, + user: &user.DefaultInfo{Name: "foobar", Groups: []string{user.AllAuthenticated}}, + namespace: "default", + appliesTo: true, + index: 0, + testCase: "binding to all authenticated and unauthenticated subjects matches authenticated user", + }, + { + subjects: []rbacv1.Subject{ + {Kind: rbacv1.GroupKind, Name: user.AllAuthenticated}, + {Kind: rbacv1.GroupKind, Name: user.AllUnauthenticated}, + }, + user: &user.DefaultInfo{Name: "system:anonymous", Groups: []string{user.AllUnauthenticated}}, + namespace: "default", + appliesTo: true, + index: 1, + testCase: "binding to all authenticated and unauthenticated subjects matches anonymous user", + }, + } + + for _, tc := range tests { + gotIndex, got := appliesTo(tc.user, tc.subjects, tc.namespace) + if got != tc.appliesTo { + t.Errorf("case %q want appliesTo=%t, got appliesTo=%t", tc.testCase, tc.appliesTo, got) + } + if gotIndex != tc.index { + t.Errorf("case %q want index %d, got %d", tc.testCase, tc.index, gotIndex) + } + } +} + +func newServiceAccount() *v1.ServiceAccount { + return &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "ns1", + }, + } +} + +func TestNewSaAccessObject(t *testing.T) { + tests := []struct { + name string + sa *v1.ServiceAccount + result *policyv1alpha1.ServiceAccountAccess + }{ + { + name: "test NewSaAccessObject", + sa: newServiceAccount(), + result: &policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "ns1", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *newServiceAccount(), + }, + }, + }, + } + for _, tc := range tests { + got := newSaAccessObject(*tc.sa) + if !reflect.DeepEqual(got, tc.result) { + t.Errorf("case %q want=%v, got=%v", tc.name, tc.result, got) + } + } +} + +var podStr1 = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "pod1", + "namespace": "my-namespace" + }, + "spec": { + "serviceAccountName": "sa1", + "nodeName": "my-node", + "containers": [ + { + "name": "my-container", + "image": "my-image", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ] + } + ] + } +}` + +var podDelStr1 = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "podDel", + "namespace": "my-namespace", + "deletionTimestamp": "2022-01-01T00:00:00Z" + }, + "spec": { + "serviceAccountName": "sa1", + "nodeName": "my-node", + "containers": [ + { + "name": "my-container", + "image": "my-image", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ] + } + ] + } + }` + +var podStr2 = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "pod2", + "namespace": "my-namespace" + }, + "spec": { + "serviceAccountName": "sa1", + "nodeName": "my-node-2", + "containers": [ + { + "name": "my-container-2", + "image": "my-image", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ] + } + ] + } +}` + +var podStrWithoutNodeName = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "podwnn1", + "namespace": "my-namespace" + }, + "spec": { + "serviceAccountName": "sa1", + "containers": [ + { + "name": "my-container", + "image": "my-image", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ] + } + ] + } +}` + +var podStrWithoutSa = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "podwnn1", + "namespace": "my-namespace" + }, + "spec": { + "nodeName": "my-node", + "containers": [ + { + "name": "my-container", + "image": "my-image", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ] + } + ] + } +}` + +var saStr1 = `{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "sa1", + "namespace": "my-namespace", + "resourceVersion": "999" + } +}` + +var saStr2 = `{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "sa2", + "namespace": "my-namespace", + "resourceVersion": "999" + } +}` + +var saNsStr = `{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "sa1", + "namespace": "my-namespace2" + } +}` + +var roleStr1 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "role1", + "namespace": "my-namespace", + "resourceVersion": "999" + }, + "rules": [ + { + "apiGroups": [""], + "resources": ["pods"], + "verbs": ["get", "list", "watch"] + }, + { + "apiGroups": ["apps"], + "resources": ["deployments"], + "verbs": ["get", "list", "watch"] + } + ] +}` + +var roleNsStr1 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "role1", + "namespace": "my-namespace2", + "resourceVersion": "999" + }, + "rules": [ + { + "apiGroups": [""], + "resources": ["pods"], + "verbs": ["get", "list", "watch"] + }, + { + "apiGroups": ["apps"], + "resources": ["deployments"], + "verbs": ["get", "list", "watch"] + } + ] +}` + +var roleStr2 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "role2", + "namespace": "my-namespace", + "resourceVersion": "999" + }, + "rules": [ + { + "apiGroups": [""], + "resources": ["pods"], + "verbs": ["get", "list", "watch"] + }, + { + "apiGroups": ["apps"], + "resources": ["configmaps"], + "verbs": ["get", "list", "watch"] + } + ] +}` + +var rbStr1 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "rb1", + "namespace": "my-namespace", + "resourceVersion": "999" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "role1" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "sa1", + "namespace": "my-namespace" + } + ] +}` + +var rbStr2 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "rb2", + "namespace": "my-namespace", + "resourceVersion": "999" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "role2" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "sa1", + "namespace": "my-namespace" + } + ] +}` + +var rbWithCrStr = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "rbWithCr", + "namespace": "my-namespace", + "resourceVersion": "999" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "cr1" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "sa1", + "namespace": "my-namespace" + } + ] +}` + +var crStr1 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "cr1", + "resourceVersion": "999" + }, + "rules": [ + { + "apiGroups": [""], + "resources": ["pods"], + "verbs": ["get", "list", "watch"] + }, + { + "apiGroups": ["apps"], + "resources": ["deployments"], + "verbs": ["get", "list", "watch"] + } + ] +}` + +var crbStr1 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "crb1", + "resourceVersion": "999" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "cr1" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "sa1", + "namespace": "my-namespace" + } + ] +}` + +var crbStr2 = `{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "crb2", + "resourceVersion": "999" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "cr1" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "sa1", + "namespace": "my-namespace" + } + ] +}` + +func TestFilterResource(t *testing.T) { + var sa1 v1.ServiceAccount + err := json.Unmarshal([]byte(saStr1), &sa1) + if err != nil { + t.Errorf("Failed to unmarshal sa1: %v", err) + } + var sa2 v1.ServiceAccount + err = json.Unmarshal([]byte(saStr2), &sa2) + if err != nil { + t.Errorf("Failed to unmarshal sa2: %v", err) + } + var saNs v1.ServiceAccount + err = json.Unmarshal([]byte(saNsStr), &saNs) + if err != nil { + t.Errorf("Failed to unmarshal sa2: %v", err) + } + var role1 rbacv1.Role + err = json.Unmarshal([]byte(roleStr1), &role1) + if err != nil { + t.Errorf("Failed to unmarshal role1: %v", err) + } + var roleNs rbacv1.Role + err = json.Unmarshal([]byte(roleNsStr1), &roleNs) + if err != nil { + t.Errorf("Failed to unmarshal roleNs: %v", err) + } + var role2 rbacv1.Role + err = json.Unmarshal([]byte(roleStr2), &role2) + if err != nil { + t.Errorf("Failed to unmarshal role2: %v", err) + } + var rb1 rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbStr1), &rb1) + if err != nil { + t.Errorf("Failed to unmarshal rb1: %v", err) + } + var rb2 rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbStr2), &rb2) + if err != nil { + t.Errorf("Failed to unmarshal rb2: %v", err) + } + var rbWithCr rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbWithCrStr), &rbWithCr) + if err != nil { + t.Errorf("Failed to unmarshal rbWithCr: %v", err) + } + var cr1 rbacv1.ClusterRole + err = json.Unmarshal([]byte(crStr1), &cr1) + if err != nil { + t.Errorf("Failed to unmarshal cr1: %v", err) + } + var crb1 rbacv1.ClusterRoleBinding + err = json.Unmarshal([]byte(crbStr1), &crb1) + if err != nil { + t.Errorf("Failed to unmarshal crb1: %v", err) + } + var crb2 rbacv1.ClusterRoleBinding + err = json.Unmarshal([]byte(crbStr2), &crb2) + if err != nil { + t.Errorf("Failed to unmarshal crb2: %v", err) + } + var pod1 v1.Pod + err = json.Unmarshal([]byte(podStr1), &pod1) + if err != nil { + t.Errorf("Failed to unmarshal pod1: %v", err) + } + var podNoNodeName v1.Pod + err = json.Unmarshal([]byte(podStrWithoutNodeName), &podNoNodeName) + if err != nil { + t.Errorf("Failed to unmarshal podNoNodeName: %v", err) + } + var podNoSa v1.Pod + err = json.Unmarshal([]byte(podStrWithoutSa), &podNoSa) + if err != nil { + t.Errorf("Failed to unmarshal podNoSa: %v", err) + } + nodeList := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + }, + } + var tests = []struct { + name string + input []client.Object + rbacObj client.Object + obj client.Object + reconcileResult []controllerruntime.Request + rbacResult bool + objResult bool + }{ + { + name: "filter role or serviceaccount success", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}, {RoleBinding: rb2}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1, &rb2}, + rbacObj: &role1, + obj: &sa1, + rbacResult: true, + objResult: true, + reconcileResult: []controllerruntime.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "sa1", + Namespace: "my-namespace", + }, + }, + }, + }, + { + name: "filter role or serviceaccount failed", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1}, + rbacObj: &role2, + obj: &sa2, + rbacResult: false, + objResult: false, + reconcileResult: []controllerruntime.Request{}, + }, + { + name: "filter role failed with nil role", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1}, + rbacObj: &role2, + rbacResult: false, + reconcileResult: []controllerruntime.Request{}, + }, + { + name: "filter role or serviceaccount failed with different namespace", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}, + }, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1}, + rbacObj: &roleNs, + obj: &saNs, + rbacResult: false, + objResult: false, + reconcileResult: []controllerruntime.Request{}, + }, + { + name: "filter role failed with nil rolebinding and clusterrolebinding", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ServiceAccount: sa1}, + }}, + rbacObj: &role2, + rbacResult: false, + reconcileResult: []controllerruntime.Request{}, + }, + { + name: "filter rolebinding success", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1}, + rbacObj: &rb1, + rbacResult: true, + reconcileResult: []controllerruntime.Request{ + {NamespacedName: types.NamespacedName{Name: "sa1", Namespace: "my-namespace"}}, + }, + }, + { + name: "filter rolebinding failed", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1}, + rbacObj: &rb2, + rbacResult: true, + reconcileResult: []controllerruntime.Request{ + {NamespacedName: types.NamespacedName{Name: "sa1", Namespace: "my-namespace"}}, + }, + }, + { + name: "filter rolebinding failed with nil rolebinding", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1}, + rbacObj: &rb2, + rbacResult: true, + reconcileResult: []controllerruntime.Request{ + {NamespacedName: types.NamespacedName{Name: "sa1", Namespace: "my-namespace"}}, + }, + }, + { + name: "filter clusterrolebinding success", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1}, + rbacObj: &crb1, + rbacResult: true, + reconcileResult: []controllerruntime.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "sa1", + Namespace: "my-namespace", + }, + }, + }, + }, + { + name: "filter clusterrole success", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rb1}, + }, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + {ClusterRoleBinding: crb1}}}, + }, &crb1, &rb1}, + rbacObj: &cr1, + rbacResult: true, + reconcileResult: []controllerruntime.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "sa1", + Namespace: "my-namespace", + }, + }, + }, + }, + { + name: "filter rolebinding bind cluster role success", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + {RoleBinding: rbWithCr}}}, + }, &rbWithCr}, + rbacObj: &cr1, + rbacResult: true, + reconcileResult: []controllerruntime.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "sa1", + Namespace: "my-namespace", + }, + }, + }, + }, + { + name: "filter pod success", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + }, + }}, + obj: &pod1, + objResult: true, + }, + { + name: "filter pod failed for without service account", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + }, + }}, + obj: &podNoSa, + objResult: false, + }, + { + name: "filter pod failed for without node name", + input: []client.Object{&policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + }, + }}, + obj: &podNoNodeName, + objResult: false, + }, + } + var accessScheme = runtime.NewScheme() + if err := policyv1alpha1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add access scheme: %v", err) + } + if err := v1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add v1 scheme: %v", err) + } + if err := rbacv1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add rbacv1 scheme: %v", err) + } + for _, tc := range tests { + fakeClient := fake.NewClientBuilder().WithScheme(accessScheme).WithObjects(tc.input...).WithLists(nodeList).Build() + ctr := &Controller{ + Client: fakeClient, + } + if tc.rbacObj != nil { + got := ctr.filterResource(context.Background(), tc.rbacObj) + if !reflect.DeepEqual(got, tc.rbacResult) { + t.Errorf("case %q want=%v, got=%v", tc.name, tc.rbacResult, got) + } + got2 := ctr.mapRolesFunc(tc.rbacObj) + if !equality.Semantic.DeepEqual(got2, tc.reconcileResult) { + t.Errorf("case %q want=%v, got=%v", tc.name, tc.reconcileResult, got2) + } + } + if tc.obj != nil { + got1 := ctr.filterObject(context.Background(), tc.obj) + if !reflect.DeepEqual(got1, tc.objResult) { + t.Errorf("case %q want=%v, got=%v", tc.name, tc.objResult, got1) + } + } + } +} + +func TestMapObjectFunc(t *testing.T) { + var pod1 v1.Pod + err := json.Unmarshal([]byte(podStr1), &pod1) + if err != nil { + t.Errorf("Failed to unmarshal pod1: %v", err) + } + var podDel v1.Pod + err = json.Unmarshal([]byte(podDelStr1), &podDel) + if err != nil { + t.Errorf("Failed to unmarshal podDel: %v", err) + } + var rb2 rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbStr2), &rb2) + if err != nil { + t.Errorf("Failed to unmarshal rb2: %v", err) + } + var crb1 rbacv1.ClusterRoleBinding + err = json.Unmarshal([]byte(crbStr1), &crb1) + if err != nil { + t.Errorf("Failed to unmarshal crb1: %v", err) + } + var sa1 v1.ServiceAccount + err = json.Unmarshal([]byte(saStr1), &sa1) + if err != nil { + t.Errorf("Failed to unmarshal sa1: %v", err) + } + var sa2 v1.ServiceAccount + err = json.Unmarshal([]byte(saStr2), &sa2) + if err != nil { + t.Errorf("Failed to unmarshal sa2: %v", err) + } + var cr1 rbacv1.ClusterRole + err = json.Unmarshal([]byte(crStr1), &cr1) + if err != nil { + t.Errorf("Failed to unmarshal cr1: %v", err) + } + var rb1 rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbStr1), &rb1) + if err != nil { + t.Errorf("Failed to unmarshal rb1: %v", err) + } + var role1 rbacv1.Role + err = json.Unmarshal([]byte(roleStr1), &role1) + if err != nil { + t.Errorf("Failed to unmarshal role1: %v", err) + } + var tests = []struct { + name string + input *policyv1alpha1.ServiceAccountAccess + obj client.Object + reconcileResult []controllerruntime.Request + output *[]policyv1alpha1.ServiceAccountAccess + }{ + { + name: "match pod success and won't reconcile", + input: &policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1}, {RoleBinding: rb2}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1}}, + }, + }, + obj: &pod1, + reconcileResult: []controllerruntime.Request{}, + output: &[]policyv1alpha1.ServiceAccountAccess{{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1}, {RoleBinding: rb2}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1}}, + }, + }}, + }, + { + name: "match deleting pod success and reconcile", + input: &policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1}, {RoleBinding: rb2}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1}}, + }, + }, + obj: &podDel, + reconcileResult: []controllerruntime.Request{ + {NamespacedName: types.NamespacedName{Name: "sa1", Namespace: "my-namespace"}}, + }, + output: &[]policyv1alpha1.ServiceAccountAccess{{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1}, {RoleBinding: rb2}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1}}, + }, + }}, + }, + { + name: "match pod not exist in access list", + input: &policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa2", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa2, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + { + RoleBinding: rb2, + }, + }, + }, + }, + obj: &pod1, + reconcileResult: []controllerruntime.Request{}, + output: &[]policyv1alpha1.ServiceAccountAccess{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "sa2", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa2, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + { + RoleBinding: rb2, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + }, + }, + }, + }, + }, + { + name: "match serviceaccount success", + input: &policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + { + RoleBinding: rb1, + }, + { + RoleBinding: rb2, + }, + }, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + { + ClusterRoleBinding: crb1, + }, + }, + }, + }, + obj: &sa1, + reconcileResult: []controllerruntime.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "sa1", + Namespace: "my-namespace", + }, + }, + }, + output: &[]policyv1alpha1.ServiceAccountAccess{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sa1", + Namespace: "my-namespace", + }, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{ + { + RoleBinding: rb1, + }, + { + RoleBinding: rb2, + }, + }, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{ + { + ClusterRoleBinding: crb1, + }, + }, + }, + }}, + }, + } + var accessScheme = runtime.NewScheme() + if err := policyv1alpha1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add access scheme: %v", err) + } + for _, tc := range tests { + fakeClient := fake.NewClientBuilder().WithScheme(accessScheme).WithObjects(tc.input).Build() + ctr := &Controller{ + Client: fakeClient, + } + got := ctr.mapObjectFunc(tc.obj) + if !equality.Semantic.DeepEqual(got, tc.reconcileResult) { + t.Errorf("mapObjectFunc() = %v, want %v", got, tc.reconcileResult) + } + sort.Slice(*tc.output, func(i, j int) bool { + return (*tc.output)[i].Name < (*tc.output)[j].Name + }) + accList := &policyv1alpha1.ServiceAccountAccessList{} + if err := ctr.Client.List(context.Background(), accList, &client.ListOptions{Namespace: tc.obj.GetNamespace()}); err != nil { + t.Errorf("Failed to list access: %v", err) + } + sort.Slice(accList.Items, func(i, j int) bool { + return accList.Items[i].Name < accList.Items[j].Name + }) + for i := range accList.Items { + if accList.Items[i].Name != (*tc.output)[i].Name { + t.Errorf("case %q, got %v, want %v", tc.name, accList.Items[i].Name, (*tc.output)[i].Name) + } + if accList.Items[i].Namespace != (*tc.output)[i].Namespace { + t.Errorf("case %q, got %v, want %v", tc.name, accList.Items[i].Namespace, (*tc.output)[i].Namespace) + } + if !equality.Semantic.DeepEqual(accList.Items[i].Spec, (*tc.output)[i].Spec) { + t.Errorf("case %q, got %v, want %v", tc.name, accList.Items[i].Spec, (*tc.output)[i].Spec) + } + } + } +} + +func TestGetNodeListOfServiceAccountAccess(t *testing.T) { + // Create a sample ServiceAccountAccess object + saa := &policyv1alpha1.ServiceAccountAccess{ + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + Namespace: "test-ns", + }, + }, + }, + } + + nodeList := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + }, + } + + // Create a sample PodList object with two pods on different nodes + podList := &v1.PodList{ + Items: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "test-ns", + }, + Spec: v1.PodSpec{ + NodeName: "node-1", + ServiceAccountName: "test-sa", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: "test-ns", + }, + Spec: v1.PodSpec{ + NodeName: "node-2", + ServiceAccountName: "test-sa", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-3", + Namespace: "test-ns", + }, + Spec: v1.PodSpec{ + NodeName: "node-2", + ServiceAccountName: "test-sa", + }, + }, + // This test case is base on sigs.k8s.io/controller-runtime version >= v0.14.0 to filter obj by fieldSelector. + // But now sigs.k8s.io/controller-runtime v0.14.0 is base on go1.19, and can't be bumped up to v0.14.0 + /* { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-4", + Namespace: "test-ns", + }, + Spec: v1.PodSpec{ + NodeName: "node-4", + ServiceAccountName: "test-sa2", + }, + },*/ + }, + } + var v1Scheme = runtime.NewScheme() + if err := v1.AddToScheme(v1Scheme); err != nil { + t.Errorf("Failed to add access scheme: %v", err) + } + withScheme := fake.NewClientBuilder().WithScheme(v1Scheme) + fakeClient := withScheme.Build() + got, err := getNodeListOfServiceAccountAccess(context.Background(), fakeClient, saa) + if err != nil { + t.Errorf("fakeClient get node list error = %v", err) + } + if !equality.Semantic.DeepEqual(got, []string{}) { + t.Errorf("testcase 1 got %v, want %v", got, []string{}) + } + fakeClient2 := withScheme.WithObjects(&podList.Items[0]).WithLists(nodeList).Build() + got2, err := getNodeListOfServiceAccountAccess(context.Background(), fakeClient2, saa) + if err != nil { + t.Errorf("fakeClient2 get node list error = %v", err) + } + if !equality.Semantic.DeepEqual(got2, []string{"node-1"}) { + t.Errorf("testcase 2 got %v, want %v", got2, []string{"node-1"}) + } + fakeClient3 := withScheme.WithObjects(&podList.Items[1]).WithObjects(&podList.Items[2]).Build() + got3, err := getNodeListOfServiceAccountAccess(context.Background(), fakeClient3, saa) + if err != nil { + t.Errorf("fakeClient3 get node list error = %v", err) + } + if !equality.Semantic.DeepEqual(got3, []string{"node-1", "node-2"}) { + t.Errorf("testcase 3 got %v, want %v", got3, []string{"node-1", "node-2"}) + } +} + +func TestSyncRules(t *testing.T) { + var pod1 v1.Pod + err := json.Unmarshal([]byte(podStr1), &pod1) + if err != nil { + t.Errorf("Failed to unmarshal pod1: %v", err) + } + var pod2 v1.Pod + err = json.Unmarshal([]byte(podStr2), &pod2) + if err != nil { + t.Errorf("Failed to unmarshal pod2: %v", err) + } + var podWithoutNodeName v1.Pod + err = json.Unmarshal([]byte(podStrWithoutNodeName), &podWithoutNodeName) + if err != nil { + t.Errorf("Failed to unmarshal podWithoutNodeName: %v", err) + } + var rb2 rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbStr2), &rb2) + if err != nil { + t.Errorf("Failed to unmarshal rb2: %v", err) + } + var crb1 rbacv1.ClusterRoleBinding + err = json.Unmarshal([]byte(crbStr1), &crb1) + if err != nil { + t.Errorf("Failed to unmarshal crb1: %v", err) + } + var sa1 v1.ServiceAccount + err = json.Unmarshal([]byte(saStr1), &sa1) + if err != nil { + t.Errorf("Failed to unmarshal sa1: %v", err) + } + var sa2 v1.ServiceAccount + err = json.Unmarshal([]byte(saStr2), &sa2) + if err != nil { + t.Errorf("Failed to unmarshal sa2: %v", err) + } + var cr1 rbacv1.ClusterRole + err = json.Unmarshal([]byte(crStr1), &cr1) + if err != nil { + t.Errorf("Failed to unmarshal cr1: %v", err) + } + var rb1 rbacv1.RoleBinding + err = json.Unmarshal([]byte(rbStr1), &rb1) + if err != nil { + t.Errorf("Failed to unmarshal rb1: %v", err) + } + var role1 rbacv1.Role + err = json.Unmarshal([]byte(roleStr1), &role1) + if err != nil { + t.Errorf("Failed to unmarshal role1: %v", err) + } + var role2 rbacv1.Role + err = json.Unmarshal([]byte(roleStr2), &role2) + if err != nil { + t.Errorf("Failed to unmarshal role1: %v", err) + } + nodeList := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node-2", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node-3", + Labels: map[string]string{ + "node-role.kubernetes.io/edge": "", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node-4", + }, + }, + }, + } + var nodeStatus1 = policyv1alpha1.AccessStatus{NodeList: []string{"my-node"}} + var nodeStatus2 = policyv1alpha1.AccessStatus{NodeList: []string{"my-node", "my-node-2"}} + var nodeStatus3 = policyv1alpha1.AccessStatus{NodeList: []string{"my-node-2"}} + var nodeStatus4 = policyv1alpha1.AccessStatus{NodeList: []string{"my-node-2", "my-node-3"}} + var saa1 = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1, Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1, Rules: cr1.Rules}}, + }, + Status: nodeStatus1, + } + var saa2 = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1, Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1, Rules: cr1.Rules}}, + }, + Status: nodeStatus2, + } + var saa3 = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1, Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1, Rules: cr1.Rules}}, + }, + Status: nodeStatus3, + } + var saa4 = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1, Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1, Rules: cr1.Rules}}, + }, + Status: nodeStatus4, + } + var saa5 = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1, Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1, Rules: cr1.Rules}}, + }, + Status: nodeStatus1, + } + var saaDiffName = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa2", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa2, + }, + Status: nodeStatus2, + } + var saaDeletion = policyv1alpha1.ServiceAccountAccess{ + ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace", DeletionTimestamp: &metav1.Time{Time: time.Now()}}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: sa1, + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: rb1, Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: crb1, Rules: cr1.Rules}}, + }, + Status: nodeStatus1, + } + var tests = []struct { + name string + input *policyv1alpha1.ServiceAccountAccess + obj []client.Object + reconcileResult controllerruntime.Result + output *policyv1alpha1.ServiceAccountAccess + msgOpr []string + }{ + { + name: "rolebinding updated only", + input: saa1.DeepCopy(), + obj: []client.Object{saa1.DeepCopy(), pod1.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), crb1.DeepCopy(), + cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node"}}, + }, + msgOpr: []string{model.UpdateOperation}, + }, + { + name: "rolebinding updated and inserted new node", + input: saa1.DeepCopy(), + obj: []client.Object{saa1.DeepCopy(), pod1.DeepCopy(), pod2.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node", "my-node-2"}}, + }, + msgOpr: []string{model.UpdateOperation, model.UpdateOperation}, + }, + { + name: "rolebinding updated and inserted/deleted new node", + input: saa3.DeepCopy(), + obj: []client.Object{saa3.DeepCopy(), pod1.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), crb1.DeepCopy(), + cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node"}}, + }, + msgOpr: []string{model.DeleteOperation, model.UpdateOperation}, + }, + { + name: "rolebinding updated and inserted/deleted/updated new node", + input: saa4.DeepCopy(), + obj: []client.Object{saa4.DeepCopy(), pod1.DeepCopy(), pod2.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node", "my-node-2"}}, + }, + msgOpr: []string{model.DeleteOperation, model.UpdateOperation}, + }, + { + name: "rolebinding updated and inserted new node with none old node", + input: saa5.DeepCopy(), + obj: []client.Object{saa5.DeepCopy(), &pod1, &pod2, &sa1, &rb1, &crb1, &cr1, &role1, &rb2, &role2}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node", "my-node-2"}}, + }, + msgOpr: []string{model.UpdateOperation, model.UpdateOperation}, + }, + { + name: "rolebinding updated and deleted old node only", + input: saa4.DeepCopy(), + obj: []client.Object{saa4.DeepCopy(), podWithoutNodeName.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{}}, + }, + msgOpr: []string{model.DeleteOperation}, + }, + { + name: "rolebinding updated and updated/deleted node", + input: saa2.DeepCopy(), + obj: []client.Object{saa2.DeepCopy(), pod1.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node"}}, + }, + msgOpr: []string{model.DeleteOperation, model.UpdateOperation}, + }, + { + name: "rolebinding updated and none nodes", + input: saa5.DeepCopy(), + obj: []client.Object{saa5.DeepCopy(), podWithoutNodeName.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}, {RoleBinding: *rb2.DeepCopy(), Rules: role2.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{}}, + }, + msgOpr: []string{}, + }, + { + name: "service account not found", + input: saa5.DeepCopy(), + obj: []client.Object{saa5.DeepCopy(), podWithoutNodeName.DeepCopy(), sa2.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy(), rb2.DeepCopy(), role2.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node"}}, + }, + msgOpr: []string{model.DeleteOperation}, + }, + { + name: "insert only", + input: saa1.DeepCopy(), + obj: []client.Object{saa1.DeepCopy(), pod1.DeepCopy(), pod2.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node", "my-node-2"}}, + }, + msgOpr: []string{model.InsertOperation}, + }, + { + name: "reconcile failed cause serviceaccountaccess not found", + input: saaDiffName.DeepCopy(), + obj: []client.Object{saa1.DeepCopy(), pod1.DeepCopy(), pod2.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), + crb1.DeepCopy(), cr1.DeepCopy(), role1.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{}, + msgOpr: []string{}, + }, + { + name: "reconcile failed cause deletionTimestamp not nil", + input: saaDeletion.DeepCopy(), + obj: []client.Object{saaDeletion.DeepCopy(), pod1.DeepCopy(), sa1.DeepCopy(), rb1.DeepCopy(), crb1.DeepCopy(), + cr1.DeepCopy(), role1.DeepCopy()}, + reconcileResult: controllerruntime.Result{}, + output: &policyv1alpha1.ServiceAccountAccess{ObjectMeta: metav1.ObjectMeta{Name: "sa1", Namespace: "my-namespace"}, + Spec: policyv1alpha1.AccessSpec{ + ServiceAccount: *sa1.DeepCopy(), + AccessRoleBinding: []policyv1alpha1.AccessRoleBinding{{RoleBinding: *rb1.DeepCopy(), Rules: role1.Rules}}, + AccessClusterRoleBinding: []policyv1alpha1.AccessClusterRoleBinding{{ClusterRoleBinding: *crb1.DeepCopy(), Rules: cr1.Rules}}, + }, + Status: policyv1alpha1.AccessStatus{NodeList: []string{"my-node"}}, + }, + msgOpr: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cloudHub := &common.ModuleInfo{ + ModuleName: modules.CloudHubModuleName, + ModuleType: common.MsgCtxTypeChannel, + } + beehiveContext.InitContext([]string{common.MsgCtxTypeChannel}) + beehiveContext.AddModule(cloudHub) + var accessScheme = runtime.NewScheme() + if err := policyv1alpha1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add policyv1alpha1 scheme: %v", err) + } + if err := v1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add v1 scheme: %v", err) + } + if err := rbacv1.AddToScheme(accessScheme); err != nil { + t.Errorf("Failed to add rbacv1 scheme: %v", err) + } + fakeClient := fake.NewClientBuilder().WithScheme(accessScheme).WithObjects(tt.obj...).WithLists(nodeList).Build() + ctr := &Controller{ + Client: fakeClient, + MessageLayer: messagelayer.PolicyControllerMessageLayer(), + } + var rst controllerruntime.Result + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + inputObj := tt.input.DeepCopy() + rst, err = ctr.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: inputObj.Name, Namespace: inputObj.Namespace}}) + if err != nil { + t.Errorf("TestCase %q Failed to syncRules: %v", tt.name, err) + } + }() + var oprs []string + for range tt.msgOpr { + message, _ := beehiveContext.Receive(modules.CloudHubModuleName) + oprs = append(oprs, message.GetOperation()) + } + wg.Wait() + sort.Strings(oprs) + sort.Strings(tt.msgOpr) + if !equality.Semantic.DeepEqual(oprs, tt.msgOpr) { + t.Errorf("TestCase %q message operation got %v, want %v", tt.name, oprs, tt.msgOpr) + } + if !equality.Semantic.DeepEqual(rst, tt.reconcileResult) { + t.Errorf("TestCase %q Expected: %v, got: %v", tt.name, tt.reconcileResult, rst) + } + saa := &policyv1alpha1.ServiceAccountAccess{} + err = fakeClient.Get(context.Background(), types.NamespacedName{Name: tt.input.Name, Namespace: tt.input.Namespace}, saa) + if err != nil && apierror.IsNotFound(err) { + return + } else if err != nil { + t.Errorf("TestCase %q Failed to get saa: %v", tt.name, err) + } + if !equality.Semantic.DeepEqual((*saa).Spec.ServiceAccount, (*(tt.output)).Spec.ServiceAccount) { + t.Errorf("TestCase %q Expected spec serviceaccount: %+v, got: %+v", tt.name, tt.output.Spec.ServiceAccount, saa.Spec.ServiceAccount) + } + if !equality.Semantic.DeepEqual((*saa).Spec.AccessClusterRoleBinding, (*(tt.output)).Spec.AccessClusterRoleBinding) { + t.Errorf("TestCase %q Expected spec crb: %+v, got: %+v", tt.name, tt.output.Spec.AccessClusterRoleBinding, saa.Spec.AccessClusterRoleBinding) + } + if !equality.Semantic.DeepEqual((*saa).Spec.AccessRoleBinding, (*(tt.output)).Spec.AccessRoleBinding) { + t.Errorf("TestCase %q Expected spec rb: %+v, got: %+v", tt.name, tt.output.Spec.AccessRoleBinding, saa.Spec.AccessRoleBinding) + } + sort.Strings(saa.Status.NodeList) + sort.Strings(tt.output.Status.NodeList) + if !equality.Semantic.DeepEqual(saa.Status.NodeList, tt.output.Status.NodeList) { + t.Errorf("TestCase %q Expected status: %v, got: %v", tt.name, tt.output.Status.NodeList, saa.Status.NodeList) + } + }) + } +} diff --git a/cloud/pkg/policycontroller/policycontroller.go b/cloud/pkg/policycontroller/policycontroller.go new file mode 100644 index 000000000..734f0aee5 --- /dev/null +++ b/cloud/pkg/policycontroller/policycontroller.go @@ -0,0 +1,104 @@ +package policycontroller + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/kubeedge/beehive/pkg/core" + beehiveContext "github.com/kubeedge/beehive/pkg/core/context" + "github.com/kubeedge/kubeedge/cloud/pkg/common/messagelayer" + "github.com/kubeedge/kubeedge/cloud/pkg/common/modules" + pm "github.com/kubeedge/kubeedge/cloud/pkg/policycontroller/manager" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" + kefeatures "github.com/kubeedge/kubeedge/pkg/features" +) + +// policyController use beehive context message layer +type policyController struct { + manager manager.Manager + ctx context.Context +} + +var _ core.Module = (*policyController)(nil) + +var accessScheme = runtime.NewScheme() + +func init() { + utilruntime.Must(scheme.AddToScheme(accessScheme)) + utilruntime.Must(policyv1alpha1.AddToScheme(accessScheme)) +} + +func NewAccessRoleControllerManager(ctx context.Context, kubeCfg *rest.Config) (manager.Manager, error) { + controllerManager, err := controllerruntime.NewManager(kubeCfg, controllerruntime.Options{ + Scheme: accessScheme, + MetricsBindAddress: "0", // disable metrics + // TODO: leader election + // TODO: /healthz + }) + if err != nil { + return nil, fmt.Errorf("failed to create controller manager, %v", err) + } + + if err := setupControllers(ctx, controllerManager); err != nil { + return nil, err + } + return controllerManager, nil +} + +func setupControllers(ctx context.Context, mgr manager.Manager) error { + // This returned cli will directly acquire the unstructured objects from API Server which + // have not be registered in the accessScheme. + cli := mgr.GetClient() + pc := &pm.Controller{ + Client: cli, + MessageLayer: messagelayer.PolicyControllerMessageLayer(), + } + + klog.Info("setup policy controller") + if err := pc.SetupWithManager(ctx, mgr); err != nil { + return fmt.Errorf("failed to setup nodegroup controller, %v", err) + } + return nil +} + +func Register(kubeCfg *rest.Config) { + var pc = &policyController{} + pc.ctx = beehiveContext.GetContext() + mgr, err := NewAccessRoleControllerManager(pc.ctx, kubeCfg) + if err != nil { + klog.Fatalf("failed to create controller manager, %v", err) + } + pc.manager = mgr + core.Register(pc) +} + +// Name of controller +func (pc *policyController) Name() string { + return modules.PolicyControllerModuleName +} + +// Group of controller +func (pc *policyController) Group() string { + return modules.PolicyControllerGroupName +} + +// Enable indicates whether enable this module +func (pc *policyController) Enable() bool { + return kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) +} + +// Start controller +func (pc *policyController) Start() { + // mgr.Start will block until the manager has stopped + if err := pc.manager.Start(pc.ctx); err != nil { + klog.Fatalf("failed to start controller manager, %v", err) + } +} diff --git a/cloud/pkg/policycontroller/policycontroller_test.go b/cloud/pkg/policycontroller/policycontroller_test.go new file mode 100644 index 000000000..1cdd3247e --- /dev/null +++ b/cloud/pkg/policycontroller/policycontroller_test.go @@ -0,0 +1,38 @@ +package policycontroller + +import ( + "reflect" + "testing" + + "github.com/kubeedge/kubeedge/pkg/features" +) + +func TestRegister(t *testing.T) { + if err := features.DefaultMutableFeatureGate.SetFromMap(map[string]bool{string(features.RequireAuthorization): true}); err != nil { + t.Errorf("Failed to set feature gate: %v", err) + } + tests := []struct { + name string + controller *policyController + ctrEnable bool + ctrName string + }{ + { + name: "Register Policy controller", + controller: &policyController{}, + ctrName: "policycontroller", + ctrEnable: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var pc = &policyController{} + if !reflect.DeepEqual(tt.ctrName, pc.Name()) { + t.Errorf("TestCase %q got %v, want %v", tt.name, pc.Name(), tt.ctrName) + } + if !reflect.DeepEqual(tt.ctrEnable, pc.Enable()) { + t.Errorf("TestCase %q got %v, want %v", tt.name, pc.Enable(), tt.ctrEnable) + } + }) + } +} diff --git a/cloud/pkg/synccontroller/objectsync.go b/cloud/pkg/synccontroller/objectsync.go index c0a4d7fe7..bb517888d 100644 --- a/cloud/pkg/synccontroller/objectsync.go +++ b/cloud/pkg/synccontroller/objectsync.go @@ -9,7 +9,6 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" @@ -90,11 +89,6 @@ func (sctl *SyncController) gcOrphanedObjectSync(sync *v1alpha1.ObjectSync) { func sendEvents(nodeName string, sync *v1alpha1.ObjectSync, resourceType string, objectResourceVersion string, obj interface{}) { - runtimeObj := obj.(runtime.Object) - if err := util.SetMetaType(runtimeObj); err != nil { - klog.Warningf("failed to set metatype :%v", err) - } - if sync.Status.ObjectResourceVersion == "" { klog.Errorf("The ObjectResourceVersion is empty in status of objectsync: %s", sync.Name) return diff --git a/common/constants/default.go b/common/constants/default.go index fab9a5f16..ea58f3229 100644 --- a/common/constants/default.go +++ b/common/constants/default.go @@ -25,11 +25,12 @@ const ( // Resources const ( // Certificates - DefaultConfigDir = "/etc/kubeedge/config/" - DefaultCAFile = "/etc/kubeedge/ca/rootCA.crt" - DefaultCAKeyFile = "/etc/kubeedge/ca/rootCA.key" - DefaultCertFile = "/etc/kubeedge/certs/server.crt" - DefaultKeyFile = "/etc/kubeedge/certs/server.key" + DefaultConfigDir = "/etc/kubeedge/config/" + DefaultCAFile = "/etc/kubeedge/ca/rootCA.crt" + DefaultCAKeyFile = "/etc/kubeedge/ca/rootCA.key" + DefaultCertFile = "/etc/kubeedge/certs/server.crt" + DefaultKeyFile = "/etc/kubeedge/certs/server.key" + DefaultServiceAccountIssuer = "https://kubernetes.default.svc.cluster.local" DefaultCAURL = "/ca.crt" DefaultCertURL = "/edge.crt" diff --git a/common/types/types.go b/common/types/types.go index 7efe5e140..4fe163a68 100644 --- a/common/types/types.go +++ b/common/types/types.go @@ -3,6 +3,7 @@ package types import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -46,3 +47,9 @@ type NodeUpgradeJobResponse struct { Status string Reason string } + +// ObjectResp is the object that api-server response +type ObjectResp struct { + Object metaV1.Object + Err error +} diff --git a/edge/pkg/edged/kubeclientbridge/typed/core/v1/serviceaccount_bridge.go b/edge/pkg/edged/kubeclientbridge/typed/core/v1/serviceaccount_bridge.go index 4f39c94f7..b16c93721 100644 --- a/edge/pkg/edged/kubeclientbridge/typed/core/v1/serviceaccount_bridge.go +++ b/edge/pkg/edged/kubeclientbridge/typed/core/v1/serviceaccount_bridge.go @@ -27,6 +27,7 @@ import ( "context" authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" fakecorev1 "k8s.io/client-go/kubernetes/typed/core/v1/fake" @@ -44,3 +45,7 @@ type ServiceAccountsBridge struct { func (c *ServiceAccountsBridge) CreateToken(ctx context.Context, serviceAccountName string, tokenRequest *authenticationv1.TokenRequest, opts metav1.CreateOptions) (result *authenticationv1.TokenRequest, err error) { return c.MetaClient.ServiceAccountToken().GetServiceAccountToken(c.ns, serviceAccountName, tokenRequest) } + +func (c *ServiceAccountsBridge) Get(ctx context.Context, name string, options metav1.GetOptions) (result *corev1.ServiceAccount, err error) { + return c.MetaClient.ServiceAccounts(c.ns).Get(name) +} diff --git a/edge/pkg/metamanager/client/metaclient.go b/edge/pkg/metamanager/client/metaclient.go index 1b3c72106..f6d0a7022 100644 --- a/edge/pkg/metamanager/client/metaclient.go +++ b/edge/pkg/metamanager/client/metaclient.go @@ -16,7 +16,7 @@ var ( syncMsgRespTimeout = 1 * time.Minute ) -//CoreInterface is interface of metaclient +// CoreInterface is interface of metaclient type CoreInterface interface { PodsGetter PodStatusGetter @@ -25,6 +25,7 @@ type CoreInterface interface { NodeStatusGetter SecretsGetter ServiceAccountTokenGetter + ServiceAccountsGetter PersistentVolumesGetter PersistentVolumeClaimsGetter VolumeAttachmentsGetter @@ -59,6 +60,10 @@ func (m *metaClient) ServiceAccountToken() ServiceAccountTokenInterface { return newServiceAccountToken(m.send) } +func (m *metaClient) ServiceAccounts(namespace string) ServiceAccountInterface { + return newServiceAccount(namespace) +} + func (m *metaClient) PodStatus(namespace string) PodStatusInterface { return newPodStatus(namespace, m.send) } @@ -89,7 +94,7 @@ func New() CoreInterface { } } -//SendInterface is to sync interface +// SendInterface is to sync interface type SendInterface interface { SendSync(message *model.Message) (*model.Message, error) Send(message *model.Message) @@ -125,11 +130,3 @@ func (s *send) SendSync(message *model.Message) (*model.Message, error) { func (s *send) Send(message *model.Message) { beehiveContext.Send(modules.MetaManagerModuleName, *message) } - -func SetSyncPeriod(time time.Duration) { - syncPeriod = time -} - -func SetSyncMsgRespTimeout(time time.Duration) { - syncMsgRespTimeout = time -} diff --git a/edge/pkg/metamanager/client/nodestatus.go b/edge/pkg/metamanager/client/nodestatus.go index 66db02a61..2f7eda187 100644 --- a/edge/pkg/metamanager/client/nodestatus.go +++ b/edge/pkg/metamanager/client/nodestatus.go @@ -38,6 +38,7 @@ func (c *nodeStatus) Create(ns *edgeapi.NodeStatusRequest) (*edgeapi.NodeStatusR return nil, nil } +// deprecated func (c *nodeStatus) Update(rsName string, ns edgeapi.NodeStatusRequest) error { resource := fmt.Sprintf("%s/%s/%s", c.namespace, model.ResourceTypeNodeStatus, rsName) nodeStatusMsg := message.BuildMsg(modules.MetaGroup, "", modules.EdgedModuleName, resource, model.UpdateOperation, ns) diff --git a/edge/pkg/metamanager/client/pod.go b/edge/pkg/metamanager/client/pod.go index 61ea60002..7cfcc1244 100644 --- a/edge/pkg/metamanager/client/pod.go +++ b/edge/pkg/metamanager/client/pod.go @@ -16,12 +16,12 @@ import ( "github.com/kubeedge/kubeedge/edge/pkg/common/modules" ) -//PodsGetter is interface to get pods +// PodsGetter is interface to get pods type PodsGetter interface { Pods(namespace string) PodsInterface } -//PodsInterface is pod interface +// PodsInterface is pod interface type PodsInterface interface { Create(*corev1.Pod) (*corev1.Pod, error) Update(*corev1.Pod) error diff --git a/edge/pkg/metamanager/client/podstatus.go b/edge/pkg/metamanager/client/podstatus.go index c6ad198af..5378518d3 100644 --- a/edge/pkg/metamanager/client/podstatus.go +++ b/edge/pkg/metamanager/client/podstatus.go @@ -39,6 +39,7 @@ func (c *podStatus) Create(ps *edgeapi.PodStatusRequest) (*edgeapi.PodStatusRequ return nil, nil } +// deprecated func (c *podStatus) Update(rsName string, ps edgeapi.PodStatusRequest) error { podStatusMsg := message.BuildMsg(commodule.MetaGroup, "", commodule.EdgedModuleName, c.namespace+"/"+model.ResourceTypePodStatus+"/"+rsName, model.UpdateOperation, ps) resp, err := c.send.SendSync(podStatusMsg) diff --git a/edge/pkg/metamanager/client/secret.go b/edge/pkg/metamanager/client/secret.go index 94f031bb7..13ee99af9 100644 --- a/edge/pkg/metamanager/client/secret.go +++ b/edge/pkg/metamanager/client/secret.go @@ -11,12 +11,12 @@ import ( "github.com/kubeedge/kubeedge/edge/pkg/common/modules" ) -//SecretsGetter is interface to get client secrets +// SecretsGetter is interface to get client secrets type SecretsGetter interface { Secrets(namespace string) SecretsInterface } -//SecretsInterface is interface for client secret +// SecretsInterface is interface for client secret type SecretsInterface interface { Create(*api.Secret) (*api.Secret, error) Update(*api.Secret) error diff --git a/edge/pkg/metamanager/client/serviceaccount.go b/edge/pkg/metamanager/client/serviceaccount.go index 0323fab40..eeb5e2efa 100644 --- a/edge/pkg/metamanager/client/serviceaccount.go +++ b/edge/pkg/metamanager/client/serviceaccount.go @@ -6,14 +6,15 @@ import ( "time" authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" "github.com/kubeedge/beehive/pkg/core/model" "github.com/kubeedge/kubeedge/edge/pkg/common/message" "github.com/kubeedge/kubeedge/edge/pkg/common/modules" - "github.com/kubeedge/kubeedge/edge/pkg/metamanager" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/dao" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" ) // ServiceAccountTokenGetter is interface to get client service account token @@ -85,8 +86,23 @@ func requiresRefresh(tr *authenticationv1.TokenRequest) bool { return false } +// KeyFunc keys should be nonconfidential and safe to log +func KeyFunc(name, namespace string, tr *authenticationv1.TokenRequest) string { + var exp int64 + if tr.Spec.ExpirationSeconds != nil { + exp = *tr.Spec.ExpirationSeconds + } + + var ref authenticationv1.BoundObjectReference + if tr.Spec.BoundObjectRef != nil { + ref = *tr.Spec.BoundObjectRef + } + + return fmt.Sprintf("%q/%q/%#v/%#v/%#v", name, namespace, tr.Spec.Audiences, exp, ref) +} + func getTokenLocally(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { - resKey := metamanager.KeyFunc(name, namespace, tr) + resKey := KeyFunc(name, namespace, tr) metas, err := dao.QueryMeta("key", resKey) if err != nil { klog.Errorf("query meta %s failed: %v", resKey, err) @@ -170,3 +186,66 @@ func handleServiceAccountTokenFromMetaManager(content []byte) (*authenticationv1 } return &serviceAccount, nil } + +// ServiceAccountGetter is interface to get Client service account +type ServiceAccountsGetter interface { + ServiceAccounts(namespace string) ServiceAccountInterface +} + +// ServiceAccountInterface is interface for Client service account token +type ServiceAccountInterface interface { + Get(name string) (*corev1.ServiceAccount, error) +} + +type serviceAccount struct { + namespace string +} + +func newServiceAccount(namespace string) *serviceAccount { + return &serviceAccount{namespace: namespace} +} + +func (s *serviceAccount) Get(name string) (*corev1.ServiceAccount, error) { + rst, err := dao.QueryMeta("type", model.ResourceTypeSaAccess) + if err != nil { + return nil, err + } + for _, v := range *rst { + var saAccess policyv1alpha1.ServiceAccountAccess + err = json.Unmarshal([]byte(v), &saAccess) + if err != nil { + klog.Errorf("failed to unmarshal saAccess %v", err) + return nil, err + } + if saAccess.Namespace == s.namespace && saAccess.Spec.ServiceAccount.Name == name { + rst := saAccess.Spec.ServiceAccount + rst.UID = saAccess.Spec.ServiceAccountUID + return &rst, nil + } + } + return nil, fmt.Errorf("serviceaccount %s/%s not found", s.namespace, name) +} + +func CheckTokenExist(token string) bool { + if token == "" { + return false + } + metas, err := dao.QueryMeta("type", model.ResourceTypeServiceAccountToken) + if err != nil { + klog.Errorf("query meta %s failed: %v", model.ResourceTypeServiceAccountToken, err) + return false + } + + for _, v := range *metas { + var tokenRequest authenticationv1.TokenRequest + err = json.Unmarshal([]byte(v), &tokenRequest) + if err != nil { + klog.Errorf("unmarshal resource %s token request failed: %v", model.ResourceTypeServiceAccountToken, err) + return false + } + if tokenRequest.Status.Token == token { + return true + } + } + return false +} diff --git a/edge/pkg/metamanager/client/serviceaccountaccess.go b/edge/pkg/metamanager/client/serviceaccountaccess.go new file mode 100644 index 000000000..560be7d9d --- /dev/null +++ b/edge/pkg/metamanager/client/serviceaccountaccess.go @@ -0,0 +1,189 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/serviceaccount" + + "github.com/kubeedge/beehive/pkg/core/model" + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/dao" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" +) + +const ( + roleKind = "Role" + clusterRoleKind = "ClusterRole" +) + +type RoleGetter struct { +} + +func (g *RoleGetter) GetRole(namespace, name string) (*rbacv1.Role, error) { + rst, err := dao.QueryMeta("type", model.ResourceTypeSaAccess) + if err != nil { + return nil, err + } + for _, v := range *rst { + var saAccess policyv1alpha1.ServiceAccountAccess + err = json.Unmarshal([]byte(v), &saAccess) + if err != nil { + klog.Errorf("failed to unmarshal saAccess %v", err) + return nil, err + } + for _, rb := range saAccess.Spec.AccessRoleBinding { + if rb.RoleBinding.RoleRef.Kind == roleKind && rb.RoleBinding.RoleRef.Name == name && + saAccess.Namespace == namespace { + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: rb.RoleBinding.RoleRef.Name, + Namespace: saAccess.Namespace, + }, + Rules: rb.Rules, + }, nil + } + } + } + return nil, fmt.Errorf("role %s/%s not found", namespace, name) +} + +type RoleBindingLister struct { +} + +func (l *RoleBindingLister) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) { + rst, err := dao.QueryMeta("type", model.ResourceTypeSaAccess) + if err != nil { + return nil, err + } + var items = make(map[string]struct{}) + var res []*rbacv1.RoleBinding + for _, v := range *rst { + var saAccess policyv1alpha1.ServiceAccountAccess + err = json.Unmarshal([]byte(v), &saAccess) + if err != nil { + klog.Errorf("failed to unmarshal saAccess %v", err) + return nil, err + } + for _, rb := range saAccess.Spec.AccessRoleBinding { + var tmp = rb.RoleBinding + if tmp.Namespace == namespace { + key, err := cache.MetaNamespaceKeyFunc(&tmp) + if err != nil { + continue + } + if _, ok := items[key]; ok { + continue + } + items[key] = struct{}{} + res = append(res, &tmp) + } + } + } + return res, nil +} + +type ClusterRoleGetter struct { +} + +func (g *ClusterRoleGetter) GetClusterRole(name string) (*rbacv1.ClusterRole, error) { + rst, err := dao.QueryMeta("type", model.ResourceTypeSaAccess) + if err != nil { + return nil, err + } + for _, v := range *rst { + var saAccess policyv1alpha1.ServiceAccountAccess + err = json.Unmarshal([]byte(v), &saAccess) + if err != nil { + klog.Errorf("failed to unmarshal saAccess %v", err) + return nil, err + } + for _, rb := range saAccess.Spec.AccessRoleBinding { + if rb.RoleBinding.RoleRef.Kind == clusterRoleKind && rb.RoleBinding.RoleRef.Name == name { + return &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: rb.RoleBinding.RoleRef.Name, + }, + Rules: rb.Rules, + }, nil + } + } + for _, crb := range saAccess.Spec.AccessClusterRoleBinding { + if crb.ClusterRoleBinding.RoleRef.Kind == clusterRoleKind && crb.ClusterRoleBinding.RoleRef.Name == name { + return &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: crb.ClusterRoleBinding.RoleRef.Name, + }, + Rules: crb.Rules, + }, nil + } + } + } + return nil, fmt.Errorf("clusterrole %s not found", name) +} + +type ClusterRoleBindingLister struct { +} + +func (l *ClusterRoleBindingLister) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) { + rst, err := dao.QueryMeta("type", model.ResourceTypeSaAccess) + if err != nil { + klog.Errorf("failed to query meta %v", err) + return nil, err + } + var items = make(map[string]struct{}) + var res []*rbacv1.ClusterRoleBinding + for _, v := range *rst { + var saAccess policyv1alpha1.ServiceAccountAccess + err = json.Unmarshal([]byte(v), &saAccess) + if err != nil { + klog.Errorf("failed to unmarshal saAccess %v", err) + return nil, err + } + for _, crb := range saAccess.Spec.AccessClusterRoleBinding { + var tmp = crb.ClusterRoleBinding + key, err := cache.MetaNamespaceKeyFunc(&tmp) + if err != nil { + klog.Warningf("failed to get key for clusterrolebinding %v", err) + continue + } + if _, ok := items[key]; ok { + continue + } + items[key] = struct{}{} + res = append(res, &tmp) + } + } + return res, nil +} + +// getter implements ServiceAccountTokenGetter using a clientset.Interface +type getter struct { + Client clientset.Interface +} + +// NewGetterFromClient returns a ServiceAccountTokenGetter that +// uses the specified Client to retrieve service accounts and secrets. +// The Client should NOT authenticate using a service account token +// the returned getter will be used to retrieve, or recursion will result. +func NewGetterFromClient(client clientset.Interface) serviceaccount.ServiceAccountTokenGetter { + return getter{Client: client} +} + +func (c getter) GetServiceAccount(namespace, name string) (*corev1.ServiceAccount, error) { + return c.Client.CoreV1().ServiceAccounts(namespace).Get(context.Background(), name, metav1.GetOptions{}) +} + +func (c getter) GetPod(namespace, name string) (*corev1.Pod, error) { + return c.Client.CoreV1().Pods(namespace).Get(context.Background(), name, metav1.GetOptions{}) +} + +func (c getter) GetSecret(namespace, name string) (*corev1.Secret, error) { + return c.Client.CoreV1().Secrets(namespace).Get(context.Background(), name, metav1.GetOptions{}) +} diff --git a/edge/pkg/metamanager/constants/constants.go b/edge/pkg/metamanager/constants/constants.go new file mode 100644 index 000000000..d9a79a898 --- /dev/null +++ b/edge/pkg/metamanager/constants/constants.go @@ -0,0 +1,9 @@ +package constants + +const ( + TokenRequestIndexer = "tokenreq" + RolesIndexer = "roles" + ClusterRolesIndexer = "clusterroles" + RolebindingIndexer = "rolebinding" + ClusterRolebindingIndexer = "clusterrolebinding" +) diff --git a/edge/pkg/metamanager/metaserver/auth/auth.go b/edge/pkg/metamanager/metaserver/auth/auth.go new file mode 100644 index 000000000..c10ef7552 --- /dev/null +++ b/edge/pkg/metamanager/metaserver/auth/auth.go @@ -0,0 +1,125 @@ +package auth + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "gopkg.in/square/go-jose.v2/jwt" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/client-go/tools/cache" + "k8s.io/kubernetes/pkg/serviceaccount" + + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/client" +) + +type jwtTokenAuthenticator struct { + // TODO implement cache indexer to authenticate token + indexer cache.Indexer + issuers map[string]bool + keys []interface{} + validator serviceaccount.Validator + implicitAuds authenticator.Audiences +} + +func JWTTokenAuthenticator(indexer cache.Indexer, issuers []string, keys []interface{}, implicitAuds authenticator.Audiences, validator serviceaccount.Validator) authenticator.Token { + issuersMap := make(map[string]bool) + for _, issuer := range issuers { + issuersMap[issuer] = true + } + return &jwtTokenAuthenticator{ + indexer: indexer, + issuers: issuersMap, + keys: keys, + implicitAuds: implicitAuds, + validator: validator, + } +} + +func (j *jwtTokenAuthenticator) hasCorrectIssuer(tokenData string) bool { + parts := strings.Split(tokenData, ".") + if len(parts) != 3 { + return false + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return false + } + claims := struct { + // WARNING: this JWT is not verified. Do not trust these claims. + Issuer string `json:"iss"` + }{} + if err := json.Unmarshal(payload, &claims); err != nil { + return false + } + return j.issuers[claims.Issuer] +} + +func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData string) (*authenticator.Response, bool, error) { + if !j.hasCorrectIssuer(tokenData) { + return nil, false, nil + } + public := &jwt.Claims{} + private := j.validator.NewPrivateClaims() + if err := parseSigned(tokenData, public, private); err != nil { + return nil, false, err + } + if len(j.keys) == 0 { + // no public key for decode, auth token is existing in local db + if !client.CheckTokenExist(tokenData) { + return nil, false, fmt.Errorf("tokenData not found when authenticating") + } + } else { + tok, err := jwt.ParseSigned(tokenData) + if err != nil { + return nil, false, nil + } + var ( + found bool + errlist []error + ) + for _, key := range j.keys { + if err := tok.Claims(key, public, private); err != nil { + errlist = append(errlist, err) + continue + } + found = true + break + } + + if !found { + return nil, false, utilerrors.NewAggregate(errlist) + } + } + + tokenAudiences := authenticator.Audiences(public.Audience) + if len(tokenAudiences) == 0 { + tokenAudiences = j.implicitAuds + } + + requestedAudiences, ok := authenticator.AudiencesFrom(ctx) + if !ok { + // default to apiserver audiences + requestedAudiences = j.implicitAuds + } + + auds := authenticator.Audiences(tokenAudiences).Intersect(requestedAudiences) + if len(auds) == 0 && len(j.implicitAuds) != 0 { + return nil, false, fmt.Errorf("tokenData audiences %q is invalid for the target audiences %q", tokenAudiences, requestedAudiences) + } + + // If we get here, we have a tokenData with a recognized signature and + // issuer string. + sa, err := j.validator.Validate(ctx, tokenData, public, private) + if err != nil { + return nil, false, err + } + + return &authenticator.Response{ + User: sa.UserInfo(), + Audiences: auds, + }, true, nil +} diff --git a/edge/pkg/metamanager/metaserver/auth/auth_test.go b/edge/pkg/metamanager/metaserver/auth/auth_test.go new file mode 100644 index 000000000..50d4808b3 --- /dev/null +++ b/edge/pkg/metamanager/metaserver/auth/auth_test.go @@ -0,0 +1,418 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/golang/mock/gomock" + "gopkg.in/square/go-jose.v2" + authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/authenticator" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/util/keyutil" + "k8s.io/kubernetes/pkg/serviceaccount" + + "github.com/kubeedge/beehive/pkg/core/model" + "github.com/kubeedge/kubeedge/edge/mocks/beego" + "github.com/kubeedge/kubeedge/edge/pkg/common/dbm" + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/client" + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/dao" +) + +const otherPublicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArXz0QkIG1B5Bj2/W69GH +rsm5e+RC3kE+VTgocge0atqlLBek35tRqLgUi3AcIrBZ/0YctMSWDVcRt5fkhWwe +Lqjj6qvAyNyOkrkBi1NFDpJBjYJtuKHgRhNxXbOzTSNpdSKXTfOkzqv56MwHOP25 +yP/NNAODUtr92D5ySI5QX8RbXW+uDn+ixul286PBW/BCrE4tuS88dA0tYJPf8LCu +sqQOwlXYH/rNUg4Pyl9xxhR5DIJR0OzNNfChjw60zieRIt2LfM83fXhwk8IxRGkc +gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0 +PwIDAQAB +-----END PUBLIC KEY-----` + +const rsaPublicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx +OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI +zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG +51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU +j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi +B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM +WwIDAQAB +-----END PUBLIC KEY----- +` + +// Obtained by: +// +// 1. Serializing rsaPublicKey as DER +// 2. Taking the SHA256 of the DER bytes +// 3. URLSafe Base64-encoding the sha bytes +const rsaKeyID = "JHJehTTTZlsspKHT-GaJxK7Kd1NQgZJu3fyK6K_QDYU" + +// Fake value for testing. +const rsaPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26 +e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy +EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T +ctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh/AjjCnMnkgvKT2tpKxYQ59P +gDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp +1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRMWwIDAQABAoIBAHJx8GqyCBDNbqk7 +e7/hI9iE1S10Wwol5GH2RWxqX28cYMKq+8aE2LI1vPiXO89xOgelk4DN6urX6xjK +ZBF8RRIMQy/e/O2F4+3wl+Nl4vOXV1u6iVXMsD6JRg137mqJf1Fr9elg1bsaRofL +Q7CxPoB8dhS+Qb+hj0DhlqhgA9zG345CQCAds0ZYAZe8fP7bkwrLqZpMn7Dz9WVm +++YgYYKjuE95kPuup/LtWfA9rJyE/Fws8/jGvRSpVn1XglMLSMKhLd27sE8ZUSV0 +2KUzbfRGE0+AnRULRrjpYaPu0XQ2JjdNvtkjBnv27RB89W9Gklxq821eH1Y8got8 +FZodjxECgYEA93pz7AQZ2xDs67d1XLCzpX84GxKzttirmyj3OIlxgzVHjEMsvw8v +sjFiBU5xEEQDosrBdSknnlJqyiq1YwWG/WDckr13d8G2RQWoySN7JVmTQfXcLoTu +YGRiiTuoEi3ab3ZqrgGrFgX7T/cHuasbYvzCvhM2b4VIR3aSxU2DTUMCgYEA4x7J +T/ErP6GkU5nKstu/mIXwNzayEO1BJvPYsy7i7EsxTm3xe/b8/6cYOz5fvJLGH5mT +Q8YvuLqBcMwZardrYcwokD55UvNLOyfADDFZ6l3WntIqbA640Ok2g1X4U8J09xIq +ZLIWK1yWbbvi4QCeN5hvWq47e8sIj5QHjIIjRwkCgYEAyNqjltxFN9zmzPDa2d24 +EAvOt3pYTYBQ1t9KtqImdL0bUqV6fZ6PsWoPCgt+DBuHb+prVPGP7Bkr/uTmznU/ ++AlTO+12NsYLbr2HHagkXE31DEXE7CSLa8RNjN/UKtz4Ohq7vnowJvG35FCz/mb3 +FUHbtHTXa2+bGBUOTf/5Hw0CgYBxw0r9EwUhw1qnUYJ5op7OzFAtp+T7m4ul8kCa +SCL8TxGsgl+SQ34opE775dtYfoBk9a0RJqVit3D8yg71KFjOTNAIqHJm/Vyyjc+h +i9rJDSXiuczsAVfLtPVMRfS0J9QkqeG4PIfkQmVLI/CZ2ZBmsqEcX+eFs4ZfPLun +Qsxe2QKBgGuPilIbLeIBDIaPiUI0FwU8v2j8CEQBYvoQn34c95hVQsig/o5z7zlo +UsO0wlTngXKlWdOcCs1kqEhTLrstf48djDxAYAxkw40nzeJOt7q52ib/fvf4/UBy +X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ +-----END RSA PRIVATE KEY----- +` + +// openssl ecparam -name prime256v1 -genkey -noout -out ecdsa256.pem +// Fake value for testing. +const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 +AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 +/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== +-----END EC PRIVATE KEY-----` + +// openssl ec -in ecdsa256.pem -pubout -out ecdsa256pub.pem +const ecdsaPublicKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPL +X2i8uIp/C/ASqiIGUeeKQtX0/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== +-----END PUBLIC KEY-----` + +// Obtained by: +// +// 1. Serializing ecdsaPublicKey as DER +// 2. Taking the SHA256 of the DER bytes +// 3. URLSafe Base64-encoding the sha bytes +const ecdsaKeyID = "SoABiieYuNx4UdqYvZRVeuC6SihxgLrhLy9peHMHpTc" + +func getPrivateKey(data string) interface{} { + key, err := keyutil.ParsePrivateKeyPEM([]byte(data)) + if err != nil { + panic(fmt.Errorf("unexpected error parsing private key: %v", err)) + } + return key +} + +func getPublicKey(data string) interface{} { + keys, err := keyutil.ParsePublicKeysPEM([]byte(data)) + if err != nil { + panic(fmt.Errorf("unexpected error parsing public key: %v", err)) + } + return keys[0] +} +func TestTokenGenerateAndValidate(t *testing.T) { + expectedUserName := "system:serviceaccount:test:my-service-account" + expectedUserUID := "12345" + + // Related API objects + serviceAccount := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-service-account", + UID: "12345", + Namespace: "test", + }, + } + rsaSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-rsa-secret", + Namespace: "test", + }, + } + ecdsaSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-ecdsa-secret", + Namespace: "test", + }, + } + + // Generate the RSA token + rsaGenerator, err := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, getPrivateKey(rsaPrivateKey)) + if err != nil { + t.Fatalf("error making generator: %v", err) + } + rsaToken, err := rsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret)) + if err != nil { + t.Fatalf("error generating token: %v", err) + } + if len(rsaToken) == 0 { + t.Fatalf("no token generated") + } + rsaSecret.Data = map[string][]byte{ + "token": []byte(rsaToken), + } + + checkJSONWebSignatureHasKeyID(t, rsaToken, rsaKeyID) + + // Generate the ECDSA token + ecdsaGenerator, err := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, getPrivateKey(ecdsaPrivateKey)) + if err != nil { + t.Fatalf("error making generator: %v", err) + } + ecdsaToken, err := ecdsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *ecdsaSecret)) + if err != nil { + t.Fatalf("error generating token: %v", err) + } + if len(ecdsaToken) == 0 { + t.Fatalf("no token generated") + } + ecdsaSecret.Data = map[string][]byte{ + "token": []byte(ecdsaToken), + } + + checkJSONWebSignatureHasKeyID(t, ecdsaToken, ecdsaKeyID) + + // Generate signer with same keys as RSA signer but different unrecognized issuer + badIssuerGenerator, err := serviceaccount.JWTTokenGenerator("foo", getPrivateKey(rsaPrivateKey)) + if err != nil { + t.Fatalf("error making generator: %v", err) + } + badIssuerToken, err := badIssuerGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret)) + if err != nil { + t.Fatalf("error generating token: %v", err) + } + + // Generate signer with same keys as RSA signer but different recognized issuer + differentIssuerGenerator, err := serviceaccount.JWTTokenGenerator("bar", getPrivateKey(rsaPrivateKey)) + if err != nil { + t.Fatalf("error making generator: %v", err) + } + differentIssuerToken, err := differentIssuerGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret)) + if err != nil { + t.Fatalf("error generating token: %v", err) + } + + testCases := map[string]struct { + Client clientset.Interface + InitObjs []runtime.Object + Keys []interface{} + Token string + + ExpectedErr bool + ExpectedOK bool + ExpectedUserName string + ExpectedUserUID string + ExpectedGroups []string + }{ + "no keys and indexer": { + Token: rsaToken, + Client: nil, + Keys: []interface{}{}, + ExpectedErr: true, + ExpectedOK: false, + }, + "no keys but indexer": { + Token: rsaToken, + Client: nil, + InitObjs: []runtime.Object{&authenticationv1.TokenRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test", + }, + Status: authenticationv1.TokenRequestStatus{ + Token: rsaToken, + }, + }}, + Keys: []interface{}{}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "invalid keys (rsa)": { + Token: rsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(ecdsaPublicKey)}, + ExpectedErr: true, + ExpectedOK: false, + }, + "invalid keys (ecdsa)": { + Token: ecdsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(rsaPublicKey)}, + ExpectedErr: true, + ExpectedOK: false, + }, + "valid key (rsa)": { + Token: rsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "valid key, invalid issuer (rsa)": { + Token: badIssuerToken, + Client: nil, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: false, + }, + "valid key, different issuer (rsa)": { + Token: differentIssuerToken, + Client: nil, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "valid key (ecdsa)": { + Token: ecdsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(ecdsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "rotated keys (rsa)": { + Token: rsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(ecdsaPublicKey), getPublicKey(rsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "rotated keys (ecdsa)": { + Token: ecdsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(rsaPublicKey), getPublicKey(ecdsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "valid lookup": { + Token: rsaToken, + Client: fake.NewSimpleClientset(serviceAccount, rsaSecret, ecdsaSecret), + InitObjs: []runtime.Object{serviceAccount, rsaSecret, ecdsaSecret}, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "invalid secret lookup": { + Token: rsaToken, + Client: fake.NewSimpleClientset(serviceAccount), + InitObjs: []runtime.Object{serviceAccount}, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, + ExpectedErr: true, + ExpectedOK: false, + }, + "invalid serviceaccount lookup": { + Token: rsaToken, + Client: fake.NewSimpleClientset(rsaSecret, ecdsaSecret), + InitObjs: []runtime.Object{rsaSecret, ecdsaSecret}, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, + ExpectedErr: true, + ExpectedOK: false, + }, + } + + for k, tc := range testCases { + if len(tc.Keys) == 0 { + // ormerMock is mocked Ormer implementation + var ormerMock *beego.MockOrmer + // querySeterMock is mocked QuerySeter implementation + var querySeterMock *beego.MockQuerySeter + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ormerMock = beego.NewMockOrmer(mockCtrl) + querySeterMock = beego.NewMockQuerySeter(mockCtrl) + dbm.DBAccess = ormerMock + var fakeTr = new([]dao.Meta) + for _, v := range tc.InitObjs { + var tmp = new(dao.Meta) + content, _ := json.Marshal(v) + tmp.Type = model.ResourceTypeServiceAccountToken + tmp.Value = string(content) + tmp.Key = fmt.Sprintf("%s/%s/%s", "test", model.ResourceTypeServiceAccountToken, "test-name") + *fakeTr = append(*fakeTr, *tmp) + } + querySeterMock.EXPECT().All(gomock.Any()).SetArg(0, *fakeTr).Return(int64(1), nil).Times(1) + querySeterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySeterMock).Times(1) + ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySeterMock).Times(1) + } + auds := authenticator.Audiences{"api"} + authn := JWTTokenAuthenticator(nil, + []string{serviceaccount.LegacyIssuer, "bar"}, tc.Keys, auds, + serviceaccount.NewLegacyValidator(tc.Client != nil, client.NewGetterFromClient(tc.Client))) + + // An invalid, non-JWT token should always fail + ctx := authenticator.WithAudiences(context.Background(), auds) + if _, ok, err := authn.AuthenticateToken(ctx, "invalid token"); err != nil || ok { + t.Errorf("%s: Expected err=nil, ok=false for non-JWT token", k) + continue + } + + resp, ok, err := authn.AuthenticateToken(ctx, tc.Token) + if (err != nil) != tc.ExpectedErr { + t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err) + continue + } + + if ok != tc.ExpectedOK { + t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok) + continue + } + + if err != nil || !ok { + continue + } + + if resp.User.GetName() != tc.ExpectedUserName { + t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, resp.User.GetName()) + continue + } + if resp.User.GetUID() != tc.ExpectedUserUID { + t.Errorf("%s: Expected userUID=%v, got %v", k, tc.ExpectedUserUID, resp.User.GetUID()) + continue + } + if !reflect.DeepEqual(resp.User.GetGroups(), tc.ExpectedGroups) { + t.Errorf("%s: Expected groups=%v, got %v", k, tc.ExpectedGroups, resp.User.GetGroups()) + continue + } + } +} + +func checkJSONWebSignatureHasKeyID(t *testing.T, jwsString string, expectedKeyID string) { + jws, err := jose.ParseSigned(jwsString) + if err != nil { + t.Fatalf("Error checking for key ID: couldn't parse token: %v", err) + } + + if jws.Signatures[0].Header.KeyID != expectedKeyID { + t.Errorf("Token %q has the wrong KeyID (got %q, want %q)", jwsString, jws.Signatures[0].Header.KeyID, expectedKeyID) + } +} diff --git a/edge/pkg/metamanager/metaserver/auth/claims.go b/edge/pkg/metamanager/metaserver/auth/claims.go new file mode 100644 index 000000000..81ab32f84 --- /dev/null +++ b/edge/pkg/metamanager/metaserver/auth/claims.go @@ -0,0 +1,169 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" + apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/serviceaccount" +) + +// time.Now stubbed out to allow testing +var now = time.Now + +type privateClaims struct { + Kubernetes kubernetes `json:"kubernetes.io,omitempty"` +} + +type ref struct { + Name string `json:"name,omitempty"` + UID string `json:"uid,omitempty"` +} + +type kubernetes struct { + Namespace string `json:"namespace,omitempty"` + Svcacct ref `json:"serviceaccount,omitempty"` + Pod *ref `json:"pod,omitempty"` + Secret *ref `json:"secret,omitempty"` + WarnAfter jwt.NumericDate `json:"warnafter,omitempty"` +} + +type validator struct { + getter serviceaccount.ServiceAccountTokenGetter +} + +var _ = serviceaccount.Validator(&validator{}) + +func NewValidator(getter serviceaccount.ServiceAccountTokenGetter) serviceaccount.Validator { + return &validator{ + getter: getter, + } +} + +func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims, privateObj interface{}) (*apiserverserviceaccount.ServiceAccountInfo, error) { + private, ok := privateObj.(*privateClaims) + if !ok { + klog.Errorf("service account jwt validator expected private claim of type *privateClaims but got: %T", privateObj) + return nil, fmt.Errorf("service account token claims could not be validated due to unexpected private claim") + } + nowTime := now() + err := public.Validate(jwt.Expected{ + Time: nowTime, + }) + switch err { + case nil: + // successful validation + + case jwt.ErrExpired: + return nil, fmt.Errorf("service account token has expired") + + case jwt.ErrNotValidYet: + return nil, fmt.Errorf("service account token is not valid yet") + + // our current use of jwt.Expected above should make these cases impossible to hit + case jwt.ErrInvalidAudience, jwt.ErrInvalidID, jwt.ErrInvalidIssuer, jwt.ErrInvalidSubject: + klog.Errorf("service account token claim validation got unexpected validation failure: %v", err) + return nil, fmt.Errorf("service account token claims could not be validated: %w", err) // safe to pass these errors back to the user + + default: + klog.Errorf("service account token claim validation got unexpected error type: %T", err) // avoid leaking unexpected information into the logs + return nil, fmt.Errorf("service account token claims could not be validated due to unexpected validation error") // return an opaque error + } + + // consider things deleted prior to now()-leeway to be invalid + invalidIfDeletedBefore := nowTime.Add(-jwt.DefaultLeeway) + namespace := private.Kubernetes.Namespace + saref := private.Kubernetes.Svcacct + podref := private.Kubernetes.Pod + secref := private.Kubernetes.Secret + // Make sure service account still exists (name and UID) + serviceAccount, err := v.getter.GetServiceAccount(namespace, saref.Name) + if err != nil { + klog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, saref.Name, err) + return nil, err + } + if serviceAccount.DeletionTimestamp != nil && serviceAccount.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { + klog.V(4).Infof("Service account has been deleted %s/%s", namespace, saref.Name) + return nil, fmt.Errorf("service account %s/%s has been deleted", namespace, saref.Name) + } + if string(serviceAccount.UID) != saref.UID { + klog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, saref.Name, string(serviceAccount.UID), saref.UID) + return nil, fmt.Errorf("service account UID (%s) does not match claim (%s)", serviceAccount.UID, saref.UID) + } + + if secref != nil { + // Make sure token hasn't been invalidated by deletion of the secret + secret, err := v.getter.GetSecret(namespace, secref.Name) + if err != nil { + klog.V(4).Infof("Could not retrieve bound secret %s/%s for service account %s/%s: %v", namespace, secref.Name, namespace, saref.Name, err) + return nil, fmt.Errorf("service account token has been invalidated") + } + if secret.DeletionTimestamp != nil && secret.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { + klog.V(4).Infof("Bound secret is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secref.Name, namespace, saref.Name) + return nil, fmt.Errorf("service account token has been invalidated") + } + if secref.UID != string(secret.UID) { + klog.V(4).Infof("Secret UID no longer matches %s/%s: %q != %q", namespace, secref.Name, string(secret.UID), secref.UID) + return nil, fmt.Errorf("secret UID (%s) does not match service account secret ref claim (%s)", secret.UID, secref.UID) + } + } + + var podName, podUID string + if podref != nil { + // Make sure token hasn't been invalidated by deletion of the pod + pod, err := v.getter.GetPod(namespace, podref.Name) + if err != nil { + klog.V(4).Infof("Could not retrieve bound pod %s/%s for service account %s/%s: %v", namespace, podref.Name, namespace, saref.Name, err) + return nil, fmt.Errorf("service account token has been invalidated") + } + if pod.DeletionTimestamp != nil && pod.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { + klog.V(4).Infof("Bound pod is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, podref.Name, namespace, saref.Name) + return nil, fmt.Errorf("service account token has been invalidated") + } + if podref.UID != string(pod.UID) { + klog.V(4).Infof("Pod UID no longer matches %s/%s: %q != %q", namespace, podref.Name, string(pod.UID), podref.UID) + return nil, fmt.Errorf("pod UID (%s) does not match service account pod ref claim (%s)", pod.UID, podref.UID) + } + podName = podref.Name + podUID = podref.UID + } + + return &apiserverserviceaccount.ServiceAccountInfo{ + Namespace: private.Kubernetes.Namespace, + Name: private.Kubernetes.Svcacct.Name, + UID: private.Kubernetes.Svcacct.UID, + PodName: podName, + PodUID: podUID, + }, nil +} + +func (v *validator) NewPrivateClaims() interface{} { + return &privateClaims{} +} + +func parseSigned(tokenData string, dest ...interface{}) error { + sig, err := jose.ParseSigned(tokenData) + if err != nil { + klog.Errorf("failed to parse token: %v", err) + return err + } + claims := func(dest ...interface{}) error { + b := sig.UnsafePayloadWithoutVerification() + for _, d := range dest { + if err := json.Unmarshal(b, d); err != nil { + return err + } + } + return nil + } + if err := claims(dest...); err != nil { + klog.Errorf("failed to parse claims: %v", err) + return err + } + return nil +} diff --git a/edge/pkg/metamanager/metaserver/auth/claims_test.go b/edge/pkg/metamanager/metaserver/auth/claims_test.go new file mode 100644 index 000000000..55ed76309 --- /dev/null +++ b/edge/pkg/metamanager/metaserver/auth/claims_test.go @@ -0,0 +1,222 @@ +package auth + +import ( + "context" + "testing" + "time" + + "gopkg.in/square/go-jose.v2/jwt" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/serviceaccount" +) + +func init() { + now = func() time.Time { + // epoch time: 1514764800 + return time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC) + } +} + +type deletionTestCase struct { + name string + time *metav1.Time + expectErr bool +} + +type claimTestCase struct { + name string + getter serviceaccount.ServiceAccountTokenGetter + private *privateClaims + expiry jwt.NumericDate + notBefore jwt.NumericDate + expectErr string +} + +func TestValidatePrivateClaims(t *testing.T) { + var ( + nowUnix = int64(1514764800) + + serviceAccount = &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "saname", Namespace: "ns", UID: "sauid"}} + secret = &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secretname", Namespace: "ns", UID: "secretuid"}} + pod = &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "podname", Namespace: "ns", UID: "poduid"}} + ) + + deletionTestCases := []deletionTestCase{ + { + name: "valid", + time: nil, + }, + { + name: "deleted now", + time: &metav1.Time{Time: time.Unix(nowUnix, 0)}, + }, + { + name: "deleted near past", + time: &metav1.Time{Time: time.Unix(nowUnix-1, 0)}, + }, + { + name: "deleted near future", + time: &metav1.Time{Time: time.Unix(nowUnix+1, 0)}, + }, + { + name: "deleted now-leeway", + time: &metav1.Time{Time: time.Unix(nowUnix-60, 0)}, + }, + { + name: "deleted now-leeway-1", + time: &metav1.Time{Time: time.Unix(nowUnix-61, 0)}, + expectErr: true, + }, + } + + testcases := []claimTestCase{ + { + name: "good", + getter: fakeGetter{serviceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}}, + expectErr: "", + }, + { + name: "expired", + getter: fakeGetter{serviceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}}, + expiry: jwt.NewNumericDate(now().Add(-1_000 * time.Hour)), + expectErr: "service account token has expired", + }, + { + name: "not yet valid", + getter: fakeGetter{serviceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}}, + notBefore: jwt.NewNumericDate(now().Add(1_000 * time.Hour)), + expectErr: "service account token is not valid yet", + }, + { + name: "missing serviceaccount", + getter: fakeGetter{nil, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}}, + expectErr: `serviceaccounts "saname" not found`, + }, + { + name: "missing secret", + getter: fakeGetter{serviceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuid"}, Namespace: "ns"}}, + expectErr: "service account token has been invalidated", + }, + { + name: "missing pod", + getter: fakeGetter{serviceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduid"}, Namespace: "ns"}}, + expectErr: "service account token has been invalidated", + }, + { + name: "different uid serviceaccount", + getter: fakeGetter{serviceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauidold"}, Namespace: "ns"}}, + expectErr: "service account UID (sauid) does not match claim (sauidold)", + }, + { + name: "different uid secret", + getter: fakeGetter{serviceAccount, secret, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuidold"}, Namespace: "ns"}}, + expectErr: "secret UID (secretuid) does not match service account secret ref claim (secretuidold)", + }, + { + name: "different uid pod", + getter: fakeGetter{serviceAccount, nil, pod}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduidold"}, Namespace: "ns"}}, + expectErr: "pod UID (poduid) does not match service account pod ref claim (poduidold)", + }, + } + + for _, deletionTestCase := range deletionTestCases { + var ( + deletedServiceAccount = serviceAccount.DeepCopy() + deletedPod = pod.DeepCopy() + deletedSecret = secret.DeepCopy() + ) + deletedServiceAccount.DeletionTimestamp = deletionTestCase.time + deletedPod.DeletionTimestamp = deletionTestCase.time + deletedSecret.DeletionTimestamp = deletionTestCase.time + + var saDeletedErr, deletedErr string + if deletionTestCase.expectErr { + saDeletedErr = "service account ns/saname has been deleted" + deletedErr = "service account token has been invalidated" + } + + testcases = append(testcases, + claimTestCase{ + name: deletionTestCase.name + " serviceaccount", + getter: fakeGetter{deletedServiceAccount, nil, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}}, + expectErr: saDeletedErr, + }, + claimTestCase{ + name: deletionTestCase.name + " secret", + getter: fakeGetter{serviceAccount, deletedSecret, nil}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuid"}, Namespace: "ns"}}, + expectErr: deletedErr, + }, + claimTestCase{ + name: deletionTestCase.name + " pod", + getter: fakeGetter{serviceAccount, nil, deletedPod}, + private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduid"}, Namespace: "ns"}}, + expectErr: deletedErr, + }, + ) + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + v := &validator{tc.getter} + expiry := jwt.NumericDate(nowUnix) + if tc.expiry != 0 { + expiry = tc.expiry + } + _, err := v.Validate(context.Background(), "", &jwt.Claims{Expiry: expiry, NotBefore: tc.notBefore}, tc.private) + if len(tc.expectErr) > 0 { + if errStr := errString(err); tc.expectErr != errStr { + t.Fatalf("expected error %q but got %q", tc.expectErr, errStr) + } + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func errString(err error) string { + if err == nil { + return "" + } + + return err.Error() +} + +type fakeGetter struct { + serviceAccount *v1.ServiceAccount + secret *v1.Secret + pod *v1.Pod +} + +func (f fakeGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) { + if f.serviceAccount == nil { + return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "serviceaccounts"}, name) + } + return f.serviceAccount, nil +} +func (f fakeGetter) GetPod(namespace, name string) (*v1.Pod, error) { + if f.pod == nil { + return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, name) + } + return f.pod, nil +} +func (f fakeGetter) GetSecret(namespace, name string) (*v1.Secret, error) { + if f.secret == nil { + return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, name) + } + return f.secret, nil +} diff --git a/edge/pkg/metamanager/metaserver/config/config.go b/edge/pkg/metamanager/metaserver/config/config.go index 85f64678f..acfa5dda8 100644 --- a/edge/pkg/metamanager/metaserver/config/config.go +++ b/edge/pkg/metamanager/metaserver/config/config.go @@ -22,5 +22,8 @@ func InitConfigure(c *v1alpha2.MetaServer) { // so edgehub must register before metamanager NodeName: edgehubconfig.Config.NodeName, } + if len(Config.APIAudiences) == 0 && len(Config.ServiceAccountIssuers) != 0 { + Config.APIAudiences = Config.ServiceAccountIssuers + } }) } diff --git a/edge/pkg/metamanager/metaserver/kubernetes/storage/storage.go b/edge/pkg/metamanager/metaserver/kubernetes/storage/storage.go index ca4c9a8b4..e82964112 100644 --- a/edge/pkg/metamanager/metaserver/kubernetes/storage/storage.go +++ b/edge/pkg/metamanager/metaserver/kubernetes/storage/storage.go @@ -22,7 +22,6 @@ import ( "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/agent" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/kubernetes/storage/sqlite" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/kubernetes/storage/sqlite/imitator" - kefeatures "github.com/kubeedge/kubeedge/pkg/features" "github.com/kubeedge/kubeedge/pkg/metaserver" "github.com/kubeedge/kubeedge/pkg/metaserver/util" ) @@ -104,9 +103,8 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) return obj, nil }() - // If we get object from cloud failed and RequireAuthorization FeatureGate - // is not enabled, try to get the object from the local metaManager - if err != nil && !kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) { + // If we get object from cloud failed, try to get the object from the local metaManager + if err != nil { obj, err = r.Store.Get(ctx, "", options) // name is needless, we get all key information from ctx if err != nil { return nil, errors.NewNotFound(schema.GroupResource{Group: info.APIGroup, Resource: info.Resource}, info.Name) @@ -141,9 +139,8 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption return list, nil }() - // If we list object from cloud failed and RequireAuthorization FeatureGate - // is not enabled, try to list the object from the local metaManager - if err != nil && !kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) { + // If we list object from cloud failed, try to list the object from the local metaManager + if err != nil { list, err = r.Store.List(ctx, options) if err != nil { return nil, err @@ -178,11 +175,9 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio return nil, nil }() - // If we watch object from cloud failed and RequireAuthorization FeatureGate - // is enabled, just return the err - if err != nil && kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) { + // If we watch object from cloud failed, try to watch the object from the local metaManager + if err != nil { klog.Errorf("[metaserver/reststorage] failed to get a approved application for watch(%v) from cloud application center, %v", info.Path, err) - return nil, err } return r.Store.Watch(ctx, options) diff --git a/edge/pkg/metamanager/metaserver/server.go b/edge/pkg/metamanager/metaserver/server.go index 58f42de2f..cb4f2232e 100644 --- a/edge/pkg/metamanager/metaserver/server.go +++ b/edge/pkg/metamanager/metaserver/server.go @@ -16,18 +16,26 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/request/bearertoken" + "k8s.io/apiserver/pkg/authorization/authorizer" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/server" genericfilters "k8s.io/apiserver/pkg/server/filters" certutil "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" beehiveContext "github.com/kubeedge/beehive/pkg/core/context" - commontypes "github.com/kubeedge/kubeedge/common/types" "github.com/kubeedge/kubeedge/edge/pkg/common/modules" + "github.com/kubeedge/kubeedge/edge/pkg/edged/kubeclientbridge" "github.com/kubeedge/kubeedge/edge/pkg/edgehub" + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/client" + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/auth" metaserverconfig "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/config" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/handlerfactory" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/kubernetes/serializer" @@ -42,6 +50,35 @@ type MetaServer struct { Handler http.Handler NegotiatedSerializer runtime.NegotiatedSerializer Factory *handlerfactory.Factory + Auth *metaServerAuth +} + +type metaServerAuth struct { + Authenticator authenticator.Request + Authorizer authorizer.Authorizer +} + +func buildAuth() *metaServerAuth { + newAuthorizer := rbac.New( + &client.RoleGetter{}, + &client.RoleBindingLister{}, + &client.ClusterRoleGetter{}, + &client.ClusterRoleBindingLister{}) + + allPublicKeys := []interface{}{} + for _, keyfile := range metaserverconfig.Config.ServiceAccountKeyFiles { + publicKeys, err := keyutil.PublicKeysFromFile(keyfile) + if err != nil { + klog.Errorf("Failed to load public key file %s: %v", keyfile, err) + return nil + } + allPublicKeys = append(allPublicKeys, publicKeys...) + } + tokenAuthenticator := auth.JWTTokenAuthenticator(nil, + metaserverconfig.Config.ServiceAccountIssuers, allPublicKeys, metaserverconfig.Config.APIAudiences, + auth.NewValidator(client.NewGetterFromClient(kubeclientbridge.NewSimpleClientset(client.New())))) + newAuthenticator := bearertoken.New(tokenAuthenticator) + return &metaServerAuth{newAuthenticator, newAuthorizer} } func NewMetaServer() *MetaServer { @@ -50,6 +87,7 @@ func NewMetaServer() *MetaServer { LongRunningFunc: genericfilters.BasicLongRunningRequestCheck(sets.NewString("watch"), sets.NewString()), NegotiatedSerializer: serializer.NewNegotiatedSerializer(), Factory: handlerfactory.NewFactory(), + Auth: buildAuth(), } return &ls } @@ -206,18 +244,13 @@ func BuildHandlerChain(handler http.Handler, ls *MetaServer) http.Handler { cfg := &server.Config{ LegacyAPIGroupPrefixes: sets.NewString(server.DefaultLegacyAPIPrefix), } - + if kefeatures.DefaultFeatureGate.Enabled(kefeatures.RequireAuthorization) { + handler = genericapifilters.WithAuthorization(handler, ls.Auth.Authorizer, legacyscheme.Codecs) + failedHandler := genericapifilters.Unauthorized(legacyscheme.Codecs) + handler = genericapifilters.WithAuthentication(handler, ls.Auth.Authenticator, failedHandler, metaserverconfig.Config.APIAudiences) + } handler = genericfilters.WithWaitGroup(handler, ls.LongRunningFunc, ls.HandlerChainWaitGroup) handler = genericapifilters.WithRequestInfo(handler, server.NewRequestInfoResolver(cfg)) handler = genericfilters.WithPanicRecovery(handler, &apirequest.RequestInfoFactory{}) - handler = WithAuthorizationHeader(handler) return handler } - -func WithAuthorizationHeader(handler http.Handler) http.Handler { - return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - token := request.Header.Get(commontypes.AuthorizationKey) - request = request.WithContext(context.WithValue(request.Context(), commontypes.AuthorizationKey, token)) - handler.ServeHTTP(writer, request) - }) -} diff --git a/edge/pkg/metamanager/process.go b/edge/pkg/metamanager/process.go index 0f347cd4e..e9aed6d36 100644 --- a/edge/pkg/metamanager/process.go +++ b/edge/pkg/metamanager/process.go @@ -16,6 +16,7 @@ import ( "github.com/kubeedge/kubeedge/common/constants" connect "github.com/kubeedge/kubeedge/edge/pkg/common/cloudconnection" "github.com/kubeedge/kubeedge/edge/pkg/common/modules" + "github.com/kubeedge/kubeedge/edge/pkg/metamanager/client" metaManagerConfig "github.com/kubeedge/kubeedge/edge/pkg/metamanager/config" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/dao" "github.com/kubeedge/kubeedge/edge/pkg/metamanager/metaserver/kubernetes/storage/sqlite/imitator" @@ -23,19 +24,14 @@ import ( // Constants to check metamanager processes const ( - OK = "OK" - - GroupResource = "resource" - + OK = "OK" + GroupResource = "resource" CloudControllerModel = "edgecontroller" + errNotConnected = "not connected" ) -func feedbackError(err error, info string, request model.Message) { - errInfo := "Something wrong" - if err != nil { - errInfo = fmt.Sprintf(info+": %v", err) - } - errResponse := model.NewErrorMessage(&request, errInfo).SetRoute(modules.MetaManagerModuleName, request.GetGroup()) +func feedbackError(err error, request model.Message) { + errResponse := model.NewErrorMessage(&request, err.Error()).SetRoute(modules.MetaManagerModuleName, request.GetGroup()) if request.GetSource() == modules.EdgedModuleName { sendToEdged(errResponse, request.IsSync()) } else { @@ -64,7 +60,8 @@ func sendToCloud(message *model.Message) { // Resource format: <namespace>/<restype>[/resid] // return <reskey, restype, resid> -func parseResource(resource string) (string, string, string) { +func parseResource(message *model.Message) (string, string, string) { + resource := message.GetResource() tokens := strings.Split(resource, constants.ResourceSep) resType := "" resID := "" @@ -76,7 +73,26 @@ func parseResource(resource string) (string, string, string) { resID = tokens[len(tokens)-1] default: } - return resource, resType, resID + if resType != model.ResourceTypeServiceAccountToken { + return resource, resType, resID + } + var tokenReq authenticationv1.TokenRequest + content, err := message.GetContentData() + if err != nil { + klog.Errorf("failed to get token request from message %s, error %s", message.GetID(), err) + return "", "", "" + } + if err = json.Unmarshal(content, &tokenReq); err != nil { + klog.Errorf("failed to unmarshal token request from message %s, error %s", message.GetID(), err) + return "", "", "" + } + + trTokens := strings.Split(resource, constants.ResourceSep) + if len(trTokens) != 3 { + klog.Errorf("failed to get resource %s name and namespace", resource) + return "", "", "" + } + return client.KeyFunc(trTokens[2], trTokens[0], &tokenReq), resType, "" } // is resource type require remote query @@ -95,38 +111,63 @@ func msgDebugInfo(message *model.Message) string { return fmt.Sprintf("msgID[%s] resource[%s]", message.GetID(), message.GetResource()) } -func (m *metaManager) processInsert(message model.Message) { - content, err := message.GetContentData() - if err != nil { - klog.Errorf("get insert message content data failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to get insert message content data", message) - return +func (m *metaManager) handleMessage(message *model.Message) error { + resKey, resType, _ := parseResource(message) + switch message.GetOperation() { + case model.InsertOperation, model.UpdateOperation, model.PatchOperation, model.ResponseOperation: + content, err := message.GetContentData() + if err != nil { + klog.Errorf("get message content data failed, message: %s, error: %s", msgDebugInfo(message), err) + return fmt.Errorf("get message content data failed, error: %s", err) + } + meta := &dao.Meta{ + Key: resKey, + Type: resType, + Value: string(content)} + err = dao.InsertOrUpdate(meta) + if err != nil { + klog.Errorf("insert or update meta failed, message: %s, error: %v", msgDebugInfo(message), err) + return fmt.Errorf("insert or update meta failed, %s", err) + } + case model.DeleteOperation: + if resType == model.ResourceTypePod { + err := processDeletePodDB(*message) + if err != nil { + klog.Errorf("delete pod meta failed, message %s, err: %v", msgDebugInfo(message), err) + return fmt.Errorf("failed to delete pod meta to DB: %s", err) + } + } else { + err := dao.DeleteMetaByKey(resKey) + if err != nil { + klog.Errorf("delete meta failed, %s", msgDebugInfo(message)) + return fmt.Errorf("delete meta failed, %s", err) + } + } } + return nil +} +func (m *metaManager) processInsert(message model.Message) { imitator.DefaultV2Client.Inject(message) - resKey, resType, _ := parseResource(message.GetResource()) - meta := &dao.Meta{ - Key: resKey, - Type: resType, - Value: string(content)} - err = dao.SaveMeta(meta) - if err != nil { - klog.Errorf("save meta failed, %s: %v", msgDebugInfo(&message), err) - feedbackError(err, "Error to save meta to DB", message) + msgSource := message.GetSource() + if msgSource == modules.EdgedModuleName { + if !connect.IsConnected() { + klog.Warningf("process remote failed, req[%s], err: %v", msgDebugInfo(&message), errNotConnected) + feedbackError(fmt.Errorf("failed to process remote: %s", errNotConnected), message) + return + } + m.processRemote(message) return } - - if (resType == model.ResourceTypeNode || resType == model.ResourceTypeLease) && message.GetSource() == modules.EdgedModuleName { - sendToCloud(&message) + if err := m.handleMessage(&message); err != nil { + feedbackError(err, message) return } - - msgSource := message.GetSource() if msgSource == cloudmodules.DeviceControllerModuleName { message.SetRoute(modules.MetaGroup, modules.DeviceTwinModuleName) beehiveContext.Send(modules.DeviceTwinModuleName, message) - } else { + } else if msgSource != cloudmodules.PolicyControllerModuleName { // Notify edged sendToEdged(&message, false) } @@ -136,38 +177,30 @@ func (m *metaManager) processInsert(message model.Message) { } func (m *metaManager) processUpdate(message model.Message) { - content, err := message.GetContentData() - if err != nil { - klog.Errorf("get update message content data failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to get update message content data", message) - return - } - imitator.DefaultV2Client.Inject(message) - resKey, resType, _ := parseResource(message.GetResource()) - - meta := &dao.Meta{ - Key: resKey, - Type: resType, - Value: string(content)} - err = dao.InsertOrUpdate(meta) - if err != nil { - klog.Errorf("update meta failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to update meta to DB", message) + msgSource := message.GetSource() + _, resType, _ := parseResource(&message) + if msgSource == modules.EdgedModuleName && resType == model.ResourceTypeLease { + if !connect.IsConnected() { + klog.Warningf("process remote failed, req[%s], err: %v", msgDebugInfo(&message), errNotConnected) + feedbackError(fmt.Errorf("failed to process remote: %s", errNotConnected), message) + return + } + m.processRemote(message) + return + } + if err := m.handleMessage(&message); err != nil { + feedbackError(err, message) return } - - msgSource := message.GetSource() switch msgSource { case modules.EdgedModuleName: - sendToCloud(&message) // For pod status update message, we need to wait for the response message // to ensure that the pod status is correctly reported to the kube-apiserver - if resType != model.ResourceTypePodStatus && resType != model.ResourceTypeLease { - resp := message.NewRespByMessage(&message, OK) - sendToEdged(resp, message.IsSync()) - } + sendToCloud(&message) + resp := message.NewRespByMessage(&message, OK) + sendToEdged(resp, message.IsSync()) case cloudmodules.EdgeControllerModuleName, cloudmodules.DynamicControllerModuleName: sendToEdged(&message, message.IsSync()) resp := message.NewRespByMessage(&message, OK) @@ -175,59 +208,29 @@ func (m *metaManager) processUpdate(message model.Message) { case cloudmodules.DeviceControllerModuleName: resp := message.NewRespByMessage(&message, OK) sendToCloud(resp) - message.SetRoute(modules.MetaGroup, modules.DeviceTwinModuleName) beehiveContext.Send(modules.DeviceTwinModuleName, message) - + case cloudmodules.PolicyControllerModuleName: + resp := message.NewRespByMessage(&message, OK) + sendToCloud(resp) default: klog.Errorf("unsupport message source, %s", msgSource) } } func (m *metaManager) processPatch(message model.Message) { - content, err := message.GetContentData() - if err != nil { - klog.Errorf("get patch message content data failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to get update message content data", message) - return - } - - resKey, resType, _ := parseResource(message.GetResource()) - - meta := &dao.Meta{ - Key: resKey, - Type: resType, - Value: string(content)} - err = dao.InsertOrUpdate(meta) - if err != nil { - klog.Errorf("update meta failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to update meta to DB", message) + if err := m.handleMessage(&message); err != nil { + feedbackError(err, message) return } - sendToCloud(&message) } func (m *metaManager) processResponse(message model.Message) { - content, err := message.GetContentData() - if err != nil { - klog.Errorf("get response message content data failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to get response message content data", message) - return - } - - resKey, resType, _ := parseResource(message.GetResource()) - meta := &dao.Meta{ - Key: resKey, - Type: resType, - Value: string(content)} - err = dao.InsertOrUpdate(meta) - if err != nil { - klog.Errorf("update meta failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to update meta to DB", message) + if err := m.handleMessage(&message); err != nil { + feedbackError(err, message) return } - // Notify edged if the data is coming from cloud if message.GetSource() == CloudControllerModel { sendToEdged(&message, message.IsSync()) @@ -239,38 +242,27 @@ func (m *metaManager) processResponse(message model.Message) { func (m *metaManager) processDelete(message model.Message) { imitator.DefaultV2Client.Inject(message) - _, resType, _ := parseResource(message.GetResource()) - + _, resType, _ := parseResource(&message) if resType == model.ResourceTypePod && message.GetSource() == modules.EdgedModuleName { + // if pod is deleted in K8s, then a new delete message will be sent to edge sendToCloud(&message) return } - var err error - if resType == model.ResourceTypePod { - err = processDeletePodDB(message) - if err != nil { - klog.Errorf("delete pod meta failed, %s, err:%v", msgDebugInfo(&message), err) - feedbackError(err, "Error to delete pod meta to DB", message) - return - } - } else { - err = dao.DeleteMetaByKey(message.GetResource()) - if err != nil { - klog.Errorf("delete meta failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to delete meta to DB", message) - return - } + if err := m.handleMessage(&message); err != nil { + feedbackError(err, message) + return } - msgSource := message.GetSource() if msgSource == cloudmodules.DeviceControllerModuleName { message.SetRoute(modules.MetaGroup, modules.DeviceTwinModuleName) beehiveContext.Send(modules.DeviceTwinModuleName, message) } - // Notify edged - sendToEdged(&message, false) + if msgSource != cloudmodules.PolicyControllerModuleName { + // Notify edged + sendToEdged(&message, false) + } resp := message.NewRespByMessage(&message, OK) sendToCloud(resp) } @@ -314,14 +306,6 @@ func processDeletePodDB(message model.Message) error { return err } - podStatusKey := strings.Replace(message.GetResource(), - constants.ResourceSep+model.ResourceTypePod+constants.ResourceSep, - constants.ResourceSep+model.ResourceTypePodStatus+constants.ResourceSep, 1) - err = dao.DeleteMetaByKey(podStatusKey) - if err != nil { - return err - } - podPatchKey := strings.Replace(message.GetResource(), constants.ResourceSep+model.ResourceTypePod+constants.ResourceSep, constants.ResourceSep+model.ResourceTypePodPatch+constants.ResourceSep, 1) @@ -333,55 +317,12 @@ func processDeletePodDB(message model.Message) error { return nil } -// KeyFunc keys should be nonconfidential and safe to log -func KeyFunc(name, namespace string, tr *authenticationv1.TokenRequest) string { - var exp int64 - if tr.Spec.ExpirationSeconds != nil { - exp = *tr.Spec.ExpirationSeconds - } - - var ref authenticationv1.BoundObjectReference - if tr.Spec.BoundObjectRef != nil { - ref = *tr.Spec.BoundObjectRef - } - - return fmt.Sprintf("%q/%q/%#v/%#v/%#v", name, namespace, tr.Spec.Audiences, exp, ref) -} - -// getSpecialResourceKey get service account db key -func getSpecialResourceKey(resType, resKey string, message model.Message) (string, error) { - if resType != model.ResourceTypeServiceAccountToken { - return resKey, nil - } - tokenReq, ok := message.GetContent().(*authenticationv1.TokenRequest) - if !ok { - return "", fmt.Errorf("failed to get resource %s name and namespace", resKey) - } - tokens := strings.Split(resKey, constants.ResourceSep) - if len(tokens) != 3 { - return "", fmt.Errorf("failed to get resource %s name and namespace", resKey) - } - return KeyFunc(tokens[2], tokens[0], tokenReq), nil -} - func (m *metaManager) processQuery(message model.Message) { - resKey, resType, resID := parseResource(message.GetResource()) + resKey, resType, resID := parseResource(&message) var metas *[]string var err error if requireRemoteQuery(resType) && connect.IsConnected() { - resKey, err = getSpecialResourceKey(resType, resKey, message) - if err != nil { - klog.Errorf("failed to get special resource %s key", resKey) - return - } - metas, err = dao.QueryMeta("key", resKey) - if err != nil || len(*metas) == 0 || resType == model.ResourceTypeNode || resType == constants.ResourceTypeVolumeAttachment || resType == model.ResourceTypeLease { - m.processRemoteQuery(message) - } else { - resp := message.NewRespByMessage(&message, *metas) - resp.SetRoute(modules.MetaManagerModuleName, resp.GetGroup()) - sendToEdged(resp, message.IsSync()) - } + m.processRemote(message) return } @@ -393,7 +334,7 @@ func (m *metaManager) processQuery(message model.Message) { } if err != nil { klog.Errorf("query meta failed, %s", msgDebugInfo(&message)) - feedbackError(err, "Error to query meta in DB", message) + feedbackError(fmt.Errorf("failed to query meta in DB: %s", err), message) } else { resp := message.NewRespByMessage(&message, *metas) resp.SetRoute(modules.MetaManagerModuleName, resp.GetGroup()) @@ -401,7 +342,7 @@ func (m *metaManager) processQuery(message model.Message) { } } -func (m *metaManager) processRemoteQuery(message model.Message) { +func (m *metaManager) processRemote(message model.Message) { go func() { // TODO: retry originalID := message.GetID() @@ -411,43 +352,50 @@ func (m *metaManager) processRemoteQuery(message model.Message) { message, time.Duration(metaManagerConfig.Config.RemoteQueryTimeout)*time.Second) if err != nil { - klog.Errorf("remote query failed, req[%s], err: %v", msgDebugInfo(&message), err) - feedbackError(err, "Error to query meta in DB", message) + klog.Errorf("process remote failed, req[%s], err: %v", msgDebugInfo(&message), err) + feedbackError(fmt.Errorf("failed to process remote: %s", err), message) + return + } + klog.V(4).Infof("process remote: req[%s], resp[%s]", msgDebugInfo(&message), msgDebugInfo(&resp)) + content, ok := resp.GetContent().(string) + if ok && content == constants.MessageSuccessfulContent { + klog.V(4).Infof("process remote successfully") + feedbackResponse(&message, originalID, &resp) return } errContent, ok := resp.GetContent().(error) if ok { - klog.V(4).Infof("process remote query err: %v", errContent) + klog.V(4).Infof("process remote err: %v", errContent) feedbackResponse(&message, originalID, &resp) return } - klog.V(4).Infof("process remote query: req[%s], resp[%s]", msgDebugInfo(&message), msgDebugInfo(&resp)) - content, err := resp.GetContentData() - if err != nil { - klog.Errorf("get remote query response content data failed, %s", msgDebugInfo(&resp)) - feedbackError(err, "Error to get remote query response message content data", message) - return + mapContent, ok := resp.GetContent().(map[string]interface{}) + if ok && isObjectResp(mapContent) { + if mapContent["Err"] != nil { + klog.V(4).Infof("process remote objResp err: %v", mapContent["Err"]) + feedbackResponse(&message, originalID, &resp) + return + } + klog.V(4).Infof("process remote objResp: %+v", mapContent["Object"]) + resp.Content = mapContent["Object"] } - - resKey, resType, _ := parseResource(message.GetResource()) - resKey, err = getSpecialResourceKey(resType, resKey, message) - if err != nil { - klog.Errorf("get remote query response content data failed, %s", msgDebugInfo(&resp)) - feedbackError(err, "Error to get remote query response message content data", message) + if err := m.handleMessage(&resp); err != nil { + feedbackError(err, message) return } - meta := &dao.Meta{ - Key: resKey, - Type: resType, - Value: string(content)} - err = dao.InsertOrUpdate(meta) - if err != nil { - klog.Errorf("update meta failed, %s", msgDebugInfo(&resp)) - } feedbackResponse(&message, originalID, &resp) }() } +func isObjectResp(data map[string]interface{}) bool { + _, ok := data["Object"] + if !ok { + return false + } + _, ok = data["Err"] + return ok +} + func (m *metaManager) processVolume(message model.Message) { klog.Info("process volume started") back, err := beehiveContext.SendSync(modules.EdgedModuleName, message, constants.CSISyncMsgRespTimeout) diff --git a/edge/pkg/metamanager/process_test.go b/edge/pkg/metamanager/process_test.go index 59e8a6d95..2bdaf1eea 100644 --- a/edge/pkg/metamanager/process_test.go +++ b/edge/pkg/metamanager/process_test.go @@ -23,11 +23,15 @@ import ( "testing" "github.com/golang/mock/gomock" + coordinationv1 "k8s.io/api/coordination/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kubeedge/beehive/pkg/common" "github.com/kubeedge/beehive/pkg/core" beehiveContext "github.com/kubeedge/beehive/pkg/core/context" "github.com/kubeedge/beehive/pkg/core/model" + "github.com/kubeedge/kubeedge/common/constants" "github.com/kubeedge/kubeedge/edge/mocks/beego" connect "github.com/kubeedge/kubeedge/edge/pkg/common/cloudconnection" "github.com/kubeedge/kubeedge/edge/pkg/common/dbm" @@ -47,8 +51,8 @@ const ( ModuleNameEdgeHub = "websocket" // ModuleNameController is the name of the controller module ModuleNameController = "edgecontroller" - // OperationNodeConnection is message with operation publish - OperationNodeConnection = "publish" + marshalError = "get message content data failed, error: marshal message content failed: json: unsupported type: chan int" + testHolderIdentity = "edgehub" ) // errFailedDBOperation is common Database operation fail error @@ -71,6 +75,7 @@ func TestProcessInsert(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() ormerMock := beego.NewMockOrmer(mockCtrl) + rawSetterMock := beego.NewMockRawSeter(mockCtrl) dbm.DBAccess = ormerMock meta := newMetaManager(true) core.Register(meta) @@ -92,54 +97,119 @@ func TestProcessInsert(t *testing.T) { ModuleType: common.MsgCtxTypeChannel, } beehiveContext.AddModule(edged) + connect.SetConnected(true) //SaveMeta Failed, feedbackError SendToCloud - ormerMock.EXPECT().Insert(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) - msg := model.NewMessage("").BuildRouter(modules.MetaManagerModuleName, GroupResource, model.ResourceTypePodStatus, model.InsertOperation) + rawSetterMock.EXPECT().Exec().Return(nil, errFailedDBOperation).Times(1) + ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) + + msg := model.NewMessage("").BuildRouter(modules.MetaManagerModuleName, GroupResource, model.ResourceTypeConfigmap, model.InsertOperation) meta.processInsert(*msg) - //beehiveContext.Send(MetaManagerModuleName, *msg) message, err := beehiveContext.Receive(ModuleNameEdgeHub) - t.Run("EdgeHubChannelRegistration", func(t *testing.T) { + t.Run("insert error", func(t *testing.T) { if err != nil { t.Errorf("EdgeHub Channel not found: %v", err) return } - want := "Error to save meta to DB: " + FailedDBOperation + want := "insert or update meta failed, " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) } }) //SaveMeta Failed, feedbackError SendToEdged and 2 resources - ormerMock.EXPECT().Insert(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus+"/secondRes", model.InsertOperation) + ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) + rawSetterMock.EXPECT().Exec().Return(nil, errFailedDBOperation).Times(1) + + resource := fmt.Sprintf("%s/%s/%s", "default", model.ResourceTypeLease, "leaseName") + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, resource, model.InsertOperation) meta.processInsert(*msg) + message, err = beehiveContext.Receive(ModuleNameEdgeHub) + if err != nil { + t.Errorf("EdgeHub Channel not found: %v", err) + return + } + message.Header.ParentID = message.GetID() + message.Content = &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "leaseName", + Namespace: "default", + }, + } + beehiveContext.SendResp(message) message, err = beehiveContext.Receive(ModuleNameEdged) - t.Run("ErrorMessageToEdged", func(t *testing.T) { + t.Run("Insert failed", func(t *testing.T) { if err != nil { t.Errorf("EdgeD Channel not found: %v", err) return } - want := "Error to save meta to DB: " + FailedDBOperation + want := "insert or update meta failed, " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) } }) + //SaveMeta Failed, feedbackError SendToEdged and 2 resources + ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) + rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) + + resource = fmt.Sprintf("%s/%s/%s", "default", model.ResourceTypeLease, "leaseName") + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, resource, model.InsertOperation) + meta.processInsert(*msg) + message, err = beehiveContext.Receive(ModuleNameEdgeHub) + if err != nil { + t.Errorf("EdgeHub Channel not found: %v", err) + return + } + message.Header.ParentID = message.GetID() + message.Content = &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "leaseName", + Namespace: "default", + }, + } + beehiveContext.SendResp(message) + message, err = beehiveContext.Receive(ModuleNameEdged) + beehiveContext.Receive(ModuleNameEdgeHub) + t.Run("Insert successfully", func(t *testing.T) { + if err != nil { + t.Errorf("EdgeD Channel not found: %v", err) + return + } + leaseBytes, _ := message.GetContentData() + lease := &coordinationv1.Lease{} + err = json.Unmarshal(leaseBytes, lease) + if err != nil { + t.Errorf("Unmarshal lease failed: %v", err) + return + } + if lease.Name != "leaseName" || lease.Namespace != "default" { + t.Errorf("Wrong lease received : Wanted leaseName and Got %v", lease.Name) + } + }) + //jsonMarshall fail - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.InsertOperation).FillBody(make(chan int)) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypeLease, model.InsertOperation).FillBody(make(chan int)) meta.processInsert(*msg) - message, _ = beehiveContext.Receive(ModuleNameEdged) + message, err = beehiveContext.Receive(ModuleNameEdgeHub) + if err != nil { + t.Errorf("EdgeHub Channel not found: %v", err) + return + } + message.Header.ParentID = message.GetID() + beehiveContext.SendResp(message) + message, err = beehiveContext.Receive(ModuleNameEdged) t.Run("MarshallFail", func(t *testing.T) { - want := "Error to get insert message content data: marshal message content failed: json: unsupported type: chan int" - if message.GetContent() != want { - t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) + if message.GetContent() != marshalError { + t.Errorf("Wrong Error message received : Wanted %v and Got %v", marshalError, message.GetContent()) } }) //Successful Case and 3 resources - ormerMock.EXPECT().Insert(gomock.Any()).Return(int64(1), nil).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus+"/secondRes"+"/thirdRes", model.InsertOperation) + ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) + rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) + + msg = model.NewMessage("").BuildRouter(ModuleNameController, GroupResource, model.ResourceTypeLease+"/secondRes"+"/thirdRes", model.InsertOperation) meta.processInsert(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("InsertMessageToEdged", func(t *testing.T) { @@ -184,26 +254,29 @@ func TestProcessUpdate(t *testing.T) { ModuleType: common.MsgCtxTypeChannel, } beehiveContext.AddModule(edged) + connect.SetConnected(true) //jsonMarshall fail - msg := model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.UpdateOperation).FillBody(make(chan int)) + msg := model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, "namespace/"+model.ResourceTypeLease+"/id", model.UpdateOperation).FillBody(make(chan int)) meta.processUpdate(*msg) - message, _ := beehiveContext.Receive(ModuleNameEdged) + message, _ := beehiveContext.Receive(ModuleNameEdgeHub) + message.Header.ParentID = message.GetID() + beehiveContext.SendResp(message) + message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("MarshallFail", func(t *testing.T) { - want := "Error to get update message content data: marshal message content failed: json: unsupported type: chan int" - if message.GetContent() != want { - t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) + if message.GetContent() != marshalError { + t.Errorf("Wrong Error message received : Wanted %v and Got %v", marshalError, message.GetContent()) } }) //Database save error rawSetterMock.EXPECT().Exec().Return(nil, errFailedDBOperation).Times(1) ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.UpdateOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypeNode, model.UpdateOperation) meta.processUpdate(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("DatabaseSaveError", func(t *testing.T) { - want := "Error to update meta to DB: " + FailedDBOperation + want := "insert or update meta failed, " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) } @@ -212,7 +285,7 @@ func TestProcessUpdate(t *testing.T) { //Success Case Source Edged, sync = true ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.UpdateOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypeLease, model.UpdateOperation) meta.processUpdate(*msg) edgehubMsg, _ := beehiveContext.Receive(ModuleNameEdgeHub) t.Run("SuccessSourceEdgedReceiveEdgehub", func(t *testing.T) { @@ -232,7 +305,7 @@ func TestProcessUpdate(t *testing.T) { //Success Case Source CloudControllerModel ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) - msg = model.NewMessage("").BuildRouter(CloudControllerModel, GroupResource, model.ResourceTypePodStatus, model.UpdateOperation) + msg = model.NewMessage("").BuildRouter(CloudControllerModel, GroupResource, model.ResourceTypeLease, model.UpdateOperation) meta.processUpdate(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("SuccessSend[CloudController->Edged]", func(t *testing.T) { @@ -248,6 +321,90 @@ func TestProcessUpdate(t *testing.T) { t.Errorf("Wrong message received : Wanted %v and Got %v", want, message.GetContent()) } }) + + ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) + rawSetterMock.EXPECT().Exec().Return(nil, errFailedDBOperation).Times(1) + resource := fmt.Sprintf("%s/%s/%s", "default", model.ResourceTypeLease, "leaseName2") + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, resource, model.UpdateOperation) + meta.processUpdate(*msg) + message, err := beehiveContext.Receive(ModuleNameEdgeHub) + if err != nil { + t.Errorf("EdgeHub Channel not found: %v", err) + return + } + message.Header.ParentID = message.GetID() + holderIdentity := testHolderIdentity + message.Content = &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "leaseName2", + Namespace: "default", + }, + Spec: coordinationv1.LeaseSpec{ + HolderIdentity: &holderIdentity, + }, + } + beehiveContext.SendResp(message) + message, err = beehiveContext.Receive(ModuleNameEdged) + t.Run("Update failed", func(t *testing.T) { + if err != nil { + t.Errorf("EdgeD Channel not found: %v", err) + return + } + want := "insert or update meta failed, " + FailedDBOperation + if message.GetContent() != want { + t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) + } + }) + + ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) + rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) + resource = fmt.Sprintf("%s/%s/%s", "default", model.ResourceTypeLease, "leaseName2") + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, resource, model.UpdateOperation) + meta.processUpdate(*msg) + message, err = beehiveContext.Receive(ModuleNameEdgeHub) + if err != nil { + t.Errorf("EdgeHub Channel not found: %v", err) + return + } + message.Header.ParentID = message.GetID() + holderIdentity = testHolderIdentity + message.Content = &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "leaseName2", + Namespace: "default", + }, + Spec: coordinationv1.LeaseSpec{ + HolderIdentity: &holderIdentity, + }, + } + beehiveContext.SendResp(message) + message, err = beehiveContext.Receive(ModuleNameEdged) + t.Run("Update successfully", func(t *testing.T) { + if err != nil { + t.Errorf("EdgeD Channel not found: %v", err) + return + } + leBytes, err := message.GetContentData() + if err != nil { + t.Errorf("Error getting lease data: %v", err) + return + } + var le = &coordinationv1.Lease{} + err = json.Unmarshal(leBytes, le) + if err != nil { + t.Errorf("Error unmarshal lease: %v", err) + return + } + + if le.Spec.HolderIdentity == nil { + t.Errorf("Error getting lease holderIdentity") + return + } + if *le.Spec.HolderIdentity != testHolderIdentity { + t.Errorf("Error getting lease holderIdentity, expected edgehub, got %s", *le.Spec.HolderIdentity) + return + } + }) } // TestProcessResponse is function to test processResponse @@ -279,25 +436,24 @@ func TestProcessResponse(t *testing.T) { beehiveContext.AddModule(addEdged) //jsonMarshall fail - msg := model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.ResponseOperation).FillBody(make(chan int)) + msg := model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypeLease, model.ResponseOperation).FillBody(make(chan int)) beehiveContext.Send(modules.MetaManagerModuleName, *msg) meta.processResponse(*msg) message, _ := beehiveContext.Receive(ModuleNameEdged) t.Run("MarshallFail", func(t *testing.T) { - want := "Error to get response message content data: marshal message content failed: json: unsupported type: chan int" - if message.GetContent() != want { - t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) + if message.GetContent() != marshalError { + t.Errorf("Wrong Error message received : Wanted %v and Got %v", marshalError, message.GetContent()) } }) //Database save error ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) rawSetterMock.EXPECT().Exec().Return(nil, errFailedDBOperation).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.ResponseOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypeLease, model.ResponseOperation) meta.processResponse(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("DatabaseSaveError", func(t *testing.T) { - want := "Error to update meta to DB: " + FailedDBOperation + want := "insert or update meta failed, " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) } @@ -306,7 +462,7 @@ func TestProcessResponse(t *testing.T) { //Success Case Source EdgeD ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypePodStatus, model.ResponseOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, model.ResourceTypeLease, model.ResponseOperation) meta.processResponse(*msg) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) t.Run("SuccessSourceEdged", func(t *testing.T) { @@ -319,7 +475,7 @@ func TestProcessResponse(t *testing.T) { //Success Case Source EdgeHub ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) rawSetterMock.EXPECT().Exec().Return(nil, nil).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameController, GroupResource, model.ResourceTypePodStatus, model.ResponseOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameController, GroupResource, model.ResourceTypeLease, model.ResponseOperation) meta.processResponse(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("SuccessSourceEdgeHub", func(t *testing.T) { @@ -362,11 +518,11 @@ func TestProcessDelete(t *testing.T) { querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) querySetterMock.EXPECT().Delete().Return(int64(1), errFailedDBOperation).Times(1) ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg := model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, model.ResourceTypePodStatus, model.DeleteOperation) + msg := model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, model.ResourceTypeLease, model.DeleteOperation) meta.processDelete(*msg) message, _ := beehiveContext.Receive(ModuleNameEdgeHub) t.Run("DatabaseDeleteError", func(t *testing.T) { - want := "Error to delete meta to DB: " + FailedDBOperation + want := "delete meta failed, " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong message received : Wanted %v and Got %v", want, message.GetContent()) } @@ -376,7 +532,7 @@ func TestProcessDelete(t *testing.T) { querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) querySetterMock.EXPECT().Delete().Return(int64(1), nil).Times(1) ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, model.ResourceTypePodStatus, model.DeleteOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, model.ResourceTypeLease, model.DeleteOperation) meta.processDelete(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) t.Run("SuccessSourceEdgeHub", func(t *testing.T) { @@ -392,17 +548,67 @@ func TestProcessDelete(t *testing.T) { t.Errorf("Wrong message received : Wanted %v and Got %v", want, message.GetContent()) } }) - // Success Case + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + Namespace: "test", + UID: "1234567890", + }, + } + podBytes, _ := json.Marshal(pod) resource := fmt.Sprintf("test/%s/nginx", model.ResourceTypePod) - msg = model.NewMessage("").BuildRouter(ModuleNameEdged, modules.MetaGroup, resource, model.DeleteOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, modules.MetaGroup, resource, model.DeleteOperation).FillBody(podBytes) meta.processDelete(*msg) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) - t.Run("SuccessSourceEdged", func(t *testing.T) { + + t.Run("SuccessDeleteRemotePod", func(t *testing.T) { want := ModuleNameEdged if message.GetSource() != want { t.Errorf("Wrong message received : Wanted from source %v and Got from source %v", want, message.GetSource()) } + if message.GetOperation() != model.DeleteOperation { + t.Errorf("Wrong message received : Wanted %v and Got %v", constants.MessageSuccessfulContent, message.GetContent()) + } + }) + + fakeDao := new([]dao.Meta) + fakeDaoArray := make([]dao.Meta, 1) + fakeDaoArray[0] = dao.Meta{Key: "Test", Value: string(podBytes)} + fakeDao = &fakeDaoArray + querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(3) + querySetterMock.EXPECT().All(gomock.Any()).SetArg(0, *fakeDao).Return(int64(1), nil).Times(1) + querySetterMock.EXPECT().Delete().Return(int64(1), nil).Times(2) + ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(3) + + resource = fmt.Sprintf("test/%s/nginx", model.ResourceTypePod) + msg = model.NewMessage("").BuildRouter(ModuleNameController, modules.MetaGroup, resource, model.DeleteOperation).FillBody(podBytes) + meta.processDelete(*msg) + message, _ = beehiveContext.Receive(ModuleNameEdged) + t.Run("SuccessDeletePodToEdged", func(t *testing.T) { + want := ModuleNameController + if message.GetSource() != want { + t.Errorf("Wrong message received : Wanted from source %v and Got from source %v", want, message.GetSource()) + } + if message.GetOperation() != model.DeleteOperation { + t.Errorf("Wrong message received : Wanted %v and Got %v", model.DeleteOperation, message.GetOperation()) + } + if message.GetResource() != resource { + t.Errorf("Wrong message received resource: Wanted %v and Got %v", resource, message.GetResource()) + } + }) + message, _ = beehiveContext.Receive(ModuleNameEdgeHub) + t.Run("SuccessDeletePodResponse", func(t *testing.T) { + want := ModuleNameController + if message.GetSource() != want { + t.Errorf("Wrong message received : Wanted from source %v and Got from source %v", want, message.GetSource()) + } + if message.GetOperation() != model.ResponseOperation { + t.Errorf("Wrong message received : Wanted %v and Got %v", model.ResponseOperation, message.GetOperation()) + } + if message.GetContent() != OK { + t.Errorf("Wrong message received : Wanted %v and Got %v", constants.MessageSuccessfulContent, message.GetContent()) + } }) } @@ -412,7 +618,6 @@ func TestProcessQuery(t *testing.T) { defer mockCtrl.Finish() ormerMock := beego.NewMockOrmer(mockCtrl) querySetterMock := beego.NewMockQuerySeter(mockCtrl) - rawSetterMock := beego.NewMockRawSeter(mockCtrl) dbm.DBAccess = ormerMock meta := newMetaManager(true) core.Register(meta) @@ -438,27 +643,21 @@ func TestProcessQuery(t *testing.T) { connect.SetConnected(true) //process remote query jsonMarshall error - querySetterMock.EXPECT().All(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) - querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) - ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg := model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation) + msg := model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation).FillBody(make(chan int)) meta.processQuery(*msg) message, _ := beehiveContext.Receive(ModuleNameEdgeHub) - msg = model.NewMessage(message.GetID()).BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation).FillBody(make(chan int)) - beehiveContext.SendResp(*msg) + message.Header.ParentID = message.GetID() + message.SetResourceOperation("test/"+model.ResourceTypeConfigmap, model.ResponseOperation) + beehiveContext.SendResp(message) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) t.Run("ProcessRemoteQueryMarshallFail", func(t *testing.T) { - want := "Error to get remote query response message content data: marshal message content failed: json: unsupported type: chan int" - if message.GetContent() != want { - t.Errorf("Wrong Error message received : Wanted %v and Got %v", want, message.GetContent()) + if message.GetContent() != marshalError { + t.Errorf("Wrong Error message received : Wanted %v and Got %v", marshalError, message.GetContent()) } }) //process remote query response error content - querySetterMock.EXPECT().All(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) - querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) - ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdged, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation) meta.processQuery(*msg) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) msg = model.NewMessage(message.GetID()).BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation).FillBody(fmt.Errorf("test")) @@ -477,11 +676,6 @@ func TestProcessQuery(t *testing.T) { }) //process remote query db fail - rawSetterMock.EXPECT().Exec().Return(nil, errFailedDBOperation).Times(1) - ormerMock.EXPECT().Raw(gomock.Any(), gomock.Any()).Return(rawSetterMock).Times(1) - querySetterMock.EXPECT().All(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) - querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) - ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation) meta.processQuery(*msg) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) @@ -496,7 +690,7 @@ func TestProcessQuery(t *testing.T) { } }) - //No error and connected true + // No error and connected true fakeDao := new([]dao.Meta) fakeDaoArray := make([]dao.Meta, 1) fakeDaoArray[0] = dao.Meta{Key: "Test", Value: MessageTest} @@ -504,10 +698,10 @@ func TestProcessQuery(t *testing.T) { querySetterMock.EXPECT().All(gomock.Any()).SetArg(0, *fakeDao).Return(int64(1), nil).Times(1) querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeConfigmap, model.QueryOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeRule, model.QueryOperation) meta.processQuery(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) - t.Run("DatabaseNoErrorAndMetaFound", func(t *testing.T) { + t.Run("LocalDatabaseNoErrorAndMetaFound", func(t *testing.T) { want := make([]string, 1) want[0] = MessageTest bytesWant, _ := json.Marshal(want) @@ -518,16 +712,14 @@ func TestProcessQuery(t *testing.T) { }) //ResId Nil database error - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, model.ResourceTypePodStatus, OperationNodeConnection).FillBody(connect.CloudDisconnected) - querySetterMock.EXPECT().All(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, model.ResourceTypeConfigmap, model.QueryOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeRule, model.QueryOperation) meta.processQuery(*msg) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) t.Run("ResIDNilDatabaseError", func(t *testing.T) { - want := "Error to query meta in DB: " + FailedDBOperation + want := "failed to query meta in DB: " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong message receive : Wanted %v and Got %v", want, message.GetContent()) } @@ -537,12 +729,11 @@ func TestProcessQuery(t *testing.T) { querySetterMock.EXPECT().All(gomock.Any()).Return(int64(1), errFailedDBOperation).Times(1) querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/test/"+model.ResourceTypeConfigmap, model.QueryOperation) - //beehiveContext.Send(MetaManagerModuleName, *msg) + msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeRule+"/test", model.QueryOperation) meta.processQuery(*msg) message, _ = beehiveContext.Receive(ModuleNameEdgeHub) t.Run("ResIDNotNilDatabaseError", func(t *testing.T) { - want := "Error to query meta in DB: " + FailedDBOperation + want := "failed to query meta in DB: " + FailedDBOperation if message.GetContent() != want { t.Errorf("Wrong message receive : Wanted %v and Got %v", want, message.GetContent()) } @@ -552,7 +743,7 @@ func TestProcessQuery(t *testing.T) { querySetterMock.EXPECT().All(gomock.Any()).SetArg(0, *fakeDao).Return(int64(1), nil).Times(1) querySetterMock.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(querySetterMock).Times(1) ormerMock.EXPECT().QueryTable(gomock.Any()).Return(querySetterMock).Times(1) - msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/test/"+model.ResourceTypeConfigmap, model.QueryOperation) + msg = model.NewMessage("").BuildRouter(ModuleNameEdgeHub, GroupResource, "test/"+model.ResourceTypeRule+"/test", model.QueryOperation) //beehiveContext.Send(MetaManagerModuleName, *msg) meta.processQuery(*msg) message, _ = beehiveContext.Receive(ModuleNameEdged) @@ -58,6 +58,7 @@ require ( require ( github.com/pkg/errors v0.9.1 + gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -246,7 +247,6 @@ require ( gopkg.in/gorp.v1 v1.7.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect - gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/warnings.v0 v0.1.1 // indirect diff --git a/hack/generate-crds.sh b/hack/generate-crds.sh index ce851073c..0fcb0f4ca 100755 --- a/hack/generate-crds.sh +++ b/hack/generate-crds.sh @@ -23,6 +23,7 @@ CRD_OUTPUTS=build/crds DEVICES_VERSION=v1alpha2 OPERATIONS_VERSION=v1alpha1 RELIABLESYNCS_VERSION=v1alpha1 +SERVICEACCOUNTACCESS_VERSION=v1alpha1 APPS_VERSION=v1alpha1 HELM_CRDS_DIR=manifests/charts/cloudcore/crds ROUTER_DIR=build/crds/router @@ -84,6 +85,7 @@ function :copy:to:destination { mkdir -p ${CRD_OUTPUTS}/devices mkdir -p ${CRD_OUTPUTS}/reliablesyncs mkdir -p ${CRD_OUTPUTS}/apps + mkdir -p ${CRD_OUTPUTS}/policy for entry in `ls /tmp/crds/*.yaml`; do CRD_NAME=$(echo ${entry} | cut -d'.' -f3 | cut -d'_' -f2) @@ -96,6 +98,10 @@ function :copy:to:destination { CRD_NAME=$(remove_suffix_s "$CRD_NAME") cp -v ${entry} ${CRD_OUTPUTS}/apps/apps_${APPS_VERSION}_${CRD_NAME}.yaml cp -v ${entry} ${HELM_CRDS_DIR}/apps_${APPS_VERSION}_${CRD_NAME}.yaml + elif [ "$CRD_NAME" == "serviceaccountaccesses" ]; then + CRD_NAME="serviceaccountaccess" + cp -v ${entry} ${CRD_OUTPUTS}/policy/policy_${SERVICEACCOUNTACCESS_VERSION}_${CRD_NAME}.yaml + cp -v ${entry} ${HELM_CRDS_DIR}/policy_${SERVICEACCOUNTACCESS_VERSION}_${CRD_NAME}.yaml elif [ "$CRD_NAME" == "clusterobjectsyncs" ]; then cp -v ${entry} ${CRD_OUTPUTS}/reliablesyncs/cluster_objectsync_${RELIABLESYNCS_VERSION}.yaml cp -v ${entry} ${HELM_CRDS_DIR}/cluster_objectsync_${RELIABLESYNCS_VERSION}.yaml diff --git a/hack/local-up-kubeedge.sh b/hack/local-up-kubeedge.sh index e00c3781b..9535621d4 100755 --- a/hack/local-up-kubeedge.sh +++ b/hack/local-up-kubeedge.sh @@ -100,6 +100,11 @@ function create_operation_crd { kubectl apply -f ${KUBEEDGE_ROOT}/build/crds/operations/operations_v1alpha1_nodeupgradejob.yaml } +function create_serviceaccountaccess_crd { + echo "creating the saaccess crd..." + kubectl apply -f ${KUBEEDGE_ROOT}/build/crds/policy/policy_v1alpha1_serviceaccountaccess.yaml +} + function build_cloudcore { echo "building the cloudcore..." make -C "${KUBEEDGE_ROOT}" WHAT="cloudcore" @@ -267,6 +272,7 @@ create_device_crd create_objectsync_crd create_rule_crd create_operation_crd +create_serviceaccountaccess_crd generate_streamserver_cert diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index ec222a0ff..7b534df08 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -36,5 +36,5 @@ export GOPATH="${go_path}" ${KUBEEDGE_ROOT}/hack/generate-groups.sh "deepcopy,client,informer,lister" \ github.com/kubeedge/kubeedge/pkg/client github.com/kubeedge/kubeedge/pkg/apis \ -"devices:v1alpha2 reliablesyncs:v1alpha1 rules:v1 apps:v1alpha1 operations:v1alpha1" \ +"devices:v1alpha2 reliablesyncs:v1alpha1 rules:v1 apps:v1alpha1 operations:v1alpha1 policy:v1alpha1" \ --go-header-file ${KUBEEDGE_ROOT}/hack/boilerplate/boilerplate.txt diff --git a/manifests/charts/cloudcore/crds/policy_v1alpha1_serviceaccountaccess.yaml b/manifests/charts/cloudcore/crds/policy_v1alpha1_serviceaccountaccess.yaml new file mode 100644 index 000000000..a6a2c27e0 --- /dev/null +++ b/manifests/charts/cloudcore/crds/policy_v1alpha1_serviceaccountaccess.yaml @@ -0,0 +1,501 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.2 + creationTimestamp: null + name: serviceaccountaccesses.policy.kubeedge.io +spec: + group: policy.kubeedge.io + names: + kind: ServiceAccountAccess + listKind: ServiceAccountAccessList + plural: serviceaccountaccesses + shortNames: + - saaccess + singular: serviceaccountaccess + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServiceAccountAccess is the Schema for the ServiceAccountAccess + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec represents the specification of rbac. + properties: + accessClusterRoleBinding: + description: AccessClusterRoleBinding represents rbac ClusterRoleBinding + plus detailed ClusterRole info. + items: + description: AccessClusterRoleBinding represents rbac ClusterRoleBinding + plus detailed ClusterRole info. + properties: + clusterRoleBinding: + description: ClusterRoleBinding represents rbac ClusterRoleBinding. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of + this representation of an object. Servers should convert + recognized schemas to the latest internal value, and may + reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST + resource this object represents. Servers may infer this + from the endpoint the client submits requests to. Cannot + be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + roleRef: + description: RoleRef can only reference a ClusterRole in + the global namespace. If the RoleRef cannot be resolved, + the Authorizer must return an error. + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - apiGroup + - kind + - name + type: object + subjects: + description: Subjects holds references to the objects the + role applies to. + items: + description: Subject contains a reference to the object + or user identities a role binding applies to. This + can either hold a direct API object reference, or a + value for non-objects such as user and group names. + properties: + apiGroup: + description: APIGroup holds the API group of the referenced + subject. Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User + and Group subjects. + type: string + kind: + description: Kind of object being referenced. Values + defined by this API group are "User", "Group", and + "ServiceAccount". If the Authorizer does not recognized + the kind value, the Authorizer should report an + error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: Namespace of the referenced object. If + the object kind is non-namespace, such as "User" + or "Group", and this value is not empty the Authorizer + should report an error. + type: string + required: + - kind + - name + type: object + type: array + required: + - roleRef + type: object + rules: + description: Rules contains role rules. + items: + description: PolicyRule holds information that describes a + policy rule, but does not contain information about who + the rule applies to or which namespace the rule applies + to. + properties: + apiGroups: + description: APIGroups is the name of the APIGroup that + contains the resources. If multiple API groups are + specified, any action requested against one of the enumerated + resources in any API group will be allowed. + items: + type: string + type: array + nonResourceURLs: + description: NonResourceURLs is a set of partial urls + that a user should have access to. *s are allowed, + but only as the full, final step in the path Since non-resource + URLs are not namespaced, this field is only applicable + for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" + or "secrets") or non-resource URL paths (such as "/api"), but + not both. + items: + type: string + type: array + resourceNames: + description: ResourceNames is an optional white list of + names that the rule applies to. An empty set means + that everything is allowed. + items: + type: string + type: array + resources: + description: Resources is a list of resources this rule + applies to. '*' represents all resources. + items: + type: string + type: array + verbs: + description: Verbs is a list of Verbs that apply to ALL + the ResourceKinds contained in this rule. '*' represents + all verbs. + items: + type: string + type: array + required: + - verbs + type: object + type: array + type: object + type: array + accessRoleBinding: + description: AccessRoleBinding represents rbac rolebinding plus detailed + role info. + items: + description: AccessRoleBinding represents rbac rolebinding plus + detailed role info. + properties: + roleBinding: + description: RoleBinding represents rbac rolebinding. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of + this representation of an object. Servers should convert + recognized schemas to the latest internal value, and may + reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST + resource this object represents. Servers may infer this + from the endpoint the client submits requests to. Cannot + be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + roleRef: + description: RoleRef can reference a Role in the current + namespace or a ClusterRole in the global namespace. If + the RoleRef cannot be resolved, the Authorizer must return + an error. + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - apiGroup + - kind + - name + type: object + subjects: + description: Subjects holds references to the objects the + role applies to. + items: + description: Subject contains a reference to the object + or user identities a role binding applies to. This + can either hold a direct API object reference, or a + value for non-objects such as user and group names. + properties: + apiGroup: + description: APIGroup holds the API group of the referenced + subject. Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User + and Group subjects. + type: string + kind: + description: Kind of object being referenced. Values + defined by this API group are "User", "Group", and + "ServiceAccount". If the Authorizer does not recognized + the kind value, the Authorizer should report an + error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: Namespace of the referenced object. If + the object kind is non-namespace, such as "User" + or "Group", and this value is not empty the Authorizer + should report an error. + type: string + required: + - kind + - name + type: object + type: array + required: + - roleRef + type: object + rules: + description: Rules contains role rules. + items: + description: PolicyRule holds information that describes a + policy rule, but does not contain information about who + the rule applies to or which namespace the rule applies + to. + properties: + apiGroups: + description: APIGroups is the name of the APIGroup that + contains the resources. If multiple API groups are + specified, any action requested against one of the enumerated + resources in any API group will be allowed. + items: + type: string + type: array + nonResourceURLs: + description: NonResourceURLs is a set of partial urls + that a user should have access to. *s are allowed, + but only as the full, final step in the path Since non-resource + URLs are not namespaced, this field is only applicable + for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" + or "secrets") or non-resource URL paths (such as "/api"), but + not both. + items: + type: string + type: array + resourceNames: + description: ResourceNames is an optional white list of + names that the rule applies to. An empty set means + that everything is allowed. + items: + type: string + type: array + resources: + description: Resources is a list of resources this rule + applies to. '*' represents all resources. + items: + type: string + type: array + verbs: + description: Verbs is a list of Verbs that apply to ALL + the ResourceKinds contained in this rule. '*' represents + all verbs. + items: + type: string + type: array + required: + - verbs + type: object + type: array + type: object + type: array + serviceAccount: + description: ServiceAccount is one-to-one corresponding relations + with the serviceaccountaccess. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this + representation of an object. Servers should convert recognized + schemas to the latest internal value, and may reject unrecognized + values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + automountServiceAccountToken: + description: AutomountServiceAccountToken indicates whether pods + running as this service account should have an API token automatically + mounted. Can be overridden at the pod level. + type: boolean + imagePullSecrets: + description: 'ImagePullSecrets is a list of references to secrets + in the same namespace to use for pulling any images in pods + that reference this ServiceAccount. ImagePullSecrets are distinct + from Secrets because Secrets can be mounted in the pod, but + ImagePullSecrets are only accessed by the kubelet. More info: + https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod' + items: + description: LocalObjectReference contains enough information + to let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: array + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + secrets: + description: 'Secrets is the list of secrets allowed to be used + by pods running using this ServiceAccount. More info: https://kubernetes.io/docs/concepts/configuration/secret' + items: + description: 'ObjectReference contains enough information to + let you inspect or modify the referred object. --- New uses + of this type are discouraged because of difficulty describing + its usage when embedded in APIs. 1. Ignored fields. It includes + many fields which are not generally honored. For instance, + ResourceVersion and FieldPath are both very rarely valid in + actual usage. 2. Invalid usage help. It is impossible to + add specific help for individual usage. In most embedded + usages, there are particular restrictions like, "must + refer only to types A and B" or "UID not honored" or "name + must be restricted". Those cannot be well described when + embedded. 3. Inconsistent validation. Because the usages + are different, the validation rules are different by usage, + which makes it hard for users to predict what will happen. 4. + The fields are both imprecise and overly precise. Kind is + not a precise mapping to a URL. This can produce ambiguity during + interpretation and require a REST mapping. In most cases, + the dependency is on the group,resource tuple and the + version of the actual struct is irrelevant. 5. We cannot + easily change it. Because this type is embedded in many locations, + updates to this type will affect numerous schemas. Don''t + make new APIs embed an underspecified API type they do not + control. Instead of using this type, create a locally provided + and used type that is well-focused on your reference. For + example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + .' + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this pod). + This syntax is chosen only to have some well-defined way + of referencing a part of an object. TODO: this design + is not final and this field is subject to change in the + future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + type: array + type: object + serviceAccountUid: + description: ServiceAccountUID is the uid of serviceaccount. + type: string + type: object + status: + description: Status represents the node list which store the rules. + properties: + nodeList: + description: NodeList represents the node name which store the rules. + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/pkg/apis/componentconfig/edgecore/v1alpha2/default.go b/pkg/apis/componentconfig/edgecore/v1alpha2/default.go index 7ee335c69..8c2ef5a7e 100644 --- a/pkg/apis/componentconfig/edgecore/v1alpha2/default.go +++ b/pkg/apis/componentconfig/edgecore/v1alpha2/default.go @@ -135,11 +135,12 @@ func NewDefaultEdgeCoreConfig() *EdgeCoreConfig { ContextSendModule: metaconfig.ModuleNameEdgeHub, RemoteQueryTimeout: constants.DefaultRemoteQueryTimeout, MetaServer: &MetaServer{ - Enable: false, - Server: constants.DefaultMetaServerAddr, - TLSCaFile: constants.DefaultCAFile, - TLSCertFile: constants.DefaultCertFile, - TLSPrivateKeyFile: constants.DefaultKeyFile, + Enable: false, + Server: constants.DefaultMetaServerAddr, + TLSCaFile: constants.DefaultCAFile, + TLSCertFile: constants.DefaultCertFile, + TLSPrivateKeyFile: constants.DefaultKeyFile, + ServiceAccountIssuers: []string{constants.DefaultServiceAccountIssuer}, }, }, ServiceBus: &ServiceBus{ diff --git a/pkg/apis/componentconfig/edgecore/v1alpha2/types.go b/pkg/apis/componentconfig/edgecore/v1alpha2/types.go index 0a93e0cdb..f2c75c108 100644 --- a/pkg/apis/componentconfig/edgecore/v1alpha2/types.go +++ b/pkg/apis/componentconfig/edgecore/v1alpha2/types.go @@ -996,11 +996,14 @@ type MetaManager struct { } type MetaServer struct { - Enable bool `json:"enable"` - Server string `json:"server"` - TLSCaFile string `json:"tlsCaFile"` - TLSCertFile string `json:"tlsCertFile"` - TLSPrivateKeyFile string `json:"tlsPrivateKeyFile"` + Enable bool `json:"enable"` + Server string `json:"server"` + TLSCaFile string `json:"tlsCaFile"` + TLSCertFile string `json:"tlsCertFile"` + TLSPrivateKeyFile string `json:"tlsPrivateKeyFile"` + ServiceAccountIssuers []string `json:"serviceAccountIssuers"` + APIAudiences []string `json:"apiAudiences"` + ServiceAccountKeyFiles []string `json:"serviceAccountKeyFiles"` } // ServiceBus indicates the ServiceBus module config diff --git a/pkg/apis/policy/v1alpha1/doc.go b/pkg/apis/policy/v1alpha1/doc.go new file mode 100644 index 000000000..2a61807f1 --- /dev/null +++ b/pkg/apis/policy/v1alpha1/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The KubeEdge Authors. + +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. +*/ + +// Package v1alpha1 is the v1alpha1 version of the API. +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +groupName=policy.kubeedge.io +package v1alpha1 diff --git a/pkg/apis/policy/v1alpha1/register.go b/pkg/apis/policy/v1alpha1/register.go new file mode 100644 index 000000000..103bc4fdd --- /dev/null +++ b/pkg/apis/policy/v1alpha1/register.go @@ -0,0 +1,61 @@ +// Copyright 2023 The KubeEdge Authors +// +// 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. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +const ( + // GroupName is the group name use in this package. + GroupName = "policy.kubeedge.io" + // Version is the API version. + Version = "v1alpha1" +) + +var ( + // SchemeGroupVersion is the group version used to register these objects. + SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Resource takes an unqualified resource and returns a Group-qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// addKnownTypes adds the set of types defined in this package to the supplied scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &ServiceAccountAccess{}, + &ServiceAccountAccessList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/policy/v1alpha1/serviceaccountaccess_types.go b/pkg/apis/policy/v1alpha1/serviceaccountaccess_types.go new file mode 100644 index 000000000..c052bb82b --- /dev/null +++ b/pkg/apis/policy/v1alpha1/serviceaccountaccess_types.go @@ -0,0 +1,88 @@ +/* +Copyright 2023 The KubeEdge Authors. + +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. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=saaccess + +// ServiceAccountAccess is the Schema for the ServiceAccountAccess API +type ServiceAccountAccess struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec represents the specification of rbac. + // +required + Spec AccessSpec `json:"spec,omitempty"` + + // Status represents the node list which store the rules. + // +optional + Status AccessStatus `json:"status,omitempty"` +} + +// AccessStatus defines the observed state of ServiceAccountAccess +type AccessStatus struct { + // NodeList represents the node name which store the rules. + NodeList []string `json:"nodeList,omitempty"` +} + +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ServiceAccountAccessList contains a list of ServiceAccountAccess +type ServiceAccountAccessList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServiceAccountAccess `json:"items"` +} + +// AccessSpec defines the desired state of AccessSpec +type AccessSpec struct { + // ServiceAccount is one-to-one corresponding relations with the serviceaccountaccess. + ServiceAccount corev1.ServiceAccount `json:"serviceAccount,omitempty"` + // ServiceAccountUID is the uid of serviceaccount. + ServiceAccountUID types.UID `json:"serviceAccountUid,omitempty"` + // AccessRoleBinding represents rbac rolebinding plus detailed role info. + AccessRoleBinding []AccessRoleBinding `json:"accessRoleBinding,omitempty"` + // AccessClusterRoleBinding represents rbac ClusterRoleBinding plus detailed ClusterRole info. + AccessClusterRoleBinding []AccessClusterRoleBinding `json:"accessClusterRoleBinding,omitempty"` +} + +// AccessRoleBinding represents rbac rolebinding plus detailed role info. +type AccessRoleBinding struct { + // RoleBinding represents rbac rolebinding. + RoleBinding rbac.RoleBinding `json:"roleBinding,omitempty"` + // Rules contains role rules. + Rules []rbac.PolicyRule `json:"rules,omitempty"` +} + +// AccessClusterRoleBinding represents rbac ClusterRoleBinding plus detailed ClusterRole info. +type AccessClusterRoleBinding struct { + // ClusterRoleBinding represents rbac ClusterRoleBinding. + ClusterRoleBinding rbac.ClusterRoleBinding `json:"clusterRoleBinding,omitempty"` + // Rules contains role rules. + Rules []rbac.PolicyRule `json:"rules,omitempty"` +} diff --git a/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..0acd0afd6 --- /dev/null +++ b/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,188 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/rbac/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessClusterRoleBinding) DeepCopyInto(out *AccessClusterRoleBinding) { + *out = *in + in.ClusterRoleBinding.DeepCopyInto(&out.ClusterRoleBinding) + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]v1.PolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessClusterRoleBinding. +func (in *AccessClusterRoleBinding) DeepCopy() *AccessClusterRoleBinding { + if in == nil { + return nil + } + out := new(AccessClusterRoleBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessRoleBinding) DeepCopyInto(out *AccessRoleBinding) { + *out = *in + in.RoleBinding.DeepCopyInto(&out.RoleBinding) + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]v1.PolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessRoleBinding. +func (in *AccessRoleBinding) DeepCopy() *AccessRoleBinding { + if in == nil { + return nil + } + out := new(AccessRoleBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessSpec) DeepCopyInto(out *AccessSpec) { + *out = *in + in.ServiceAccount.DeepCopyInto(&out.ServiceAccount) + if in.AccessRoleBinding != nil { + in, out := &in.AccessRoleBinding, &out.AccessRoleBinding + *out = make([]AccessRoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AccessClusterRoleBinding != nil { + in, out := &in.AccessClusterRoleBinding, &out.AccessClusterRoleBinding + *out = make([]AccessClusterRoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessSpec. +func (in *AccessSpec) DeepCopy() *AccessSpec { + if in == nil { + return nil + } + out := new(AccessSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessStatus) DeepCopyInto(out *AccessStatus) { + *out = *in + if in.NodeList != nil { + in, out := &in.NodeList, &out.NodeList + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessStatus. +func (in *AccessStatus) DeepCopy() *AccessStatus { + if in == nil { + return nil + } + out := new(AccessStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountAccess) DeepCopyInto(out *ServiceAccountAccess) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountAccess. +func (in *ServiceAccountAccess) DeepCopy() *ServiceAccountAccess { + if in == nil { + return nil + } + out := new(ServiceAccountAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceAccountAccess) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountAccessList) DeepCopyInto(out *ServiceAccountAccessList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceAccountAccess, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountAccessList. +func (in *ServiceAccountAccessList) DeepCopy() *ServiceAccountAccessList { + if in == nil { + return nil + } + out := new(ServiceAccountAccessList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceAccountAccessList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index 33ba808de..10908589e 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -25,6 +25,7 @@ import ( appsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/apps/v1alpha1" devicesv1alpha2 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/devices/v1alpha2" operationsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/operations/v1alpha1" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/policy/v1alpha1" reliablesyncsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/reliablesyncs/v1alpha1" rulesv1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/rules/v1" discovery "k8s.io/client-go/discovery" @@ -37,6 +38,7 @@ type Interface interface { AppsV1alpha1() appsv1alpha1.AppsV1alpha1Interface DevicesV1alpha2() devicesv1alpha2.DevicesV1alpha2Interface OperationsV1alpha1() operationsv1alpha1.OperationsV1alpha1Interface + PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface ReliablesyncsV1alpha1() reliablesyncsv1alpha1.ReliablesyncsV1alpha1Interface RulesV1() rulesv1.RulesV1Interface } @@ -48,6 +50,7 @@ type Clientset struct { appsV1alpha1 *appsv1alpha1.AppsV1alpha1Client devicesV1alpha2 *devicesv1alpha2.DevicesV1alpha2Client operationsV1alpha1 *operationsv1alpha1.OperationsV1alpha1Client + policyV1alpha1 *policyv1alpha1.PolicyV1alpha1Client reliablesyncsV1alpha1 *reliablesyncsv1alpha1.ReliablesyncsV1alpha1Client rulesV1 *rulesv1.RulesV1Client } @@ -67,6 +70,11 @@ func (c *Clientset) OperationsV1alpha1() operationsv1alpha1.OperationsV1alpha1In return c.operationsV1alpha1 } +// PolicyV1alpha1 retrieves the PolicyV1alpha1Client +func (c *Clientset) PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface { + return c.policyV1alpha1 +} + // ReliablesyncsV1alpha1 retrieves the ReliablesyncsV1alpha1Client func (c *Clientset) ReliablesyncsV1alpha1() reliablesyncsv1alpha1.ReliablesyncsV1alpha1Interface { return c.reliablesyncsV1alpha1 @@ -133,6 +141,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, if err != nil { return nil, err } + cs.policyV1alpha1, err = policyv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.reliablesyncsV1alpha1, err = reliablesyncsv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err @@ -165,6 +177,7 @@ func New(c rest.Interface) *Clientset { cs.appsV1alpha1 = appsv1alpha1.New(c) cs.devicesV1alpha2 = devicesv1alpha2.New(c) cs.operationsV1alpha1 = operationsv1alpha1.New(c) + cs.policyV1alpha1 = policyv1alpha1.New(c) cs.reliablesyncsV1alpha1 = reliablesyncsv1alpha1.New(c) cs.rulesV1 = rulesv1.New(c) diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index ce1710f79..5da802c6b 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -26,6 +26,8 @@ import ( fakedevicesv1alpha2 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/devices/v1alpha2/fake" operationsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/operations/v1alpha1" fakeoperationsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/operations/v1alpha1/fake" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/policy/v1alpha1" + fakepolicyv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake" reliablesyncsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/reliablesyncs/v1alpha1" fakereliablesyncsv1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/reliablesyncs/v1alpha1/fake" rulesv1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/rules/v1" @@ -102,6 +104,11 @@ func (c *Clientset) OperationsV1alpha1() operationsv1alpha1.OperationsV1alpha1In return &fakeoperationsv1alpha1.FakeOperationsV1alpha1{Fake: &c.Fake} } +// PolicyV1alpha1 retrieves the PolicyV1alpha1Client +func (c *Clientset) PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface { + return &fakepolicyv1alpha1.FakePolicyV1alpha1{Fake: &c.Fake} +} + // ReliablesyncsV1alpha1 retrieves the ReliablesyncsV1alpha1Client func (c *Clientset) ReliablesyncsV1alpha1() reliablesyncsv1alpha1.ReliablesyncsV1alpha1Interface { return &fakereliablesyncsv1alpha1.FakeReliablesyncsV1alpha1{Fake: &c.Fake} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index 30a08cf8d..80a7b2fcc 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -22,6 +22,7 @@ import ( appsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/apps/v1alpha1" devicesv1alpha2 "github.com/kubeedge/kubeedge/pkg/apis/devices/v1alpha2" operationsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/operations/v1alpha1" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" reliablesyncsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/reliablesyncs/v1alpha1" rulesv1 "github.com/kubeedge/kubeedge/pkg/apis/rules/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -38,6 +39,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ appsv1alpha1.AddToScheme, devicesv1alpha2.AddToScheme, operationsv1alpha1.AddToScheme, + policyv1alpha1.AddToScheme, reliablesyncsv1alpha1.AddToScheme, rulesv1.AddToScheme, } diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 55b21eafb..766c6f57c 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -22,6 +22,7 @@ import ( appsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/apps/v1alpha1" devicesv1alpha2 "github.com/kubeedge/kubeedge/pkg/apis/devices/v1alpha2" operationsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/operations/v1alpha1" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" reliablesyncsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/reliablesyncs/v1alpha1" rulesv1 "github.com/kubeedge/kubeedge/pkg/apis/rules/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -38,6 +39,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ appsv1alpha1.AddToScheme, devicesv1alpha2.AddToScheme, operationsv1alpha1.AddToScheme, + policyv1alpha1.AddToScheme, reliablesyncsv1alpha1.AddToScheme, rulesv1.AddToScheme, } diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/doc.go new file mode 100644 index 000000000..62dbf54f4 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/doc.go new file mode 100644 index 000000000..937651e2e --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/fake_policy_client.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/fake_policy_client.go new file mode 100644 index 000000000..2b2d4f4df --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/fake_policy_client.go @@ -0,0 +1,40 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/typed/policy/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakePolicyV1alpha1 struct { + *testing.Fake +} + +func (c *FakePolicyV1alpha1) ServiceAccountAccesses(namespace string) v1alpha1.ServiceAccountAccessInterface { + return &FakeServiceAccountAccesses{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakePolicyV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/fake_serviceaccountaccess.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/fake_serviceaccountaccess.go new file mode 100644 index 000000000..318c14bb4 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/fake/fake_serviceaccountaccess.go @@ -0,0 +1,142 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeServiceAccountAccesses implements ServiceAccountAccessInterface +type FakeServiceAccountAccesses struct { + Fake *FakePolicyV1alpha1 + ns string +} + +var serviceaccountaccessesResource = schema.GroupVersionResource{Group: "policy.kubeedge.io", Version: "v1alpha1", Resource: "serviceaccountaccesses"} + +var serviceaccountaccessesKind = schema.GroupVersionKind{Group: "policy.kubeedge.io", Version: "v1alpha1", Kind: "ServiceAccountAccess"} + +// Get takes name of the serviceAccountAccess, and returns the corresponding serviceAccountAccess object, and an error if there is any. +func (c *FakeServiceAccountAccesses) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(serviceaccountaccessesResource, c.ns, name), &v1alpha1.ServiceAccountAccess{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ServiceAccountAccess), err +} + +// List takes label and field selectors, and returns the list of ServiceAccountAccesses that match those selectors. +func (c *FakeServiceAccountAccesses) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ServiceAccountAccessList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(serviceaccountaccessesResource, serviceaccountaccessesKind, c.ns, opts), &v1alpha1.ServiceAccountAccessList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ServiceAccountAccessList{ListMeta: obj.(*v1alpha1.ServiceAccountAccessList).ListMeta} + for _, item := range obj.(*v1alpha1.ServiceAccountAccessList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested serviceAccountAccesses. +func (c *FakeServiceAccountAccesses) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(serviceaccountaccessesResource, c.ns, opts)) + +} + +// Create takes the representation of a serviceAccountAccess and creates it. Returns the server's representation of the serviceAccountAccess, and an error, if there is any. +func (c *FakeServiceAccountAccesses) Create(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.CreateOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(serviceaccountaccessesResource, c.ns, serviceAccountAccess), &v1alpha1.ServiceAccountAccess{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ServiceAccountAccess), err +} + +// Update takes the representation of a serviceAccountAccess and updates it. Returns the server's representation of the serviceAccountAccess, and an error, if there is any. +func (c *FakeServiceAccountAccesses) Update(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.UpdateOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(serviceaccountaccessesResource, c.ns, serviceAccountAccess), &v1alpha1.ServiceAccountAccess{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ServiceAccountAccess), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeServiceAccountAccesses) UpdateStatus(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.UpdateOptions) (*v1alpha1.ServiceAccountAccess, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(serviceaccountaccessesResource, "status", c.ns, serviceAccountAccess), &v1alpha1.ServiceAccountAccess{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ServiceAccountAccess), err +} + +// Delete takes name of the serviceAccountAccess and deletes it. Returns an error if one occurs. +func (c *FakeServiceAccountAccesses) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(serviceaccountaccessesResource, c.ns, name, opts), &v1alpha1.ServiceAccountAccess{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeServiceAccountAccesses) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(serviceaccountaccessesResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ServiceAccountAccessList{}) + return err +} + +// Patch applies the patch and returns the patched serviceAccountAccess. +func (c *FakeServiceAccountAccesses) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ServiceAccountAccess, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(serviceaccountaccessesResource, c.ns, name, pt, data, subresources...), &v1alpha1.ServiceAccountAccess{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ServiceAccountAccess), err +} diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/generated_expansion.go new file mode 100644 index 000000000..3a23c0ae4 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type ServiceAccountAccessExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/policy_client.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/policy_client.go new file mode 100644 index 000000000..e1ef05fe2 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/policy_client.go @@ -0,0 +1,107 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" + "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type PolicyV1alpha1Interface interface { + RESTClient() rest.Interface + ServiceAccountAccessesGetter +} + +// PolicyV1alpha1Client is used to interact with features provided by the policy.kubeedge.io group. +type PolicyV1alpha1Client struct { + restClient rest.Interface +} + +func (c *PolicyV1alpha1Client) ServiceAccountAccesses(namespace string) ServiceAccountAccessInterface { + return newServiceAccountAccesses(c, namespace) +} + +// NewForConfig creates a new PolicyV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*PolicyV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new PolicyV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*PolicyV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &PolicyV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new PolicyV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *PolicyV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new PolicyV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *PolicyV1alpha1Client { + return &PolicyV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *PolicyV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/clientset/versioned/typed/policy/v1alpha1/serviceaccountaccess.go b/pkg/client/clientset/versioned/typed/policy/v1alpha1/serviceaccountaccess.go new file mode 100644 index 000000000..98ddc8d94 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/policy/v1alpha1/serviceaccountaccess.go @@ -0,0 +1,195 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" + scheme "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ServiceAccountAccessesGetter has a method to return a ServiceAccountAccessInterface. +// A group's client should implement this interface. +type ServiceAccountAccessesGetter interface { + ServiceAccountAccesses(namespace string) ServiceAccountAccessInterface +} + +// ServiceAccountAccessInterface has methods to work with ServiceAccountAccess resources. +type ServiceAccountAccessInterface interface { + Create(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.CreateOptions) (*v1alpha1.ServiceAccountAccess, error) + Update(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.UpdateOptions) (*v1alpha1.ServiceAccountAccess, error) + UpdateStatus(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.UpdateOptions) (*v1alpha1.ServiceAccountAccess, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.ServiceAccountAccess, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ServiceAccountAccessList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ServiceAccountAccess, err error) + ServiceAccountAccessExpansion +} + +// serviceAccountAccesses implements ServiceAccountAccessInterface +type serviceAccountAccesses struct { + client rest.Interface + ns string +} + +// newServiceAccountAccesses returns a ServiceAccountAccesses +func newServiceAccountAccesses(c *PolicyV1alpha1Client, namespace string) *serviceAccountAccesses { + return &serviceAccountAccesses{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the serviceAccountAccess, and returns the corresponding serviceAccountAccess object, and an error if there is any. +func (c *serviceAccountAccesses) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + result = &v1alpha1.ServiceAccountAccess{} + err = c.client.Get(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ServiceAccountAccesses that match those selectors. +func (c *serviceAccountAccesses) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ServiceAccountAccessList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ServiceAccountAccessList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested serviceAccountAccesses. +func (c *serviceAccountAccesses) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a serviceAccountAccess and creates it. Returns the server's representation of the serviceAccountAccess, and an error, if there is any. +func (c *serviceAccountAccesses) Create(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.CreateOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + result = &v1alpha1.ServiceAccountAccess{} + err = c.client.Post(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(serviceAccountAccess). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a serviceAccountAccess and updates it. Returns the server's representation of the serviceAccountAccess, and an error, if there is any. +func (c *serviceAccountAccesses) Update(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.UpdateOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + result = &v1alpha1.ServiceAccountAccess{} + err = c.client.Put(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + Name(serviceAccountAccess.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(serviceAccountAccess). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *serviceAccountAccesses) UpdateStatus(ctx context.Context, serviceAccountAccess *v1alpha1.ServiceAccountAccess, opts v1.UpdateOptions) (result *v1alpha1.ServiceAccountAccess, err error) { + result = &v1alpha1.ServiceAccountAccess{} + err = c.client.Put(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + Name(serviceAccountAccess.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(serviceAccountAccess). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the serviceAccountAccess and deletes it. Returns an error if one occurs. +func (c *serviceAccountAccesses) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *serviceAccountAccesses) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched serviceAccountAccess. +func (c *serviceAccountAccesses) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ServiceAccountAccess, err error) { + result = &v1alpha1.ServiceAccountAccess{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("serviceaccountaccesses"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index 0caccdf0c..3f7a50649 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -28,6 +28,7 @@ import ( devices "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/devices" internalinterfaces "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/internalinterfaces" operations "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/operations" + policy "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/policy" reliablesyncs "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/reliablesyncs" rules "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/rules" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -179,6 +180,7 @@ type SharedInformerFactory interface { Apps() apps.Interface Devices() devices.Interface Operations() operations.Interface + Policy() policy.Interface Reliablesyncs() reliablesyncs.Interface Rules() rules.Interface } @@ -195,6 +197,10 @@ func (f *sharedInformerFactory) Operations() operations.Interface { return operations.New(f, f.namespace, f.tweakListOptions) } +func (f *sharedInformerFactory) Policy() policy.Interface { + return policy.New(f, f.namespace, f.tweakListOptions) +} + func (f *sharedInformerFactory) Reliablesyncs() reliablesyncs.Interface { return reliablesyncs.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 0efc5fe3b..c9fd39b70 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -24,6 +24,7 @@ import ( v1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/apps/v1alpha1" v1alpha2 "github.com/kubeedge/kubeedge/pkg/apis/devices/v1alpha2" operationsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/operations/v1alpha1" + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" reliablesyncsv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/reliablesyncs/v1alpha1" v1 "github.com/kubeedge/kubeedge/pkg/apis/rules/v1" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -72,6 +73,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case operationsv1alpha1.SchemeGroupVersion.WithResource("nodeupgradejobs"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operations().V1alpha1().NodeUpgradeJobs().Informer()}, nil + // Group=policy.kubeedge.io, Version=v1alpha1 + case policyv1alpha1.SchemeGroupVersion.WithResource("serviceaccountaccesses"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Policy().V1alpha1().ServiceAccountAccesses().Informer()}, nil + // Group=reliablesyncs.kubeedge.io, Version=v1alpha1 case reliablesyncsv1alpha1.SchemeGroupVersion.WithResource("clusterobjectsyncs"): return &genericInformer{resource: resource.GroupResource(), informer: f.Reliablesyncs().V1alpha1().ClusterObjectSyncs().Informer()}, nil diff --git a/pkg/client/informers/externalversions/policy/interface.go b/pkg/client/informers/externalversions/policy/interface.go new file mode 100644 index 000000000..d0ccc848c --- /dev/null +++ b/pkg/client/informers/externalversions/policy/interface.go @@ -0,0 +1,46 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package policy + +import ( + internalinterfaces "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/policy/v1alpha1" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/policy/v1alpha1/interface.go b/pkg/client/informers/externalversions/policy/v1alpha1/interface.go new file mode 100644 index 000000000..1af843a9f --- /dev/null +++ b/pkg/client/informers/externalversions/policy/v1alpha1/interface.go @@ -0,0 +1,45 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // ServiceAccountAccesses returns a ServiceAccountAccessInformer. + ServiceAccountAccesses() ServiceAccountAccessInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// ServiceAccountAccesses returns a ServiceAccountAccessInformer. +func (v *version) ServiceAccountAccesses() ServiceAccountAccessInformer { + return &serviceAccountAccessInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/policy/v1alpha1/serviceaccountaccess.go b/pkg/client/informers/externalversions/policy/v1alpha1/serviceaccountaccess.go new file mode 100644 index 000000000..7c6694aeb --- /dev/null +++ b/pkg/client/informers/externalversions/policy/v1alpha1/serviceaccountaccess.go @@ -0,0 +1,90 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + policyv1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" + versioned "github.com/kubeedge/kubeedge/pkg/client/clientset/versioned" + internalinterfaces "github.com/kubeedge/kubeedge/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/kubeedge/kubeedge/pkg/client/listers/policy/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ServiceAccountAccessInformer provides access to a shared informer and lister for +// ServiceAccountAccesses. +type ServiceAccountAccessInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ServiceAccountAccessLister +} + +type serviceAccountAccessInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewServiceAccountAccessInformer constructs a new informer for ServiceAccountAccess type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewServiceAccountAccessInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredServiceAccountAccessInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredServiceAccountAccessInformer constructs a new informer for ServiceAccountAccess type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredServiceAccountAccessInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.PolicyV1alpha1().ServiceAccountAccesses(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.PolicyV1alpha1().ServiceAccountAccesses(namespace).Watch(context.TODO(), options) + }, + }, + &policyv1alpha1.ServiceAccountAccess{}, + resyncPeriod, + indexers, + ) +} + +func (f *serviceAccountAccessInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredServiceAccountAccessInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *serviceAccountAccessInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&policyv1alpha1.ServiceAccountAccess{}, f.defaultInformer) +} + +func (f *serviceAccountAccessInformer) Lister() v1alpha1.ServiceAccountAccessLister { + return v1alpha1.NewServiceAccountAccessLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/listers/policy/v1alpha1/expansion_generated.go b/pkg/client/listers/policy/v1alpha1/expansion_generated.go new file mode 100644 index 000000000..e94ff1647 --- /dev/null +++ b/pkg/client/listers/policy/v1alpha1/expansion_generated.go @@ -0,0 +1,27 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// ServiceAccountAccessListerExpansion allows custom methods to be added to +// ServiceAccountAccessLister. +type ServiceAccountAccessListerExpansion interface{} + +// ServiceAccountAccessNamespaceListerExpansion allows custom methods to be added to +// ServiceAccountAccessNamespaceLister. +type ServiceAccountAccessNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/policy/v1alpha1/serviceaccountaccess.go b/pkg/client/listers/policy/v1alpha1/serviceaccountaccess.go new file mode 100644 index 000000000..722eb516a --- /dev/null +++ b/pkg/client/listers/policy/v1alpha1/serviceaccountaccess.go @@ -0,0 +1,99 @@ +/* +Copyright The KubeEdge Authors. + +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/kubeedge/kubeedge/pkg/apis/policy/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ServiceAccountAccessLister helps list ServiceAccountAccesses. +// All objects returned here must be treated as read-only. +type ServiceAccountAccessLister interface { + // List lists all ServiceAccountAccesses in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ServiceAccountAccess, err error) + // ServiceAccountAccesses returns an object that can list and get ServiceAccountAccesses. + ServiceAccountAccesses(namespace string) ServiceAccountAccessNamespaceLister + ServiceAccountAccessListerExpansion +} + +// serviceAccountAccessLister implements the ServiceAccountAccessLister interface. +type serviceAccountAccessLister struct { + indexer cache.Indexer +} + +// NewServiceAccountAccessLister returns a new ServiceAccountAccessLister. +func NewServiceAccountAccessLister(indexer cache.Indexer) ServiceAccountAccessLister { + return &serviceAccountAccessLister{indexer: indexer} +} + +// List lists all ServiceAccountAccesses in the indexer. +func (s *serviceAccountAccessLister) List(selector labels.Selector) (ret []*v1alpha1.ServiceAccountAccess, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ServiceAccountAccess)) + }) + return ret, err +} + +// ServiceAccountAccesses returns an object that can list and get ServiceAccountAccesses. +func (s *serviceAccountAccessLister) ServiceAccountAccesses(namespace string) ServiceAccountAccessNamespaceLister { + return serviceAccountAccessNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ServiceAccountAccessNamespaceLister helps list and get ServiceAccountAccesses. +// All objects returned here must be treated as read-only. +type ServiceAccountAccessNamespaceLister interface { + // List lists all ServiceAccountAccesses in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ServiceAccountAccess, err error) + // Get retrieves the ServiceAccountAccess from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.ServiceAccountAccess, error) + ServiceAccountAccessNamespaceListerExpansion +} + +// serviceAccountAccessNamespaceLister implements the ServiceAccountAccessNamespaceLister +// interface. +type serviceAccountAccessNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ServiceAccountAccesses in the indexer for a given namespace. +func (s serviceAccountAccessNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ServiceAccountAccess, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ServiceAccountAccess)) + }) + return ret, err +} + +// Get retrieves the ServiceAccountAccess from the indexer for a given namespace and name. +func (s serviceAccountAccessNamespaceLister) Get(name string) (*v1alpha1.ServiceAccountAccess, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("serviceaccountaccess"), name) + } + return obj.(*v1alpha1.ServiceAccountAccess), nil +} diff --git a/pkg/metaserver/application.go b/pkg/metaserver/application.go index 2a25c4b24..ccd64361f 100644 --- a/pkg/metaserver/application.go +++ b/pkg/metaserver/application.go @@ -32,7 +32,6 @@ import ( beehiveContext "github.com/kubeedge/beehive/pkg/core/context" "github.com/kubeedge/beehive/pkg/core/model" - commontypes "github.com/kubeedge/kubeedge/common/types" ) // Application record the resources that are in applying for requesting to be transferred down from the cloud, please: @@ -50,7 +49,6 @@ type Application struct { Option []byte ReqBody []byte Subresource string - Token string // The following field defines the Application response result RespBody []byte @@ -78,11 +76,6 @@ func NewApplication(ctx context.Context, key string, verb ApplicationVerb, noden } option = v1 } - token, ok := ctx.Value(commontypes.AuthorizationKey).(string) - if !ok { - klog.Errorf("unsupported Token type :%T", ctx.Value(commontypes.AuthorizationKey)) - return nil, fmt.Errorf("unsupported Token type :%T", ctx.Value(commontypes.AuthorizationKey)) - } ctx2, cancel := context.WithCancel(ctx) app := &Application{ Key: key, @@ -92,7 +85,6 @@ func NewApplication(ctx context.Context, key string, verb ApplicationVerb, noden Status: PreApplying, Option: ToBytes(option), ReqBody: ToBytes(reqBody), - Token: token, ctx: ctx2, cancel: cancel, count: 0, @@ -113,7 +105,6 @@ func (a *Application) Identifier() string { b = append(b, a.Option...) b = append(b, a.ReqBody...) b = append(b, []byte(a.Subresource)...) - b = append(b, []byte(a.Token)...) a.ID = fmt.Sprintf("%x", sha256.Sum256(b)) return a.ID } diff --git a/staging/src/github.com/kubeedge/beehive/pkg/core/model/message.go b/staging/src/github.com/kubeedge/beehive/pkg/core/model/message.go index 172c819d9..bdf1bec92 100644 --- a/staging/src/github.com/kubeedge/beehive/pkg/core/model/message.go +++ b/staging/src/github.com/kubeedge/beehive/pkg/core/model/message.go @@ -35,6 +35,7 @@ const ( ResourceTypeRuleEndpoint = "ruleendpoint" ResourceTypeRuleStatus = "rulestatus" ResourceTypeLease = "lease" + ResourceTypeSaAccess = "serviceaccountaccess" ) // Message struct diff --git a/vendor/k8s.io/component-helpers/auth/rbac/validation/policy_comparator.go b/vendor/k8s.io/component-helpers/auth/rbac/validation/policy_comparator.go new file mode 100644 index 000000000..7a0268b5e --- /dev/null +++ b/vendor/k8s.io/component-helpers/auth/rbac/validation/policy_comparator.go @@ -0,0 +1,173 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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. +*/ + +package validation + +import ( + "strings" + + rbacv1 "k8s.io/api/rbac/v1" +) + +// Covers determines whether or not the ownerRules cover the servantRules in terms of allowed actions. +// It returns whether or not the ownerRules cover and a list of the rules that the ownerRules do not cover. +func Covers(ownerRules, servantRules []rbacv1.PolicyRule) (bool, []rbacv1.PolicyRule) { + // 1. Break every servantRule into individual rule tuples: group, verb, resource, resourceName + // 2. Compare the mini-rules against each owner rule. Because the breakdown is down to the most atomic level, we're guaranteed that each mini-servant rule will be either fully covered or not covered by a single owner rule + // 3. Any left over mini-rules means that we are not covered and we have a nice list of them. + // TODO: it might be nice to collapse the list down into something more human readable + + subrules := []rbacv1.PolicyRule{} + for _, servantRule := range servantRules { + subrules = append(subrules, BreakdownRule(servantRule)...) + } + + uncoveredRules := []rbacv1.PolicyRule{} + for _, subrule := range subrules { + covered := false + for _, ownerRule := range ownerRules { + if ruleCovers(ownerRule, subrule) { + covered = true + break + } + } + + if !covered { + uncoveredRules = append(uncoveredRules, subrule) + } + } + + return (len(uncoveredRules) == 0), uncoveredRules +} + +// BreadownRule takes a rule and builds an equivalent list of rules that each have at most one verb, one +// resource, and one resource name +func BreakdownRule(rule rbacv1.PolicyRule) []rbacv1.PolicyRule { + subrules := []rbacv1.PolicyRule{} + for _, group := range rule.APIGroups { + for _, resource := range rule.Resources { + for _, verb := range rule.Verbs { + if len(rule.ResourceNames) > 0 { + for _, resourceName := range rule.ResourceNames { + subrules = append(subrules, rbacv1.PolicyRule{APIGroups: []string{group}, Resources: []string{resource}, Verbs: []string{verb}, ResourceNames: []string{resourceName}}) + } + + } else { + subrules = append(subrules, rbacv1.PolicyRule{APIGroups: []string{group}, Resources: []string{resource}, Verbs: []string{verb}}) + } + + } + } + } + + // Non-resource URLs are unique because they only combine with verbs. + for _, nonResourceURL := range rule.NonResourceURLs { + for _, verb := range rule.Verbs { + subrules = append(subrules, rbacv1.PolicyRule{NonResourceURLs: []string{nonResourceURL}, Verbs: []string{verb}}) + } + } + + return subrules +} + +func has(set []string, ele string) bool { + for _, s := range set { + if s == ele { + return true + } + } + return false +} + +func hasAll(set, contains []string) bool { + owning := make(map[string]struct{}, len(set)) + for _, ele := range set { + owning[ele] = struct{}{} + } + for _, ele := range contains { + if _, ok := owning[ele]; !ok { + return false + } + } + return true +} + +func resourceCoversAll(setResources, coversResources []string) bool { + // if we have a star or an exact match on all resources, then we match + if has(setResources, rbacv1.ResourceAll) || hasAll(setResources, coversResources) { + return true + } + + for _, path := range coversResources { + // if we have an exact match, then we match. + if has(setResources, path) { + continue + } + // if we're not a subresource, then we definitely don't match. fail. + if !strings.Contains(path, "/") { + return false + } + tokens := strings.SplitN(path, "/", 2) + resourceToCheck := "*/" + tokens[1] + if !has(setResources, resourceToCheck) { + return false + } + } + + return true +} + +func nonResourceURLsCoversAll(set, covers []string) bool { + for _, path := range covers { + covered := false + for _, owner := range set { + if nonResourceURLCovers(owner, path) { + covered = true + break + } + } + if !covered { + return false + } + } + return true +} + +func nonResourceURLCovers(ownerPath, subPath string) bool { + if ownerPath == subPath { + return true + } + return strings.HasSuffix(ownerPath, "*") && strings.HasPrefix(subPath, strings.TrimRight(ownerPath, "*")) +} + +// ruleCovers determines whether the ownerRule (which may have multiple verbs, resources, and resourceNames) covers +// the subrule (which may only contain at most one verb, resource, and resourceName) +func ruleCovers(ownerRule, subRule rbacv1.PolicyRule) bool { + verbMatches := has(ownerRule.Verbs, rbacv1.VerbAll) || hasAll(ownerRule.Verbs, subRule.Verbs) + groupMatches := has(ownerRule.APIGroups, rbacv1.APIGroupAll) || hasAll(ownerRule.APIGroups, subRule.APIGroups) + resourceMatches := resourceCoversAll(ownerRule.Resources, subRule.Resources) + nonResourceURLMatches := nonResourceURLsCoversAll(ownerRule.NonResourceURLs, subRule.NonResourceURLs) + + resourceNameMatches := false + + if len(subRule.ResourceNames) == 0 { + resourceNameMatches = (len(ownerRule.ResourceNames) == 0) + } else { + resourceNameMatches = (len(ownerRule.ResourceNames) == 0) || hasAll(ownerRule.ResourceNames, subRule.ResourceNames) + } + + return verbMatches && groupMatches && resourceMatches && resourceNameMatches && nonResourceURLMatches +} diff --git a/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/internal_version_adapter.go b/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/internal_version_adapter.go new file mode 100644 index 000000000..bfb57242d --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/internal_version_adapter.go @@ -0,0 +1,39 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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. +*/ + +package validation + +import ( + "context" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/kubernetes/pkg/apis/rbac" + rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1" +) + +func ConfirmNoEscalationInternal(ctx context.Context, ruleResolver AuthorizationRuleResolver, inRules []rbac.PolicyRule) error { + rules := []rbacv1.PolicyRule{} + for i := range inRules { + v1Rule := rbacv1.PolicyRule{} + err := rbacv1helpers.Convert_rbac_PolicyRule_To_v1_PolicyRule(&inRules[i], &v1Rule, nil) + if err != nil { + return err + } + rules = append(rules, v1Rule) + } + + return ConfirmNoEscalation(ctx, ruleResolver, rules) +} diff --git a/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/policy_compact.go b/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/policy_compact.go new file mode 100644 index 000000000..182657b1c --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/policy_compact.go @@ -0,0 +1,89 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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. +*/ + +package validation + +import ( + "reflect" + + rbacv1 "k8s.io/api/rbac/v1" +) + +type simpleResource struct { + Group string + Resource string + ResourceNameExist bool + ResourceName string +} + +// CompactRules combines rules that contain a single APIGroup/Resource, differ only by verb, and contain no other attributes. +// this is a fast check, and works well with the decomposed "missing rules" list from a Covers check. +func CompactRules(rules []rbacv1.PolicyRule) ([]rbacv1.PolicyRule, error) { + compacted := make([]rbacv1.PolicyRule, 0, len(rules)) + + simpleRules := map[simpleResource]*rbacv1.PolicyRule{} + for _, rule := range rules { + if resource, isSimple := isSimpleResourceRule(&rule); isSimple { + if existingRule, ok := simpleRules[resource]; ok { + // Add the new verbs to the existing simple resource rule + if existingRule.Verbs == nil { + existingRule.Verbs = []string{} + } + existingRule.Verbs = append(existingRule.Verbs, rule.Verbs...) + } else { + // Copy the rule to accumulate matching simple resource rules into + simpleRules[resource] = rule.DeepCopy() + } + } else { + compacted = append(compacted, rule) + } + } + + // Once we've consolidated the simple resource rules, add them to the compacted list + for _, simpleRule := range simpleRules { + compacted = append(compacted, *simpleRule) + } + + return compacted, nil +} + +// isSimpleResourceRule returns true if the given rule contains verbs, a single resource, a single API group, at most one Resource Name, and no other values +func isSimpleResourceRule(rule *rbacv1.PolicyRule) (simpleResource, bool) { + resource := simpleResource{} + + // If we have "complex" rule attributes, return early without allocations or expensive comparisons + if len(rule.ResourceNames) > 1 || len(rule.NonResourceURLs) > 0 { + return resource, false + } + // If we have multiple api groups or resources, return early + if len(rule.APIGroups) != 1 || len(rule.Resources) != 1 { + return resource, false + } + + // Test if this rule only contains APIGroups/Resources/Verbs/ResourceNames + simpleRule := &rbacv1.PolicyRule{APIGroups: rule.APIGroups, Resources: rule.Resources, Verbs: rule.Verbs, ResourceNames: rule.ResourceNames} + if !reflect.DeepEqual(simpleRule, rule) { + return resource, false + } + + if len(rule.ResourceNames) == 0 { + resource = simpleResource{Group: rule.APIGroups[0], Resource: rule.Resources[0], ResourceNameExist: false} + } else { + resource = simpleResource{Group: rule.APIGroups[0], Resource: rule.Resources[0], ResourceNameExist: true, ResourceName: rule.ResourceNames[0]} + } + + return resource, true +} diff --git a/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/rule.go b/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/rule.go new file mode 100644 index 000000000..603f56afb --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/registry/rbac/validation/rule.go @@ -0,0 +1,368 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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. +*/ + +package validation + +import ( + "context" + "errors" + "fmt" + "strings" + + "k8s.io/klog/v2" + + rbacv1 "k8s.io/api/rbac/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/component-helpers/auth/rbac/validation" + rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1" +) + +type AuthorizationRuleResolver interface { + // GetRoleReferenceRules attempts to resolve the role reference of a RoleBinding or ClusterRoleBinding. The passed namespace should be the namespace + // of the role binding, the empty string if a cluster role binding. + GetRoleReferenceRules(roleRef rbacv1.RoleRef, namespace string) ([]rbacv1.PolicyRule, error) + + // RulesFor returns the list of rules that apply to a given user in a given namespace and error. If an error is returned, the slice of + // PolicyRules may not be complete, but it contains all retrievable rules. This is done because policy rules are purely additive and policy determinations + // can be made on the basis of those rules that are found. + RulesFor(user user.Info, namespace string) ([]rbacv1.PolicyRule, error) + + // VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, and each error encountered resolving those rules. + // If visitor() returns false, visiting is short-circuited. + VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) +} + +// ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role. +func ConfirmNoEscalation(ctx context.Context, ruleResolver AuthorizationRuleResolver, rules []rbacv1.PolicyRule) error { + ruleResolutionErrors := []error{} + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return fmt.Errorf("no user on context") + } + namespace, _ := genericapirequest.NamespaceFrom(ctx) + + ownerRules, err := ruleResolver.RulesFor(user, namespace) + if err != nil { + // As per AuthorizationRuleResolver contract, this may return a non fatal error with an incomplete list of policies. Log the error and continue. + klog.V(1).Infof("non-fatal error getting local rules for %v: %v", user, err) + ruleResolutionErrors = append(ruleResolutionErrors, err) + } + + ownerRightsCover, missingRights := validation.Covers(ownerRules, rules) + if !ownerRightsCover { + compactMissingRights := missingRights + if compact, err := CompactRules(missingRights); err == nil { + compactMissingRights = compact + } + + missingDescriptions := sets.NewString() + for _, missing := range compactMissingRights { + missingDescriptions.Insert(rbacv1helpers.CompactString(missing)) + } + + msg := fmt.Sprintf("user %q (groups=%q) is attempting to grant RBAC permissions not currently held:\n%s", user.GetName(), user.GetGroups(), strings.Join(missingDescriptions.List(), "\n")) + if len(ruleResolutionErrors) > 0 { + msg = msg + fmt.Sprintf("; resolution errors: %v", ruleResolutionErrors) + } + + return errors.New(msg) + } + return nil +} + +type DefaultRuleResolver struct { + roleGetter RoleGetter + roleBindingLister RoleBindingLister + clusterRoleGetter ClusterRoleGetter + clusterRoleBindingLister ClusterRoleBindingLister +} + +func NewDefaultRuleResolver(roleGetter RoleGetter, roleBindingLister RoleBindingLister, clusterRoleGetter ClusterRoleGetter, clusterRoleBindingLister ClusterRoleBindingLister) *DefaultRuleResolver { + return &DefaultRuleResolver{roleGetter, roleBindingLister, clusterRoleGetter, clusterRoleBindingLister} +} + +type RoleGetter interface { + GetRole(namespace, name string) (*rbacv1.Role, error) +} + +type RoleBindingLister interface { + ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) +} + +type ClusterRoleGetter interface { + GetClusterRole(name string) (*rbacv1.ClusterRole, error) +} + +type ClusterRoleBindingLister interface { + ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) +} + +func (r *DefaultRuleResolver) RulesFor(user user.Info, namespace string) ([]rbacv1.PolicyRule, error) { + visitor := &ruleAccumulator{} + r.VisitRulesFor(user, namespace, visitor.visit) + return visitor.rules, utilerrors.NewAggregate(visitor.errors) +} + +type ruleAccumulator struct { + rules []rbacv1.PolicyRule + errors []error +} + +func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool { + if rule != nil { + r.rules = append(r.rules, *rule) + } + if err != nil { + r.errors = append(r.errors, err) + } + return true +} + +func describeSubject(s *rbacv1.Subject, bindingNamespace string) string { + switch s.Kind { + case rbacv1.ServiceAccountKind: + if len(s.Namespace) > 0 { + return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+s.Namespace) + } + return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+bindingNamespace) + default: + return fmt.Sprintf("%s %q", s.Kind, s.Name) + } +} + +type clusterRoleBindingDescriber struct { + binding *rbacv1.ClusterRoleBinding + subject *rbacv1.Subject +} + +func (d *clusterRoleBindingDescriber) String() string { + return fmt.Sprintf("ClusterRoleBinding %q of %s %q to %s", + d.binding.Name, + d.binding.RoleRef.Kind, + d.binding.RoleRef.Name, + describeSubject(d.subject, ""), + ) +} + +type roleBindingDescriber struct { + binding *rbacv1.RoleBinding + subject *rbacv1.Subject +} + +func (d *roleBindingDescriber) String() string { + return fmt.Sprintf("RoleBinding %q of %s %q to %s", + d.binding.Name+"/"+d.binding.Namespace, + d.binding.RoleRef.Kind, + d.binding.RoleRef.Name, + describeSubject(d.subject, d.binding.Namespace), + ) +} + +func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) { + if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil { + if !visitor(nil, nil, err) { + return + } + } else { + sourceDescriber := &clusterRoleBindingDescriber{} + for _, clusterRoleBinding := range clusterRoleBindings { + subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "") + if !applies { + continue + } + rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "") + if err != nil { + if !visitor(nil, nil, err) { + return + } + continue + } + sourceDescriber.binding = clusterRoleBinding + sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex] + for i := range rules { + if !visitor(sourceDescriber, &rules[i], nil) { + return + } + } + } + } + + if len(namespace) > 0 { + if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil { + if !visitor(nil, nil, err) { + return + } + } else { + sourceDescriber := &roleBindingDescriber{} + for _, roleBinding := range roleBindings { + subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace) + if !applies { + continue + } + rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace) + if err != nil { + if !visitor(nil, nil, err) { + return + } + continue + } + sourceDescriber.binding = roleBinding + sourceDescriber.subject = &roleBinding.Subjects[subjectIndex] + for i := range rules { + if !visitor(sourceDescriber, &rules[i], nil) { + return + } + } + } + } + } +} + +// GetRoleReferenceRules attempts to resolve the RoleBinding or ClusterRoleBinding. +func (r *DefaultRuleResolver) GetRoleReferenceRules(roleRef rbacv1.RoleRef, bindingNamespace string) ([]rbacv1.PolicyRule, error) { + switch roleRef.Kind { + case "Role": + role, err := r.roleGetter.GetRole(bindingNamespace, roleRef.Name) + if err != nil { + return nil, err + } + return role.Rules, nil + + case "ClusterRole": + clusterRole, err := r.clusterRoleGetter.GetClusterRole(roleRef.Name) + if err != nil { + return nil, err + } + return clusterRole.Rules, nil + + default: + return nil, fmt.Errorf("unsupported role reference kind: %q", roleRef.Kind) + } +} + +// appliesTo returns whether any of the bindingSubjects applies to the specified subject, +// and if true, the index of the first subject that applies +func appliesTo(user user.Info, bindingSubjects []rbacv1.Subject, namespace string) (int, bool) { + for i, bindingSubject := range bindingSubjects { + if appliesToUser(user, bindingSubject, namespace) { + return i, true + } + } + return 0, false +} + +func has(set []string, ele string) bool { + for _, s := range set { + if s == ele { + return true + } + } + return false +} + +func appliesToUser(user user.Info, subject rbacv1.Subject, namespace string) bool { + switch subject.Kind { + case rbacv1.UserKind: + return user.GetName() == subject.Name + + case rbacv1.GroupKind: + return has(user.GetGroups(), subject.Name) + + case rbacv1.ServiceAccountKind: + // default the namespace to namespace we're working in if its available. This allows rolebindings that reference + // SAs in th local namespace to avoid having to qualify them. + saNamespace := namespace + if len(subject.Namespace) > 0 { + saNamespace = subject.Namespace + } + if len(saNamespace) == 0 { + return false + } + // use a more efficient comparison for RBAC checking + return serviceaccount.MatchesUsername(saNamespace, subject.Name, user.GetName()) + default: + return false + } +} + +// NewTestRuleResolver returns a rule resolver from lists of role objects. +func NewTestRuleResolver(roles []*rbacv1.Role, roleBindings []*rbacv1.RoleBinding, clusterRoles []*rbacv1.ClusterRole, clusterRoleBindings []*rbacv1.ClusterRoleBinding) (AuthorizationRuleResolver, *StaticRoles) { + r := StaticRoles{ + roles: roles, + roleBindings: roleBindings, + clusterRoles: clusterRoles, + clusterRoleBindings: clusterRoleBindings, + } + return newMockRuleResolver(&r), &r +} + +func newMockRuleResolver(r *StaticRoles) AuthorizationRuleResolver { + return NewDefaultRuleResolver(r, r, r, r) +} + +// StaticRoles is a rule resolver that resolves from lists of role objects. +type StaticRoles struct { + roles []*rbacv1.Role + roleBindings []*rbacv1.RoleBinding + clusterRoles []*rbacv1.ClusterRole + clusterRoleBindings []*rbacv1.ClusterRoleBinding +} + +func (r *StaticRoles) GetRole(namespace, name string) (*rbacv1.Role, error) { + if len(namespace) == 0 { + return nil, errors.New("must provide namespace when getting role") + } + for _, role := range r.roles { + if role.Namespace == namespace && role.Name == name { + return role, nil + } + } + return nil, errors.New("role not found") +} + +func (r *StaticRoles) GetClusterRole(name string) (*rbacv1.ClusterRole, error) { + for _, clusterRole := range r.clusterRoles { + if clusterRole.Name == name { + return clusterRole, nil + } + } + return nil, errors.New("clusterrole not found") +} + +func (r *StaticRoles) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) { + if len(namespace) == 0 { + return nil, errors.New("must provide namespace when listing role bindings") + } + + roleBindingList := []*rbacv1.RoleBinding{} + for _, roleBinding := range r.roleBindings { + if roleBinding.Namespace != namespace { + continue + } + // TODO(ericchiang): need to implement label selectors? + roleBindingList = append(roleBindingList, roleBinding) + } + return roleBindingList, nil +} + +func (r *StaticRoles) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) { + return r.clusterRoleBindings, nil +} diff --git a/vendor/k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/rbac.go b/vendor/k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/rbac.go new file mode 100644 index 000000000..4f25d1372 --- /dev/null +++ b/vendor/k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/rbac.go @@ -0,0 +1,225 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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. +*/ + +// Package rbac implements the authorizer.Authorizer interface using roles base access control. +package rbac + +import ( + "bytes" + "context" + "fmt" + + "k8s.io/klog/v2" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + rbaclisters "k8s.io/client-go/listers/rbac/v1" + rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +type RequestToRuleMapper interface { + // RulesFor returns all known PolicyRules and any errors that happened while locating those rules. + // Any rule returned is still valid, since rules are deny by default. If you can pass with the rules + // supplied, you do not have to fail the request. If you cannot, you should indicate the error along + // with your denial. + RulesFor(subject user.Info, namespace string) ([]rbacv1.PolicyRule, error) + + // VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, + // and each error encountered resolving those rules. Rule may be nil if err is non-nil. + // If visitor() returns false, visiting is short-circuited. + VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) +} + +type RBACAuthorizer struct { + authorizationRuleResolver RequestToRuleMapper +} + +// authorizingVisitor short-circuits once allowed, and collects any resolution errors encountered +type authorizingVisitor struct { + requestAttributes authorizer.Attributes + + allowed bool + reason string + errors []error +} + +func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool { + if rule != nil && RuleAllows(v.requestAttributes, rule) { + v.allowed = true + v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String()) + return false + } + if err != nil { + v.errors = append(v.errors, err) + } + return true +} + +func (r *RBACAuthorizer) Authorize(ctx context.Context, requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) { + ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes} + + r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit) + if ruleCheckingVisitor.allowed { + return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil + } + + // Build a detailed log of the denial. + // Make the whole block conditional so we don't do a lot of string-building we won't use. + if klog.V(5).Enabled() { + var operation string + if requestAttributes.IsResourceRequest() { + b := &bytes.Buffer{} + b.WriteString(`"`) + b.WriteString(requestAttributes.GetVerb()) + b.WriteString(`" resource "`) + b.WriteString(requestAttributes.GetResource()) + if len(requestAttributes.GetAPIGroup()) > 0 { + b.WriteString(`.`) + b.WriteString(requestAttributes.GetAPIGroup()) + } + if len(requestAttributes.GetSubresource()) > 0 { + b.WriteString(`/`) + b.WriteString(requestAttributes.GetSubresource()) + } + b.WriteString(`"`) + if len(requestAttributes.GetName()) > 0 { + b.WriteString(` named "`) + b.WriteString(requestAttributes.GetName()) + b.WriteString(`"`) + } + operation = b.String() + } else { + operation = fmt.Sprintf("%q nonResourceURL %q", requestAttributes.GetVerb(), requestAttributes.GetPath()) + } + + var scope string + if ns := requestAttributes.GetNamespace(); len(ns) > 0 { + scope = fmt.Sprintf("in namespace %q", ns) + } else { + scope = "cluster-wide" + } + + klog.Infof("RBAC: no rules authorize user %q with groups %q to %s %s", requestAttributes.GetUser().GetName(), requestAttributes.GetUser().GetGroups(), operation, scope) + } + + reason := "" + if len(ruleCheckingVisitor.errors) > 0 { + reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors)) + } + return authorizer.DecisionNoOpinion, reason, nil +} + +func (r *RBACAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + ) + + policyRules, err := r.authorizationRuleResolver.RulesFor(user, namespace) + for _, policyRule := range policyRules { + if len(policyRule.Resources) > 0 { + r := authorizer.DefaultResourceRuleInfo{ + Verbs: policyRule.Verbs, + APIGroups: policyRule.APIGroups, + Resources: policyRule.Resources, + ResourceNames: policyRule.ResourceNames, + } + var resourceRule authorizer.ResourceRuleInfo = &r + resourceRules = append(resourceRules, resourceRule) + } + if len(policyRule.NonResourceURLs) > 0 { + r := authorizer.DefaultNonResourceRuleInfo{ + Verbs: policyRule.Verbs, + NonResourceURLs: policyRule.NonResourceURLs, + } + var nonResourceRule authorizer.NonResourceRuleInfo = &r + nonResourceRules = append(nonResourceRules, nonResourceRule) + } + } + return resourceRules, nonResourceRules, false, err +} + +func New(roles rbacregistryvalidation.RoleGetter, roleBindings rbacregistryvalidation.RoleBindingLister, clusterRoles rbacregistryvalidation.ClusterRoleGetter, clusterRoleBindings rbacregistryvalidation.ClusterRoleBindingLister) *RBACAuthorizer { + authorizer := &RBACAuthorizer{ + authorizationRuleResolver: rbacregistryvalidation.NewDefaultRuleResolver( + roles, roleBindings, clusterRoles, clusterRoleBindings, + ), + } + return authorizer +} + +func RulesAllow(requestAttributes authorizer.Attributes, rules ...rbacv1.PolicyRule) bool { + for i := range rules { + if RuleAllows(requestAttributes, &rules[i]) { + return true + } + } + + return false +} + +func RuleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool { + if requestAttributes.IsResourceRequest() { + combinedResource := requestAttributes.GetResource() + if len(requestAttributes.GetSubresource()) > 0 { + combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource() + } + + return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) && + rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) && + rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) && + rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName()) + } + + return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) && + rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath()) +} + +type RoleGetter struct { + Lister rbaclisters.RoleLister +} + +func (g *RoleGetter) GetRole(namespace, name string) (*rbacv1.Role, error) { + return g.Lister.Roles(namespace).Get(name) +} + +type RoleBindingLister struct { + Lister rbaclisters.RoleBindingLister +} + +func (l *RoleBindingLister) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) { + return l.Lister.RoleBindings(namespace).List(labels.Everything()) +} + +type ClusterRoleGetter struct { + Lister rbaclisters.ClusterRoleLister +} + +func (g *ClusterRoleGetter) GetClusterRole(name string) (*rbacv1.ClusterRole, error) { + return g.Lister.Get(name) +} + +type ClusterRoleBindingLister struct { + Lister rbaclisters.ClusterRoleBindingLister +} + +func (l *ClusterRoleBindingLister) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) { + return l.Lister.List(labels.Everything()) +} diff --git a/vendor/k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/subject_locator.go b/vendor/k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/subject_locator.go new file mode 100644 index 000000000..cdd327e5b --- /dev/null +++ b/vendor/k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/subject_locator.go @@ -0,0 +1,123 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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. +*/ + +// Package rbac implements the authorizer.Authorizer interface using roles base access control. +package rbac + +import ( + rbacv1 "k8s.io/api/rbac/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +type RoleToRuleMapper interface { + // GetRoleReferenceRules attempts to resolve the role reference of a RoleBinding or ClusterRoleBinding. The passed namespace should be the namespace + // of the role binding, the empty string if a cluster role binding. + GetRoleReferenceRules(roleRef rbacv1.RoleRef, namespace string) ([]rbacv1.PolicyRule, error) +} + +type SubjectLocator interface { + AllowedSubjects(attributes authorizer.Attributes) ([]rbacv1.Subject, error) +} + +var _ = SubjectLocator(&SubjectAccessEvaluator{}) + +type SubjectAccessEvaluator struct { + superUser string + + roleBindingLister rbacregistryvalidation.RoleBindingLister + clusterRoleBindingLister rbacregistryvalidation.ClusterRoleBindingLister + roleToRuleMapper RoleToRuleMapper +} + +func NewSubjectAccessEvaluator(roles rbacregistryvalidation.RoleGetter, roleBindings rbacregistryvalidation.RoleBindingLister, clusterRoles rbacregistryvalidation.ClusterRoleGetter, clusterRoleBindings rbacregistryvalidation.ClusterRoleBindingLister, superUser string) *SubjectAccessEvaluator { + subjectLocator := &SubjectAccessEvaluator{ + superUser: superUser, + roleBindingLister: roleBindings, + clusterRoleBindingLister: clusterRoleBindings, + roleToRuleMapper: rbacregistryvalidation.NewDefaultRuleResolver( + roles, roleBindings, clusterRoles, clusterRoleBindings, + ), + } + return subjectLocator +} + +// AllowedSubjects returns the subjects that can perform an action and any errors encountered while computing the list. +// It is possible to have both subjects and errors returned if some rolebindings couldn't be resolved, but others could be. +func (r *SubjectAccessEvaluator) AllowedSubjects(requestAttributes authorizer.Attributes) ([]rbacv1.Subject, error) { + subjects := []rbacv1.Subject{{Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: user.SystemPrivilegedGroup}} + if len(r.superUser) > 0 { + subjects = append(subjects, rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: r.superUser}) + } + errorlist := []error{} + + if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil { + errorlist = append(errorlist, err) + + } else { + for _, clusterRoleBinding := range clusterRoleBindings { + rules, err := r.roleToRuleMapper.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "") + if err != nil { + // if we have an error, just keep track of it and keep processing. Since rules are additive, + // missing a reference is bad, but we can continue with other rolebindings and still have a list + // that does not contain any invalid values + errorlist = append(errorlist, err) + } + if RulesAllow(requestAttributes, rules...) { + subjects = append(subjects, clusterRoleBinding.Subjects...) + } + } + } + + if namespace := requestAttributes.GetNamespace(); len(namespace) > 0 { + if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil { + errorlist = append(errorlist, err) + + } else { + for _, roleBinding := range roleBindings { + rules, err := r.roleToRuleMapper.GetRoleReferenceRules(roleBinding.RoleRef, namespace) + if err != nil { + // if we have an error, just keep track of it and keep processing. Since rules are additive, + // missing a reference is bad, but we can continue with other rolebindings and still have a list + // that does not contain any invalid values + errorlist = append(errorlist, err) + } + if RulesAllow(requestAttributes, rules...) { + subjects = append(subjects, roleBinding.Subjects...) + } + } + } + } + + dedupedSubjects := []rbacv1.Subject{} + for _, subject := range subjects { + found := false + for _, curr := range dedupedSubjects { + if curr == subject { + found = true + break + } + } + + if !found { + dedupedSubjects = append(dedupedSubjects, subject) + } + } + + return subjects, utilerrors.NewAggregate(errorlist) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4e8dc5f4d..ab6d0cb47 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1989,6 +1989,7 @@ k8s.io/component-base/version/verflag # k8s.io/component-helpers v0.0.0 => github.com/kubeedge/kubernetes/staging/src/k8s.io/component-helpers v1.23.15-kubeedge1 ## explicit; go 1.16 k8s.io/component-helpers/apimachinery/lease +k8s.io/component-helpers/auth/rbac/validation k8s.io/component-helpers/node/util k8s.io/component-helpers/node/util/sysctl k8s.io/component-helpers/scheduling/corev1 @@ -2320,6 +2321,7 @@ k8s.io/kubernetes/pkg/proxy/metaproxier k8s.io/kubernetes/pkg/proxy/metrics k8s.io/kubernetes/pkg/proxy/util k8s.io/kubernetes/pkg/proxy/util/iptables +k8s.io/kubernetes/pkg/registry/rbac/validation k8s.io/kubernetes/pkg/scheduler/apis/config k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta2 k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta3 @@ -2389,6 +2391,7 @@ k8s.io/kubernetes/pkg/volume/util/volumepathhandler k8s.io/kubernetes/pkg/volume/validation k8s.io/kubernetes/pkg/windows/service k8s.io/kubernetes/plugin/pkg/admission/serviceaccount +k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac k8s.io/kubernetes/test/e2e/apps k8s.io/kubernetes/test/e2e/auth k8s.io/kubernetes/test/e2e/common @@ -2509,6 +2512,7 @@ sigs.k8s.io/controller-runtime/pkg/certwatcher sigs.k8s.io/controller-runtime/pkg/client sigs.k8s.io/controller-runtime/pkg/client/apiutil sigs.k8s.io/controller-runtime/pkg/client/config +sigs.k8s.io/controller-runtime/pkg/client/fake sigs.k8s.io/controller-runtime/pkg/cluster sigs.k8s.io/controller-runtime/pkg/config sigs.k8s.io/controller-runtime/pkg/config/v1alpha1 diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go new file mode 100644 index 000000000..f72afe19d --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go @@ -0,0 +1,765 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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. +*/ + +package fake + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/testing" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/internal/objectutil" +) + +type versionedTracker struct { + testing.ObjectTracker + scheme *runtime.Scheme +} + +type fakeClient struct { + tracker versionedTracker + scheme *runtime.Scheme + restMapper meta.RESTMapper + schemeWriteLock sync.Mutex +} + +var _ client.WithWatch = &fakeClient{} + +const ( + maxNameLength = 63 + randomLength = 5 + maxGeneratedNameLength = maxNameLength - randomLength +) + +// NewFakeClient creates a new fake client for testing. +// You can choose to initialize it with a slice of runtime.Object. +// +// Deprecated: Please use NewClientBuilder instead. +func NewFakeClient(initObjs ...runtime.Object) client.WithWatch { + return NewClientBuilder().WithRuntimeObjects(initObjs...).Build() +} + +// NewFakeClientWithScheme creates a new fake client with the given scheme +// for testing. +// You can choose to initialize it with a slice of runtime.Object. +// +// Deprecated: Please use NewClientBuilder instead. +func NewFakeClientWithScheme(clientScheme *runtime.Scheme, initObjs ...runtime.Object) client.WithWatch { + return NewClientBuilder().WithScheme(clientScheme).WithRuntimeObjects(initObjs...).Build() +} + +// NewClientBuilder returns a new builder to create a fake client. +func NewClientBuilder() *ClientBuilder { + return &ClientBuilder{} +} + +// ClientBuilder builds a fake client. +type ClientBuilder struct { + scheme *runtime.Scheme + restMapper meta.RESTMapper + initObject []client.Object + initLists []client.ObjectList + initRuntimeObjects []runtime.Object +} + +// WithScheme sets this builder's internal scheme. +// If not set, defaults to client-go's global scheme.Scheme. +func (f *ClientBuilder) WithScheme(scheme *runtime.Scheme) *ClientBuilder { + f.scheme = scheme + return f +} + +// WithRESTMapper sets this builder's restMapper. +// The restMapper is directly set as mapper in the Client. This can be used for example +// with a meta.DefaultRESTMapper to provide a static rest mapping. +// If not set, defaults to an empty meta.DefaultRESTMapper. +func (f *ClientBuilder) WithRESTMapper(restMapper meta.RESTMapper) *ClientBuilder { + f.restMapper = restMapper + return f +} + +// WithObjects can be optionally used to initialize this fake client with client.Object(s). +func (f *ClientBuilder) WithObjects(initObjs ...client.Object) *ClientBuilder { + f.initObject = append(f.initObject, initObjs...) + return f +} + +// WithLists can be optionally used to initialize this fake client with client.ObjectList(s). +func (f *ClientBuilder) WithLists(initLists ...client.ObjectList) *ClientBuilder { + f.initLists = append(f.initLists, initLists...) + return f +} + +// WithRuntimeObjects can be optionally used to initialize this fake client with runtime.Object(s). +func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *ClientBuilder { + f.initRuntimeObjects = append(f.initRuntimeObjects, initRuntimeObjs...) + return f +} + +// Build builds and returns a new fake client. +func (f *ClientBuilder) Build() client.WithWatch { + if f.scheme == nil { + f.scheme = scheme.Scheme + } + if f.restMapper == nil { + f.restMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{}) + } + + tracker := versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme} + for _, obj := range f.initObject { + if err := tracker.Add(obj); err != nil { + panic(fmt.Errorf("failed to add object %v to fake client: %w", obj, err)) + } + } + for _, obj := range f.initLists { + if err := tracker.Add(obj); err != nil { + panic(fmt.Errorf("failed to add list %v to fake client: %w", obj, err)) + } + } + for _, obj := range f.initRuntimeObjects { + if err := tracker.Add(obj); err != nil { + panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err)) + } + } + return &fakeClient{ + tracker: tracker, + scheme: f.scheme, + restMapper: f.restMapper, + } +} + +const trackerAddResourceVersion = "999" + +func (t versionedTracker) Add(obj runtime.Object) error { + var objects []runtime.Object + if meta.IsListType(obj) { + var err error + objects, err = meta.ExtractList(obj) + if err != nil { + return err + } + } else { + objects = []runtime.Object{obj} + } + for _, obj := range objects { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %w", err) + } + if accessor.GetResourceVersion() == "" { + // We use a "magic" value of 999 here because this field + // is parsed as uint and and 0 is already used in Update. + // As we can't go lower, go very high instead so this can + // be recognized + accessor.SetResourceVersion(trackerAddResourceVersion) + } + + obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + if err != nil { + return err + } + if err := t.ObjectTracker.Add(obj); err != nil { + return err + } + } + + return nil +} + +func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %v", err) + } + if accessor.GetName() == "" { + return apierrors.NewInvalid( + obj.GetObjectKind().GroupVersionKind().GroupKind(), + accessor.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) + } + if accessor.GetResourceVersion() != "" { + return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") + } + accessor.SetResourceVersion("1") + obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + if err != nil { + return err + } + if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil { + accessor.SetResourceVersion("") + return err + } + + return nil +} + +// convertFromUnstructuredIfNecessary will convert *unstructured.Unstructured for a GVK that is recocnized +// by the schema into the whatever the schema produces with New() for said GVK. +// This is required because the tracker unconditionally saves on manipulations, but it's List() implementation +// tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure +// we save as the very same type, otherwise subsequent List requests will fail. +func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) { + u, isUnstructured := o.(*unstructured.Unstructured) + if !isUnstructured || !s.Recognizes(u.GroupVersionKind()) { + return o, nil + } + + typed, err := s.New(u.GroupVersionKind()) + if err != nil { + return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", u.GroupVersionKind().String(), err) + } + + unstructuredSerialized, err := json.Marshal(u) + if err != nil { + return nil, fmt.Errorf("failed to serialize %T: %w", unstructuredSerialized, err) + } + if err := json.Unmarshal(unstructuredSerialized, typed); err != nil { + return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", u, typed, err) + } + + return typed, nil +} + +func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %v", err) + } + + if accessor.GetName() == "" { + return apierrors.NewInvalid( + obj.GetObjectKind().GroupVersionKind().GroupKind(), + accessor.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) + } + + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk.Empty() { + gvk, err = apiutil.GVKForObject(obj, t.scheme) + if err != nil { + return err + } + } + + oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName()) + if err != nil { + // If the resource is not found and the resource allows create on update, issue a + // create instead. + if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) { + return t.Create(gvr, obj, ns) + } + return err + } + + oldAccessor, err := meta.Accessor(oldObject) + if err != nil { + return err + } + + // If the new object does not have the resource version set and it allows unconditional update, + // default it to the resource version of the existing resource + if accessor.GetResourceVersion() == "" && allowsUnconditionalUpdate(gvk) { + accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) + } + if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() { + return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified")) + } + if oldAccessor.GetResourceVersion() == "" { + oldAccessor.SetResourceVersion("0") + } + intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64) + if err != nil { + return fmt.Errorf("can not convert resourceVersion %q to int: %v", oldAccessor.GetResourceVersion(), err) + } + intResourceVersion++ + accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10)) + if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 { + return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) + } + obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + if err != nil { + return err + } + return t.ObjectTracker.Update(gvr, obj, ns) +} + +func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + o, err := c.tracker.Get(gvr, key.Namespace, key.Name) + if err != nil { + return err + } + + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + ta, err := meta.TypeAccessor(o) + if err != nil { + return err + } + ta.SetKind(gvk.Kind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + zero(obj) + _, _, err = decoder.Decode(j, nil, obj) + return err +} + +func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { + gvk, err := apiutil.GVKForObject(list, c.scheme) + if err != nil { + return nil, err + } + + if strings.HasSuffix(gvk.Kind, "List") { + gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] + } + + listOpts := client.ListOptions{} + listOpts.ApplyOptions(opts) + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + return c.tracker.Watch(gvr, listOpts.Namespace) +} + +func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error { + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + + originalKind := gvk.Kind + + if strings.HasSuffix(gvk.Kind, "List") { + gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] + } + + if _, isUnstructuredList := obj.(*unstructured.UnstructuredList); isUnstructuredList && !c.scheme.Recognizes(gvk) { + // We need to register the ListKind with UnstructuredList: + // https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51 + c.schemeWriteLock.Lock() + c.scheme.AddKnownTypeWithName(gvk.GroupVersion().WithKind(gvk.Kind+"List"), &unstructured.UnstructuredList{}) + c.schemeWriteLock.Unlock() + } + + listOpts := client.ListOptions{} + listOpts.ApplyOptions(opts) + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + o, err := c.tracker.List(gvr, gvk, listOpts.Namespace) + if err != nil { + return err + } + + ta, err := meta.TypeAccessor(o) + if err != nil { + return err + } + ta.SetKind(originalKind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + zero(obj) + _, _, err = decoder.Decode(j, nil, obj) + if err != nil { + return err + } + + if listOpts.LabelSelector != nil { + objs, err := meta.ExtractList(obj) + if err != nil { + return err + } + filteredObjs, err := objectutil.FilterWithLabels(objs, listOpts.LabelSelector) + if err != nil { + return err + } + err = meta.SetList(obj, filteredObjs) + if err != nil { + return err + } + } + return nil +} + +func (c *fakeClient) Scheme() *runtime.Scheme { + return c.scheme +} + +func (c *fakeClient) RESTMapper() meta.RESTMapper { + return c.restMapper +} + +func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + createOptions := &client.CreateOptions{} + createOptions.ApplyOptions(opts) + + for _, dryRunOpt := range createOptions.DryRun { + if dryRunOpt == metav1.DryRunAll { + return nil + } + } + + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + + if accessor.GetName() == "" && accessor.GetGenerateName() != "" { + base := accessor.GetGenerateName() + if len(base) > maxGeneratedNameLength { + base = base[:maxGeneratedNameLength] + } + accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength))) + } + + return c.tracker.Create(gvr, obj, accessor.GetNamespace()) +} + +func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + delOptions := client.DeleteOptions{} + delOptions.ApplyOptions(opts) + + // Check the ResourceVersion if that Precondition was specified. + if delOptions.Preconditions != nil && delOptions.Preconditions.ResourceVersion != nil { + name := accessor.GetName() + dbObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), name) + if err != nil { + return err + } + oldAccessor, err := meta.Accessor(dbObj) + if err != nil { + return err + } + actualRV := oldAccessor.GetResourceVersion() + expectRV := *delOptions.Preconditions.ResourceVersion + if actualRV != expectRV { + msg := fmt.Sprintf( + "the ResourceVersion in the precondition (%s) does not match the ResourceVersion in record (%s). "+ + "The object might have been modified", + expectRV, actualRV) + return apierrors.NewConflict(gvr.GroupResource(), name, errors.New(msg)) + } + } + + return c.deleteObject(gvr, accessor) +} + +func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + + dcOptions := client.DeleteAllOfOptions{} + dcOptions.ApplyOptions(opts) + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace) + if err != nil { + return err + } + + objs, err := meta.ExtractList(o) + if err != nil { + return err + } + filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.LabelSelector) + if err != nil { + return err + } + for _, o := range filteredObjs { + accessor, err := meta.Accessor(o) + if err != nil { + return err + } + err = c.deleteObject(gvr, accessor) + if err != nil { + return err + } + } + return nil +} + +func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + updateOptions := &client.UpdateOptions{} + updateOptions.ApplyOptions(opts) + + for _, dryRunOpt := range updateOptions.DryRun { + if dryRunOpt == metav1.DryRunAll { + return nil + } + } + + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + return c.tracker.Update(gvr, obj, accessor.GetNamespace()) +} + +func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + patchOptions := &client.PatchOptions{} + patchOptions.ApplyOptions(opts) + + for _, dryRunOpt := range patchOptions.DryRun { + if dryRunOpt == metav1.DryRunAll { + return nil + } + } + + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + data, err := patch.Data(obj) + if err != nil { + return err + } + + reaction := testing.ObjectReaction(c.tracker) + handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)) + if err != nil { + return err + } + if !handled { + panic("tracker could not handle patch method") + } + + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + ta, err := meta.TypeAccessor(o) + if err != nil { + return err + } + ta.SetKind(gvk.Kind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + zero(obj) + _, _, err = decoder.Decode(j, nil, obj) + return err +} + +func (c *fakeClient) Status() client.StatusWriter { + return &fakeStatusWriter{client: c} +} + +func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error { + old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) + if err == nil { + oldAccessor, err := meta.Accessor(old) + if err == nil { + if len(oldAccessor.GetFinalizers()) > 0 { + now := metav1.Now() + oldAccessor.SetDeletionTimestamp(&now) + return c.tracker.Update(gvr, old, accessor.GetNamespace()) + } + } + } + + //TODO: implement propagation + return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) +} + +func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) { + gvk, err := apiutil.GVKForObject(obj, scheme) + if err != nil { + return schema.GroupVersionResource{}, err + } + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + return gvr, nil +} + +type fakeStatusWriter struct { + client *fakeClient +} + +func (sw *fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + // TODO(droot): This results in full update of the obj (spec + status). Need + // a way to update status field only. + return sw.client.Update(ctx, obj, opts...) +} + +func (sw *fakeStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + // TODO(droot): This results in full update of the obj (spec + status). Need + // a way to update status field only. + return sw.client.Patch(ctx, obj, patch, opts...) +} + +func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool { + switch gvk.Group { + case "apps": + switch gvk.Kind { + case "ControllerRevision", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet": + return true + } + case "autoscaling": + switch gvk.Kind { + case "HorizontalPodAutoscaler": + return true + } + case "batch": + switch gvk.Kind { + case "CronJob", "Job": + return true + } + case "certificates": + switch gvk.Kind { + case "Certificates": + return true + } + case "flowcontrol": + switch gvk.Kind { + case "FlowSchema", "PriorityLevelConfiguration": + return true + } + case "networking": + switch gvk.Kind { + case "Ingress", "IngressClass", "NetworkPolicy": + return true + } + case "policy": + switch gvk.Kind { + case "PodSecurityPolicy": + return true + } + case "rbac": + switch gvk.Kind { + case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding": + return true + } + case "scheduling": + switch gvk.Kind { + case "PriorityClass": + return true + } + case "settings": + switch gvk.Kind { + case "PodPreset": + return true + } + case "storage": + switch gvk.Kind { + case "StorageClass": + return true + } + case "": + switch gvk.Kind { + case "ConfigMap", "Endpoint", "Event", "LimitRange", "Namespace", "Node", + "PersistentVolume", "PersistentVolumeClaim", "Pod", "PodTemplate", + "ReplicationController", "ResourceQuota", "Secret", "Service", + "ServiceAccount", "EndpointSlice": + return true + } + } + + return false +} + +func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool { + switch gvk.Group { + case "coordination": + switch gvk.Kind { + case "Lease": + return true + } + case "node": + switch gvk.Kind { + case "RuntimeClass": + return true + } + case "rbac": + switch gvk.Kind { + case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding": + return true + } + case "": + switch gvk.Kind { + case "Endpoint", "Event", "LimitRange", "Service": + return true + } + } + + return false +} + +// zero zeros the value of a pointer. +func zero(x interface{}) { + if x == nil { + return + } + res := reflect.ValueOf(x).Elem() + res.Set(reflect.Zero(res.Type())) +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go new file mode 100644 index 000000000..7d680690d --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go @@ -0,0 +1,39 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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. +*/ + +/* +Package fake provides a fake client for testing. + +A fake client is backed by its simple object store indexed by GroupVersionResource. +You can create a fake client with optional objects. + + client := NewFakeClientWithScheme(scheme, initObjs...) // initObjs is a slice of runtime.Object + +You can invoke the methods defined in the Client interface. + +When in doubt, it's almost always better not to use this package and instead use +envtest.Environment with a real client and API server. + +WARNING: ⚠️ Current Limitations / Known Issues with the fake Client ⚠️ +- This client does not have a way to inject specific errors to test handled vs. unhandled errors. +- There is some support for sub resources which can cause issues with tests if you're trying to update + e.g. metadata and status in the same reconcile. +- No OpeanAPI validation is performed when creating or updating objects. +- ObjectMeta's `Generation` and `ResourceVersion` don't behave properly, Patch or Update +operations that rely on these fields will fail, or give false positives. + +*/ +package fake |
