├── _example ├── .gitignore ├── pod.yaml └── main.go ├── types ├── doc.go ├── gen │ ├── client.go │ └── template.go ├── job │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── node │ ├── client.go │ └── fiximport.go ├── event │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── daemonset │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── deployment │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── ingress │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── replicaset │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── statefulset │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── pod │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── secret │ ├── client.go │ └── fiximport.go ├── service │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go └── replicationcontroller │ ├── client.go │ ├── fiximport.go │ ├── filter.go │ └── filter_test.go ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── join ├── fiximport.go ├── generated_job_pod.go ├── generated_service_pod.go ├── generated_rs_pod.go ├── generated_daemonset_pod.go ├── generated_deployment_pod.go ├── generated_statefulset_pod.go ├── generated_ingress_service.go ├── generated_rc_pod.go ├── join.go └── gen │ └── main.go ├── nsname ├── nsname.go └── nsname_test.go ├── util.go ├── event.go ├── util └── kclient.go ├── client ├── mocks │ └── client.go └── client.go ├── testutils_test.go ├── LICENSE.txt ├── filter ├── labels.go ├── composite.go ├── composite_test.go ├── labels_test.go ├── filter.go └── filter_test.go ├── subscription_test.go ├── ticker.go ├── testutil └── async.go ├── subscription.go ├── go.mod ├── monitor_test.go ├── lister.go ├── watcher.go ├── monitor.go ├── watch_session.go ├── Makefile ├── controller.go ├── subscription_filter.go ├── publisher.go ├── README.md ├── cache.go ├── controller_test.go ├── cache_test.go ├── subscription_filter_test.go └── publisher_test.go /_example/.gitignore: -------------------------------------------------------------------------------- 1 | /example 2 | -------------------------------------------------------------------------------- /types/doc.go: -------------------------------------------------------------------------------- 1 | package types 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /join/gen/gen 2 | /types/gen/gen 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /_example/pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: sleeper 6 | spec: 7 | containers: 8 | - name: busybox 9 | image: busybox 10 | args: 11 | - sleep 12 | - "10000" 13 | -------------------------------------------------------------------------------- /types/gen/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 9 | return client.ForResource( 10 | cs.CoreV1().RESTClient(), "pods", ns) 11 | } 12 | -------------------------------------------------------------------------------- /types/job/client.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "jobs" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.BatchV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/node/client.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "nodes" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.CoreV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/event/client.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "events" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.CoreV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/daemonset/client.go: -------------------------------------------------------------------------------- 1 | package daemonset 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "daemonsets" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.AppsV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/deployment/client.go: -------------------------------------------------------------------------------- 1 | package deployment 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "deployments" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.AppsV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/ingress/client.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "ingresses" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.NetworkingV1beta1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/replicaset/client.go: -------------------------------------------------------------------------------- 1 | package replicaset 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "replicasets" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.AppsV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/statefulset/client.go: -------------------------------------------------------------------------------- 1 | package statefulset 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | "k8s.io/client-go/kubernetes" 6 | ) 7 | 8 | const resourceName = "statefulsets" 9 | 10 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 11 | scope := cs.AppsV1() 12 | return client.ForResource(scope.RESTClient(), resourceName, ns) 13 | } 14 | -------------------------------------------------------------------------------- /types/pod/client.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/client-go/kubernetes" 7 | ) 8 | 9 | const resourceName = string(corev1.ResourcePods) 10 | 11 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 12 | scope := cs.CoreV1() 13 | return client.ForResource(scope.RESTClient(), resourceName, ns) 14 | } 15 | -------------------------------------------------------------------------------- /types/secret/client.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/client-go/kubernetes" 7 | ) 8 | 9 | const resourceName = string(corev1.ResourceSecrets) 10 | 11 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 12 | scope := cs.CoreV1() 13 | return client.ForResource(scope.RESTClient(), resourceName, ns) 14 | } 15 | -------------------------------------------------------------------------------- /types/service/client.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/client-go/kubernetes" 7 | ) 8 | 9 | const resourceName = string(corev1.ResourceServices) 10 | 11 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 12 | scope := cs.CoreV1() 13 | return client.ForResource(scope.RESTClient(), resourceName, ns) 14 | } 15 | -------------------------------------------------------------------------------- /types/replicationcontroller/client.go: -------------------------------------------------------------------------------- 1 | package replicationcontroller 2 | 3 | import ( 4 | "github.com/boz/kcache/client" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/client-go/kubernetes" 7 | ) 8 | 9 | const resourceName = string(corev1.ResourceReplicationControllers) 10 | 11 | func NewClient(cs kubernetes.Interface, ns string) client.Client { 12 | scope := cs.CoreV1() 13 | return client.ForResource(scope.RESTClient(), resourceName, ns) 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-go@v3 20 | with: 21 | go-version-file: go.mod 22 | cache: true 23 | - run: go test -v ./... 24 | -------------------------------------------------------------------------------- /join/fiximport.go: -------------------------------------------------------------------------------- 1 | package join 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/job/fiximport.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/node/fiximport.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/pod/fiximport.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/event/fiximport.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/secret/fiximport.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/daemonset/fiximport.go: -------------------------------------------------------------------------------- 1 | package daemonset 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/ingress/fiximport.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/service/fiximport.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/deployment/fiximport.go: -------------------------------------------------------------------------------- 1 | package deployment 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/replicaset/fiximport.go: -------------------------------------------------------------------------------- 1 | package replicaset 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/statefulset/fiximport.go: -------------------------------------------------------------------------------- 1 | package statefulset 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /types/replicationcontroller/fiximport.go: -------------------------------------------------------------------------------- 1 | package replicationcontroller 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var _ metav1.Object 12 | var _ corev1.Pod 13 | var _ corev1.Secret 14 | var _ corev1.Service 15 | var _ corev1.Event 16 | var _ corev1.Node 17 | var _ corev1.ReplicationController 18 | var _ appsv1.Deployment 19 | var _ networkingv1beta1.Ingress 20 | var _ appsv1.ReplicaSet 21 | var _ appsv1.DaemonSet 22 | var _ batchv1.Job 23 | var _ appsv1.StatefulSet 24 | -------------------------------------------------------------------------------- /nsname/nsname.go: -------------------------------------------------------------------------------- 1 | package nsname 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | var ErrInvalidID = errors.New("Invalid ID") 12 | 13 | type NSName struct { 14 | Namespace string 15 | Name string 16 | } 17 | 18 | func Parse(id string) (NSName, error) { 19 | parts := strings.Split(id, "/") 20 | if len(parts) != 2 { 21 | return NSName{}, ErrInvalidID 22 | } 23 | return New(parts[0], parts[1]), nil 24 | } 25 | 26 | func New(ns, name string) NSName { 27 | return NSName{ns, name} 28 | } 29 | 30 | func ForObject(obj metav1.Object) NSName { 31 | return New(obj.GetNamespace(), obj.GetName()) 32 | } 33 | 34 | func (obj NSName) String() string { 35 | return fmt.Sprintf("%v/%v", obj.Namespace, obj.Name) 36 | } 37 | -------------------------------------------------------------------------------- /types/replicationcontroller/filter.go: -------------------------------------------------------------------------------- 1 | package replicationcontroller 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/boz/kcache/filter" 7 | corev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | func PodsFilter(sources ...*corev1.ReplicationController) filter.ComparableFilter { 11 | 12 | // make a copy and sort 13 | srcs := make([]*corev1.ReplicationController, len(sources)) 14 | copy(srcs, sources) 15 | 16 | sort.Slice(srcs, func(i, j int) bool { 17 | if srcs[i].Namespace != srcs[j].Namespace { 18 | return srcs[i].Namespace < srcs[j].Namespace 19 | } 20 | return srcs[i].Name < srcs[j].Name 21 | }) 22 | 23 | filters := make([]filter.Filter, 0, len(srcs)) 24 | 25 | for _, svc := range srcs { 26 | filters = append(filters, filter.Labels(svc.Spec.Selector)) 27 | } 28 | 29 | return filter.Or(filters...) 30 | } 31 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | ) 8 | 9 | func listResourceVersion(obj runtime.Object) (string, error) { 10 | list, err := meta.ListAccessor(obj) 11 | if err != nil { 12 | return "", err 13 | } 14 | return list.GetResourceVersion(), nil 15 | } 16 | 17 | func extractList(obj runtime.Object) ([]metav1.Object, error) { 18 | olist, err := meta.ExtractList(obj) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | mlist := make([]metav1.Object, 0, len(olist)) 24 | 25 | for _, obj := range olist { 26 | if obj, ok := obj.(metav1.Object); ok { 27 | mlist = append(mlist, obj) 28 | continue 29 | } 30 | return nil, errInvalidType 31 | } 32 | 33 | return mlist, nil 34 | } 35 | -------------------------------------------------------------------------------- /types/pod/filter.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | "reflect" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | 8 | "github.com/boz/kcache/filter" 9 | corev1 "k8s.io/api/core/v1" 10 | ) 11 | 12 | func NodeFilter(names ...string) filter.ComparableFilter { 13 | set := make(map[string]interface{}) 14 | for _, name := range names { 15 | set[name] = struct{}{} 16 | } 17 | return nodeFilter(set) 18 | } 19 | 20 | type nodeFilter map[string]interface{} 21 | 22 | func (f nodeFilter) Accept(obj metav1.Object) bool { 23 | pod, ok := obj.(*corev1.Pod) 24 | if !ok { 25 | return false 26 | } 27 | _, ok = f[pod.Spec.NodeName] 28 | return ok 29 | } 30 | 31 | func (f nodeFilter) Equals(other filter.Filter) bool { 32 | if other, ok := other.(nodeFilter); ok { 33 | return reflect.DeepEqual(f, other) 34 | } 35 | return false 36 | } 37 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type EventType string 10 | 11 | const ( 12 | EventTypeCreate EventType = "create" 13 | EventTypeUpdate EventType = "update" 14 | EventTypeDelete EventType = "delete" 15 | ) 16 | 17 | type Event interface { 18 | Type() EventType 19 | Resource() v1.Object 20 | } 21 | 22 | type event struct { 23 | eventType EventType 24 | resource v1.Object 25 | } 26 | 27 | func NewEvent(et EventType, resource v1.Object) Event { 28 | return event{et, resource} 29 | } 30 | 31 | func (e event) Type() EventType { 32 | return e.eventType 33 | } 34 | 35 | func (e event) Resource() v1.Object { 36 | return e.resource 37 | } 38 | 39 | func (e event) String() string { 40 | return fmt.Sprintf( 41 | "Event{%v %v/%v}", e.eventType, e.Resource().GetNamespace(), e.resource.GetName()) 42 | } 43 | -------------------------------------------------------------------------------- /util/kclient.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | "k8s.io/client-go/rest" 6 | "k8s.io/client-go/tools/clientcmd" 7 | ) 8 | 9 | func KubeClient(overrides *clientcmd.ConfigOverrides) (kubernetes.Interface, *rest.Config, error) { 10 | config, err := KubeConfig(overrides) 11 | if err != nil { 12 | return nil, nil, err 13 | } 14 | clientset, err := kubernetes.NewForConfig(config) 15 | if err != nil { 16 | return nil, nil, err 17 | } 18 | return clientset, config, nil 19 | } 20 | 21 | func KubeConfig(overrides *clientcmd.ConfigOverrides) (*rest.Config, error) { 22 | config, err := rest.InClusterConfig() 23 | if err == nil { 24 | return config, err 25 | } 26 | 27 | if overrides == nil { 28 | overrides = &clientcmd.ConfigOverrides{} 29 | } 30 | 31 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 32 | clientcmd.NewDefaultClientConfigLoadingRules(), 33 | overrides, 34 | ).ClientConfig() 35 | } 36 | -------------------------------------------------------------------------------- /client/mocks/client.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stretchr/testify/mock" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/watch" 10 | ) 11 | 12 | type Client struct { 13 | mock.Mock 14 | } 15 | 16 | func (c *Client) List(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { 17 | args := c.Called(ctx, opts) 18 | return args.Get(0).(runtime.Object), args.Error(1) 19 | } 20 | 21 | func (c *Client) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 22 | args := c.Called(ctx, opts) 23 | return args.Get(0).(watch.Interface), args.Error(1) 24 | } 25 | 26 | type WatchInterface struct { 27 | mock.Mock 28 | } 29 | 30 | func (w *WatchInterface) Stop() { 31 | w.Called() 32 | } 33 | 34 | func (w *WatchInterface) ResultChan() <-chan watch.Event { 35 | args := w.Called() 36 | return args.Get(0).(chan watch.Event) 37 | } 38 | -------------------------------------------------------------------------------- /types/ingress/filter.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "github.com/boz/kcache/filter" 5 | "github.com/boz/kcache/nsname" 6 | appsv1 "k8s.io/api/networking/v1beta1" 7 | ) 8 | 9 | func ServicesFilter(ingresses ...*appsv1.Ingress) filter.ComparableFilter { 10 | var ids []nsname.NSName 11 | 12 | for _, ing := range ingresses { 13 | ids = append(ids, buildServicesFilter(ing)...) 14 | } 15 | 16 | return filter.NSName(ids...) 17 | } 18 | 19 | func buildServicesFilter(ing *appsv1.Ingress) []nsname.NSName { 20 | var ids []nsname.NSName 21 | 22 | if be := ing.Spec.Backend; be != nil && be.ServiceName != "" { 23 | ids = append(ids, nsname.New(ing.GetNamespace(), be.ServiceName)) 24 | } 25 | 26 | for _, rule := range ing.Spec.Rules { 27 | if http := rule.HTTP; http != nil { 28 | for _, path := range http.Paths { 29 | if service := path.Backend.ServiceName; service != "" { 30 | ids = append(ids, nsname.New(ing.GetNamespace(), service)) 31 | } 32 | } 33 | } 34 | } 35 | 36 | return ids 37 | } 38 | -------------------------------------------------------------------------------- /testutils_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | 10 | "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func testGenPod(ns, name, vsn string) *v1.Pod { 15 | return &v1.Pod{ 16 | ObjectMeta: metav1.ObjectMeta{ 17 | Namespace: ns, 18 | Name: name, 19 | ResourceVersion: vsn, 20 | }, 21 | } 22 | } 23 | 24 | func testGenEvent(et EventType, ns, name, vsn string) Event { 25 | return NewEvent(et, testGenPod(ns, name, vsn)) 26 | } 27 | 28 | func testNewSubscription(t *testing.T, log logutil.Log, f filter.Filter) (subscription, cache, chan struct{}) { 29 | 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | readych := make(chan struct{}) 32 | cache := newCache(ctx, log, nil, f) 33 | 34 | sub := newSubscription(log, nil, readych, cache) 35 | 36 | go func() { 37 | <-sub.Done() 38 | cancel() 39 | }() 40 | 41 | return sub, cache, readych 42 | 43 | } 44 | -------------------------------------------------------------------------------- /types/job/filter.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/boz/kcache/filter" 7 | "github.com/boz/kcache/nsname" 8 | batchv1 "k8s.io/api/batch/v1" 9 | ) 10 | 11 | func PodsFilter(sources ...*batchv1.Job) filter.ComparableFilter { 12 | 13 | // make a copy and sort 14 | srcs := make([]*batchv1.Job, len(sources)) 15 | copy(srcs, sources) 16 | 17 | sort.Slice(srcs, func(i, j int) bool { 18 | if srcs[i].Namespace != srcs[j].Namespace { 19 | return srcs[i].Namespace < srcs[j].Namespace 20 | } 21 | return srcs[i].Name < srcs[j].Name 22 | }) 23 | 24 | filters := make([]filter.Filter, 0, len(srcs)) 25 | 26 | for _, svc := range srcs { 27 | 28 | var sfilter filter.Filter 29 | if sel := svc.Spec.Selector; sel != nil { 30 | sfilter = filter.LabelSelector(sel) 31 | } else { 32 | sfilter = filter.Labels(svc.Spec.Template.Labels) 33 | } 34 | 35 | nsfilter := filter.NSName(nsname.New(svc.GetNamespace(), "")) 36 | 37 | filters = append(filters, filter.And(nsfilter, sfilter)) 38 | } 39 | 40 | return filter.Or(filters...) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /types/daemonset/filter.go: -------------------------------------------------------------------------------- 1 | package daemonset 2 | 3 | import ( 4 | "sort" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/nsname" 10 | ) 11 | 12 | func PodsFilter(sources ...*appsv1.DaemonSet) filter.ComparableFilter { 13 | 14 | // make a copy and sort 15 | srcs := make([]*appsv1.DaemonSet, len(sources)) 16 | copy(srcs, sources) 17 | 18 | sort.Slice(srcs, func(i, j int) bool { 19 | if srcs[i].Namespace != srcs[j].Namespace { 20 | return srcs[i].Namespace < srcs[j].Namespace 21 | } 22 | return srcs[i].Name < srcs[j].Name 23 | }) 24 | 25 | filters := make([]filter.Filter, 0, len(srcs)) 26 | 27 | for _, svc := range srcs { 28 | 29 | var sfilter filter.Filter 30 | if sel := svc.Spec.Selector; sel != nil { 31 | sfilter = filter.LabelSelector(sel) 32 | } else { 33 | sfilter = filter.Labels(svc.Spec.Template.Labels) 34 | } 35 | 36 | nsfilter := filter.NSName(nsname.New(svc.GetNamespace(), "")) 37 | 38 | filters = append(filters, filter.And(nsfilter, sfilter)) 39 | } 40 | 41 | return filter.Or(filters...) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /types/deployment/filter.go: -------------------------------------------------------------------------------- 1 | package deployment 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/boz/kcache/filter" 7 | "github.com/boz/kcache/nsname" 8 | appsv1 "k8s.io/api/apps/v1" 9 | ) 10 | 11 | func PodsFilter(sources ...*appsv1.Deployment) filter.ComparableFilter { 12 | 13 | // make a copy and sort 14 | srcs := make([]*appsv1.Deployment, len(sources)) 15 | copy(srcs, sources) 16 | 17 | sort.Slice(srcs, func(i, j int) bool { 18 | if srcs[i].Namespace != srcs[j].Namespace { 19 | return srcs[i].Namespace < srcs[j].Namespace 20 | } 21 | return srcs[i].Name < srcs[j].Name 22 | }) 23 | 24 | filters := make([]filter.Filter, 0, len(srcs)) 25 | 26 | for _, svc := range srcs { 27 | 28 | var sfilter filter.Filter 29 | if sel := svc.Spec.Selector; sel != nil { 30 | sfilter = filter.LabelSelector(sel) 31 | } else { 32 | sfilter = filter.Labels(svc.Spec.Template.Labels) 33 | } 34 | 35 | nsfilter := filter.NSName(nsname.New(svc.GetNamespace(), "")) 36 | 37 | filters = append(filters, filter.And(nsfilter, sfilter)) 38 | } 39 | 40 | return filter.Or(filters...) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /types/replicaset/filter.go: -------------------------------------------------------------------------------- 1 | package replicaset 2 | 3 | import ( 4 | "sort" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/nsname" 10 | ) 11 | 12 | func PodsFilter(sources ...*appsv1.ReplicaSet) filter.ComparableFilter { 13 | 14 | // make a copy and sort 15 | srcs := make([]*appsv1.ReplicaSet, len(sources)) 16 | copy(srcs, sources) 17 | 18 | sort.Slice(srcs, func(i, j int) bool { 19 | if srcs[i].Namespace != srcs[j].Namespace { 20 | return srcs[i].Namespace < srcs[j].Namespace 21 | } 22 | return srcs[i].Name < srcs[j].Name 23 | }) 24 | 25 | filters := make([]filter.Filter, 0, len(srcs)) 26 | 27 | for _, svc := range srcs { 28 | 29 | var sfilter filter.Filter 30 | if sel := svc.Spec.Selector; sel != nil { 31 | sfilter = filter.LabelSelector(sel) 32 | } else { 33 | sfilter = filter.Labels(svc.Spec.Template.Labels) 34 | } 35 | 36 | nsfilter := filter.NSName(nsname.New(svc.GetNamespace(), "")) 37 | 38 | filters = append(filters, filter.And(nsfilter, sfilter)) 39 | } 40 | 41 | return filter.Or(filters...) 42 | } 43 | -------------------------------------------------------------------------------- /types/statefulset/filter.go: -------------------------------------------------------------------------------- 1 | package statefulset 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/boz/kcache/filter" 7 | "github.com/boz/kcache/nsname" 8 | appsv1 "k8s.io/api/apps/v1" 9 | ) 10 | 11 | func PodsFilter(sources ...*appsv1.StatefulSet) filter.ComparableFilter { 12 | 13 | // make a copy and sort 14 | srcs := make([]*appsv1.StatefulSet, len(sources)) 15 | copy(srcs, sources) 16 | 17 | sort.Slice(srcs, func(i, j int) bool { 18 | if srcs[i].Namespace != srcs[j].Namespace { 19 | return srcs[i].Namespace < srcs[j].Namespace 20 | } 21 | return srcs[i].Name < srcs[j].Name 22 | }) 23 | 24 | filters := make([]filter.Filter, 0, len(srcs)) 25 | 26 | for _, svc := range srcs { 27 | 28 | var sfilter filter.Filter 29 | if sel := svc.Spec.Selector; sel != nil { 30 | sfilter = filter.LabelSelector(sel) 31 | } else { 32 | sfilter = filter.Labels(svc.Spec.Template.Labels) 33 | } 34 | 35 | nsfilter := filter.NSName(nsname.New(svc.GetNamespace(), "")) 36 | 37 | filters = append(filters, filter.And(nsfilter, sfilter)) 38 | } 39 | 40 | return filter.Or(filters...) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adam Bozanich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /filter/labels.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "reflect" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/labels" 8 | ) 9 | 10 | // Labels() returns a filter which returns true if 11 | // the provided map is a subset of the object's labels. 12 | func Labels(match map[string]string) ComparableFilter { 13 | return Selector(labels.SelectorFromSet(match)) 14 | } 15 | 16 | func LabelSelector(ls *metav1.LabelSelector) ComparableFilter { 17 | selector, err := metav1.LabelSelectorAsSelector(ls) 18 | if err != nil { 19 | // todo: return error 20 | panic("invalid selector") 21 | } 22 | return Selector(selector) 23 | } 24 | 25 | func Selector(selector labels.Selector) ComparableFilter { 26 | // assumes selector is sorted 27 | return &selectorFilter{selector} 28 | } 29 | 30 | type selectorFilter struct { 31 | selector labels.Selector 32 | } 33 | 34 | func (f *selectorFilter) Accept(obj metav1.Object) bool { 35 | return f.selector.Matches(labels.Set(obj.GetLabels())) 36 | } 37 | 38 | func (f *selectorFilter) Equals(other Filter) bool { 39 | if other, ok := other.(*selectorFilter); ok { 40 | return reflect.DeepEqual(f.selector, other.selector) 41 | } 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /types/event/filter.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/boz/kcache/filter" 5 | corev1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | type Object interface { 11 | GetObjectKind() schema.ObjectKind 12 | GetNamespace() string 13 | GetName() string 14 | } 15 | 16 | func InvolvedObjectFilter(obj Object) filter.ComparableFilter { 17 | kind := obj.GetObjectKind().GroupVersionKind().Kind 18 | return InvolvedFilter(kind, obj.GetNamespace(), obj.GetName()) 19 | } 20 | 21 | func InvolvedFilter(kind, ns, name string) filter.ComparableFilter { 22 | return &involvedFilter{kind, ns, name} 23 | } 24 | 25 | type involvedFilter struct { 26 | kind string 27 | ns string 28 | name string 29 | } 30 | 31 | func (f *involvedFilter) Accept(obj metav1.Object) bool { 32 | event, ok := obj.(*corev1.Event) 33 | if !ok { 34 | return false 35 | } 36 | ref := event.InvolvedObject 37 | return ref.Kind == f.kind && 38 | ref.Namespace == f.ns && 39 | ref.Name == f.name 40 | } 41 | 42 | func (f *involvedFilter) Equals(other filter.Filter) bool { 43 | if other, ok := other.(*involvedFilter); ok { 44 | return *f == *other 45 | } 46 | return false 47 | } 48 | -------------------------------------------------------------------------------- /nsname/nsname_test.go: -------------------------------------------------------------------------------- 1 | package nsname_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/nsname" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParse(t *testing.T) { 11 | 12 | { 13 | id, err := nsname.Parse("foo/bar") 14 | require.NoError(t, err) 15 | require.Equal(t, "foo", id.Namespace) 16 | require.Equal(t, "bar", id.Name) 17 | require.Equal(t, "foo/bar", id.String()) 18 | } 19 | 20 | { 21 | id, err := nsname.Parse("foo/") 22 | require.NoError(t, err) 23 | require.Equal(t, "foo", id.Namespace) 24 | require.Equal(t, "", id.Name) 25 | require.Equal(t, "foo/", id.String()) 26 | } 27 | 28 | { 29 | id, err := nsname.Parse("/bar") 30 | require.NoError(t, err) 31 | require.Equal(t, "", id.Namespace) 32 | require.Equal(t, "bar", id.Name) 33 | require.Equal(t, "/bar", id.String()) 34 | } 35 | 36 | { 37 | id, err := nsname.Parse("/") 38 | require.NoError(t, err) 39 | require.Equal(t, "", id.Namespace) 40 | require.Equal(t, "", id.Name) 41 | require.Equal(t, "/", id.String()) 42 | } 43 | 44 | { 45 | _, err := nsname.Parse("/bar/") 46 | require.Equal(t, nsname.ErrInvalidID, err) 47 | } 48 | 49 | { 50 | _, err := nsname.Parse("") 51 | require.Equal(t, nsname.ErrInvalidID, err) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /types/pod/filter_test.go: -------------------------------------------------------------------------------- 1 | package pod_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/pod" 7 | "github.com/stretchr/testify/assert" 8 | 9 | "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestNodeFilter(t *testing.T) { 14 | 15 | genpod := func(name, node string) *v1.Pod { 16 | return &v1.Pod{ 17 | ObjectMeta: metav1.ObjectMeta{Name: name}, 18 | Spec: v1.PodSpec{NodeName: node}, 19 | } 20 | } 21 | 22 | assert.True(t, pod.NodeFilter("a").Accept(genpod("x", "a"))) 23 | assert.True(t, pod.NodeFilter("a", "b").Accept(genpod("x", "a"))) 24 | assert.False(t, pod.NodeFilter().Accept(genpod("x", "a"))) 25 | assert.False(t, pod.NodeFilter("a").Accept(genpod("x", "b"))) 26 | assert.False(t, pod.NodeFilter("a", "c").Accept(genpod("x", "b"))) 27 | 28 | assert.True(t, pod.NodeFilter().Equals(pod.NodeFilter())) 29 | assert.True(t, pod.NodeFilter("a").Equals(pod.NodeFilter("a"))) 30 | assert.True(t, pod.NodeFilter("a", "b").Equals(pod.NodeFilter("a", "b"))) 31 | assert.True(t, pod.NodeFilter("b", "a").Equals(pod.NodeFilter("a", "b"))) 32 | 33 | other := otherFilter(make(map[string]interface{})) 34 | assert.False(t, pod.NodeFilter().Equals(other)) 35 | } 36 | 37 | type otherFilter map[string]interface{} 38 | 39 | func (otherFilter) Accept(_ metav1.Object) bool { 40 | return false 41 | } 42 | -------------------------------------------------------------------------------- /join/generated_job_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/job" 13 | "github.com/boz/kcache/types/pod" 14 | batchv1 "k8s.io/api/batch/v1" 15 | ) 16 | 17 | func JobPodsWith(ctx context.Context, 18 | srcController job.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*batchv1.Job) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *batchv1.Job) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(job,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := job.BuildHandler(). 39 | OnInitialize(func(objs []*batchv1.Job) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := job.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(job,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /join/generated_service_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/pod" 13 | "github.com/boz/kcache/types/service" 14 | corev1 "k8s.io/api/core/v1" 15 | ) 16 | 17 | func ServicePodsWith(ctx context.Context, 18 | srcController service.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*corev1.Service) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *corev1.Service) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(service,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := service.BuildHandler(). 39 | OnInitialize(func(objs []*corev1.Service) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := service.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(service,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /join/generated_rs_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/pod" 13 | "github.com/boz/kcache/types/replicaset" 14 | appsv1 "k8s.io/api/apps/v1" 15 | ) 16 | 17 | func RSPodsWith(ctx context.Context, 18 | srcController replicaset.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*appsv1.ReplicaSet) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *appsv1.ReplicaSet) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(replicaset,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := replicaset.BuildHandler(). 39 | OnInitialize(func(objs []*appsv1.ReplicaSet) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := replicaset.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(replicaset,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /join/generated_daemonset_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/daemonset" 13 | "github.com/boz/kcache/types/pod" 14 | appsv1 "k8s.io/api/apps/v1" 15 | ) 16 | 17 | func DaemonSetPodsWith(ctx context.Context, 18 | srcController daemonset.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*appsv1.DaemonSet) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *appsv1.DaemonSet) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(daemonset,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := daemonset.BuildHandler(). 39 | OnInitialize(func(objs []*appsv1.DaemonSet) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := daemonset.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(daemonset,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /join/generated_deployment_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/deployment" 13 | "github.com/boz/kcache/types/pod" 14 | appsv1 "k8s.io/api/apps/v1" 15 | ) 16 | 17 | func DeploymentPodsWith(ctx context.Context, 18 | srcController deployment.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*appsv1.Deployment) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *appsv1.Deployment) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(deployment,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := deployment.BuildHandler(). 39 | OnInitialize(func(objs []*appsv1.Deployment) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := deployment.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(deployment,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /join/generated_statefulset_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/pod" 13 | "github.com/boz/kcache/types/statefulset" 14 | appsv1 "k8s.io/api/apps/v1" 15 | ) 16 | 17 | func StatefulSetPodsWith(ctx context.Context, 18 | srcController statefulset.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*appsv1.StatefulSet) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *appsv1.StatefulSet) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(statefulset,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := statefulset.BuildHandler(). 39 | OnInitialize(func(objs []*appsv1.StatefulSet) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := statefulset.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(statefulset,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /join/generated_ingress_service.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/ingress" 13 | "github.com/boz/kcache/types/service" 14 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 15 | ) 16 | 17 | func IngressServicesWith(ctx context.Context, 18 | srcController ingress.Controller, 19 | dstController service.Publisher, 20 | filterFn func(...*networkingv1beta1.Ingress) filter.ComparableFilter) (service.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *networkingv1beta1.Ingress) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(ingress,service: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := ingress.BuildHandler(). 39 | OnInitialize(func(objs []*networkingv1beta1.Ingress) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := ingress.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(ingress,service): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /filter/composite.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | type andFilter []Filter 8 | 9 | func And(children ...Filter) ComparableFilter { 10 | return andFilter(children) 11 | } 12 | 13 | func (f andFilter) Accept(obj metav1.Object) bool { 14 | for _, child := range f { 15 | if !child.Accept(obj) { 16 | return false 17 | } 18 | } 19 | return true 20 | } 21 | 22 | func (f andFilter) Equals(other Filter) bool { 23 | if other, ok := other.(andFilter); ok { 24 | return compareFilterList(f, other) 25 | } 26 | return false 27 | } 28 | 29 | type orFilter []Filter 30 | 31 | func Or(children ...Filter) ComparableFilter { 32 | return orFilter(children) 33 | } 34 | 35 | func (f orFilter) Accept(obj metav1.Object) bool { 36 | for _, child := range f { 37 | if child.Accept(obj) { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | func (f orFilter) Equals(other Filter) bool { 45 | if other, ok := other.(orFilter); ok { 46 | return compareFilterList(f, other) 47 | } 48 | return false 49 | } 50 | 51 | func compareFilterList(a []Filter, b []Filter) bool { 52 | if len(a) != len(b) { 53 | return false 54 | } 55 | 56 | // must be in same order 57 | for idx := range a { 58 | 59 | fa, ok := a[idx].(ComparableFilter) 60 | if !ok { 61 | return false 62 | } 63 | 64 | fb, ok := b[idx].(ComparableFilter) 65 | if !ok { 66 | return false 67 | } 68 | 69 | if !fa.Equals(fb) { 70 | return false 71 | } 72 | } 73 | 74 | return true 75 | } 76 | -------------------------------------------------------------------------------- /join/generated_rc_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AUTO GENERATED - DO NOT EDIT BY HAND 3 | */ 4 | 5 | package join 6 | 7 | import ( 8 | "context" 9 | 10 | logutil "github.com/boz/go-logutil" 11 | "github.com/boz/kcache/filter" 12 | "github.com/boz/kcache/types/pod" 13 | "github.com/boz/kcache/types/replicationcontroller" 14 | corev1 "k8s.io/api/core/v1" 15 | ) 16 | 17 | func RCPodsWith(ctx context.Context, 18 | srcController replicationcontroller.Controller, 19 | dstController pod.Publisher, 20 | filterFn func(...*corev1.ReplicationController) filter.ComparableFilter) (pod.Controller, error) { 21 | 22 | log := logutil.FromContextOrDefault(ctx) 23 | 24 | dst, err := dstController.CloneForFilter() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | update := func(_ *corev1.ReplicationController) { 30 | objs, err := srcController.Cache().List() 31 | if err != nil { 32 | log.Err(err, "join(replicationcontroller,pod: cache list") 33 | return 34 | } 35 | dst.Refilter(filterFn(objs...)) 36 | } 37 | 38 | handler := replicationcontroller.BuildHandler(). 39 | OnInitialize(func(objs []*corev1.ReplicationController) { dst.Refilter(filterFn(objs...)) }). 40 | OnCreate(update). 41 | OnUpdate(update). 42 | OnDelete(update). 43 | Create() 44 | 45 | monitor, err := replicationcontroller.NewMonitor(srcController, handler) 46 | if err != nil { 47 | dst.Close() 48 | return nil, log.Err(err, "join(replicationcontroller,pod): monitor") 49 | } 50 | 51 | go func() { 52 | <-dst.Done() 53 | monitor.Close() 54 | }() 55 | 56 | return dst, nil 57 | } 58 | -------------------------------------------------------------------------------- /subscription_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/testutil" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSubscription(t *testing.T) { 14 | testDoTestSubscription(t, "close", func(s subscription, _ chan struct{}) { s.Close() }) 15 | testDoTestSubscription(t, "stopch", func(_ subscription, stopch chan struct{}) { close(stopch) }) 16 | } 17 | 18 | func testDoTestSubscription(t *testing.T, name string, stopfn func(subscription, chan struct{})) { 19 | log := logutil.Default() 20 | ctx, cancel := context.WithCancel(context.Background()) 21 | defer cancel() 22 | 23 | readych := make(chan struct{}) 24 | stopch := make(chan struct{}) 25 | cache := newCache(ctx, log, stopch, filter.Null()) 26 | 27 | sub := newSubscription(log, stopch, readych, cache) 28 | defer sub.Close() 29 | 30 | testutil.AssertNotDone(t, name, sub) 31 | testutil.AssertNotReady(t, name, sub) 32 | 33 | evt := testGenEvent(EventTypeCreate, "a", "b", "1") 34 | sub.send(evt) 35 | 36 | select { 37 | case ev, ok := <-sub.Events(): 38 | assert.True(t, ok, name) 39 | assert.Equal(t, evt, ev, name) 40 | case <-testutil.AsyncWaitch(ctx): 41 | assert.Fail(t, name) 42 | } 43 | 44 | stopfn(sub, stopch) 45 | 46 | testutil.AssertDone(t, name, sub) 47 | testutil.AssertNotReady(t, name, sub) 48 | 49 | sub.send(evt) 50 | 51 | select { 52 | case _, ok := <-sub.Events(): 53 | assert.False(t, ok, name) 54 | case <-testutil.AsyncWaitch(ctx): 55 | assert.Fail(t, name) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /filter/composite_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "k8s.io/api/core/v1" 7 | 8 | "github.com/boz/kcache/filter" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAndFilter(t *testing.T) { 13 | f := filter.And() 14 | assert.True(t, f.Accept(&v1.Pod{})) 15 | 16 | f = filter.And(filter.Null()) 17 | assert.True(t, f.Accept(&v1.Pod{})) 18 | 19 | f = filter.And(filter.All()) 20 | assert.False(t, f.Accept(&v1.Pod{})) 21 | 22 | f = filter.And(filter.Null(), filter.All()) 23 | assert.False(t, f.Accept(&v1.Pod{})) 24 | 25 | a := filter.And() 26 | b := filter.And() 27 | assert.True(t, a.Equals(b)) 28 | 29 | a = filter.And(filter.Null()) 30 | b = filter.And() 31 | assert.False(t, a.Equals(b)) 32 | 33 | a = filter.And() 34 | b = filter.And(filter.Null()) 35 | assert.False(t, a.Equals(b)) 36 | 37 | a = filter.And(filter.Null()) 38 | b = filter.And(filter.Null()) 39 | assert.True(t, a.Equals(b)) 40 | 41 | a = filter.And(filter.Null()) 42 | b = filter.And(filter.All()) 43 | assert.False(t, a.Equals(b)) 44 | assert.False(t, b.Equals(a)) 45 | 46 | a = filter.And(filter.Null(), filter.All()) 47 | b = filter.And(filter.All(), filter.Null()) 48 | assert.False(t, a.Equals(b)) 49 | assert.False(t, b.Equals(a)) 50 | 51 | a = filter.And() 52 | b = filter.Or() 53 | assert.False(t, a.Equals(b)) 54 | } 55 | 56 | func TestOrFilter(t *testing.T) { 57 | f := filter.Or() 58 | assert.False(t, f.Accept(&v1.Pod{})) 59 | 60 | f = filter.Or(filter.Null()) 61 | assert.True(t, f.Accept(&v1.Pod{})) 62 | 63 | f = filter.Or(filter.All()) 64 | assert.False(t, f.Accept(&v1.Pod{})) 65 | 66 | f = filter.Or(filter.Null(), filter.All()) 67 | assert.True(t, f.Accept(&v1.Pod{})) 68 | 69 | a := filter.Or() 70 | b := filter.And() 71 | assert.False(t, a.Equals(b)) 72 | } 73 | -------------------------------------------------------------------------------- /ticker.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | type ticker interface { 9 | Next() <-chan int 10 | Reset() 11 | Stop() 12 | Done() <-chan struct{} 13 | } 14 | 15 | func newTicker(period time.Duration, fuzz float64) ticker { 16 | 17 | t := &_ticker{ 18 | period: period, 19 | fuzz: fuzz, 20 | nextch: make(chan int), 21 | resetch: make(chan bool), 22 | stopch: make(chan bool), 23 | donech: make(chan struct{}), 24 | } 25 | 26 | go t.run() 27 | 28 | return t 29 | } 30 | 31 | type _ticker struct { 32 | period time.Duration 33 | fuzz float64 34 | 35 | nextch chan int 36 | resetch chan bool 37 | stopch chan bool 38 | donech chan struct{} 39 | } 40 | 41 | func (t *_ticker) Next() <-chan int { 42 | return t.nextch 43 | } 44 | 45 | func (t *_ticker) Reset() { 46 | select { 47 | case t.resetch <- true: 48 | case <-t.donech: 49 | } 50 | } 51 | 52 | func (t *_ticker) Stop() { 53 | select { 54 | case t.stopch <- true: 55 | case <-t.donech: 56 | } 57 | } 58 | 59 | func (t *_ticker) Done() <-chan struct{} { 60 | return t.donech 61 | } 62 | 63 | func (t *_ticker) run() { 64 | defer close(t.donech) 65 | 66 | count := 0 67 | timer := time.NewTimer(t.nextPeriod()) 68 | 69 | var nextch chan int 70 | 71 | for { 72 | 73 | select { 74 | 75 | case <-t.resetch: 76 | if !timer.Stop() { 77 | <-timer.C 78 | } 79 | timer.Reset(t.nextPeriod()) 80 | nextch = nil 81 | 82 | case <-t.stopch: 83 | timer.Stop() 84 | return 85 | 86 | case <-timer.C: 87 | timer.Stop() 88 | nextch = t.nextch 89 | 90 | case nextch <- count: 91 | count++ 92 | nextch = nil 93 | timer.Reset(t.nextPeriod()) 94 | 95 | } 96 | } 97 | } 98 | 99 | func (t *_ticker) nextPeriod() time.Duration { 100 | delta := t.fuzz * float64(t.period) 101 | 102 | min := float64(t.period) - delta 103 | max := float64(t.period) + delta 104 | 105 | r := rand.Float64() 106 | 107 | return time.Duration(min + r*(max-min+1)) 108 | 109 | } 110 | -------------------------------------------------------------------------------- /types/service/filter.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/boz/kcache/filter" 7 | "github.com/boz/kcache/nsname" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/labels" 11 | ) 12 | 13 | // SelectorMatchFilter() removes all objects that are not services whose 14 | // selector matches the given target. 15 | func SelectorMatchFilter(target map[string]string) filter.ComparableFilter { 16 | return &serviceForFilter{target} 17 | } 18 | 19 | type serviceForFilter struct { 20 | target map[string]string 21 | } 22 | 23 | // Accept() returns true if the object is a Service whose 24 | // selector matches the target fields of the filter. 25 | func (f *serviceForFilter) Accept(obj metav1.Object) bool { 26 | svc, ok := obj.(*corev1.Service) 27 | 28 | if !ok { 29 | return false 30 | } 31 | 32 | if len(svc.Spec.Selector) == 0 || len(f.target) == 0 { 33 | return false 34 | } 35 | 36 | for k, v := range svc.Spec.Selector { 37 | if val, ok := f.target[k]; !ok || val != v { 38 | return false 39 | } 40 | } 41 | 42 | return true 43 | } 44 | 45 | func (f *serviceForFilter) Equals(other filter.Filter) bool { 46 | if other, ok := other.(*serviceForFilter); ok { 47 | return labels.Equals(f.target, other.target) 48 | } 49 | return false 50 | } 51 | 52 | func PodsFilter(services ...*corev1.Service) filter.ComparableFilter { 53 | 54 | // make a copy and sort 55 | svcs := make([]*corev1.Service, len(services)) 56 | copy(svcs, services) 57 | 58 | sort.Slice(svcs, func(i, j int) bool { 59 | if svcs[i].Namespace != svcs[j].Namespace { 60 | return svcs[i].Namespace < svcs[j].Namespace 61 | } 62 | return svcs[i].Name < svcs[j].Name 63 | }) 64 | 65 | var filters []filter.Filter 66 | 67 | for _, svc := range svcs { 68 | if len(svc.Spec.Selector) > 0 { 69 | sfilter := filter.Labels(svc.Spec.Selector) 70 | nsfilter := filter.NSName(nsname.New(svc.GetNamespace(), "")) 71 | filters = append(filters, filter.And(nsfilter, sfilter)) 72 | } 73 | } 74 | 75 | return filter.Or(filters...) 76 | } 77 | -------------------------------------------------------------------------------- /types/ingress/filter_test.go: -------------------------------------------------------------------------------- 1 | package ingress_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/ingress" 7 | "github.com/stretchr/testify/assert" 8 | 9 | "k8s.io/api/core/v1" 10 | "k8s.io/api/networking/v1beta1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func TestServicesFilter(t *testing.T) { 15 | 16 | gensvc := func(ns, name string) *v1.Service { 17 | return &v1.Service{ 18 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 19 | } 20 | } 21 | 22 | ing1 := &v1beta1.Ingress{ 23 | ObjectMeta: metav1.ObjectMeta{Namespace: "a", Name: "1"}, 24 | Spec: v1beta1.IngressSpec{ 25 | Backend: &v1beta1.IngressBackend{ 26 | ServiceName: "foo", 27 | }, 28 | }, 29 | } 30 | 31 | ing2 := &v1beta1.Ingress{ 32 | ObjectMeta: metav1.ObjectMeta{Namespace: "b", Name: "2"}, 33 | Spec: v1beta1.IngressSpec{ 34 | Rules: []v1beta1.IngressRule{ 35 | { 36 | IngressRuleValue: v1beta1.IngressRuleValue{ 37 | HTTP: &v1beta1.HTTPIngressRuleValue{ 38 | Paths: []v1beta1.HTTPIngressPath{ 39 | { 40 | Backend: v1beta1.IngressBackend{ 41 | ServiceName: "bar", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | } 51 | 52 | assert.True(t, ingress.ServicesFilter(ing1).Accept(gensvc("a", "foo"))) 53 | assert.False(t, ingress.ServicesFilter(ing1).Accept(gensvc("a", "bar"))) 54 | assert.False(t, ingress.ServicesFilter(ing1).Accept(gensvc("b", "foo"))) 55 | 56 | assert.True(t, ingress.ServicesFilter(ing2).Accept(gensvc("b", "bar"))) 57 | assert.False(t, ingress.ServicesFilter(ing2).Accept(gensvc("b", "foo"))) 58 | assert.False(t, ingress.ServicesFilter(ing2).Accept(gensvc("a", "bar"))) 59 | 60 | assert.True(t, ingress.ServicesFilter(ing1, ing2).Accept(gensvc("a", "foo"))) 61 | assert.True(t, ingress.ServicesFilter(ing1, ing2).Accept(gensvc("b", "bar"))) 62 | 63 | assert.True(t, ingress.ServicesFilter(ing1).Equals(ingress.ServicesFilter(ing1))) 64 | assert.False(t, ingress.ServicesFilter(ing1).Equals(ingress.ServicesFilter(ing2))) 65 | assert.True(t, ingress.ServicesFilter(ing1, ing2).Equals(ingress.ServicesFilter(ing2, ing1))) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /testutil/async.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var testAsyncWaitDuration time.Duration 13 | 14 | func init() { 15 | dstr := os.Getenv("KCACHE_TEST_ASYNC_DURATION") 16 | if dstr == "" { 17 | dstr = "10ms" 18 | } 19 | d, err := time.ParseDuration(dstr) 20 | if err != nil { 21 | panic("invalid KCACHE_TEST_ASYNC_DURATION: " + err.Error()) 22 | } 23 | testAsyncWaitDuration = d 24 | } 25 | 26 | type readyable interface { 27 | Ready() <-chan struct{} 28 | } 29 | 30 | type doneable interface { 31 | Done() <-chan struct{} 32 | } 33 | 34 | func Timerch(ctx context.Context, duration time.Duration) <-chan time.Time { 35 | t := time.NewTimer(duration) 36 | go func() { 37 | <-ctx.Done() 38 | t.Stop() 39 | select { 40 | case <-t.C: 41 | default: 42 | } 43 | }() 44 | return t.C 45 | } 46 | 47 | func AsyncWaitch(ctx context.Context) <-chan time.Time { 48 | return Timerch(ctx, testAsyncWaitDuration) 49 | } 50 | 51 | func AssertReady(t *testing.T, name string, obj readyable) { 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | defer cancel() 54 | select { 55 | case <-obj.Ready(): 56 | case <-AsyncWaitch(ctx): 57 | assert.Fail(t, "expected to be ready but wasn't: %v", name) 58 | } 59 | } 60 | 61 | func AssertNotReady(t *testing.T, name string, obj readyable) { 62 | ctx, cancel := context.WithCancel(context.Background()) 63 | defer cancel() 64 | select { 65 | case <-obj.Ready(): 66 | assert.Fail(t, "expected to not be ready but was: %v", name) 67 | case <-AsyncWaitch(ctx): 68 | } 69 | } 70 | 71 | func AssertDone(t *testing.T, name string, obj doneable) { 72 | ctx, cancel := context.WithCancel(context.Background()) 73 | defer cancel() 74 | select { 75 | case <-obj.Done(): 76 | case <-AsyncWaitch(ctx): 77 | assert.Fail(t, "expected to be done but wasn't: %v", name) 78 | } 79 | } 80 | 81 | func AssertNotDone(t *testing.T, name string, obj doneable) { 82 | ctx, cancel := context.WithCancel(context.Background()) 83 | defer cancel() 84 | select { 85 | case <-obj.Done(): 86 | assert.Fail(t, "expected to be not done but wasn: %v", name) 87 | case <-AsyncWaitch(ctx): 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /types/event/filter_test.go: -------------------------------------------------------------------------------- 1 | package event_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/event" 7 | "github.com/stretchr/testify/assert" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | "k8s.io/api/core/v1" 12 | ) 13 | 14 | func TestInvolvedForFilter(t *testing.T) { 15 | 16 | genpod := func(ns, name string) *v1.Pod { 17 | return &v1.Pod{ 18 | TypeMeta: metav1.TypeMeta{Kind: "pod"}, 19 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 20 | } 21 | } 22 | 23 | genevt := func(kind, ns, name string) *v1.Event { 24 | return &v1.Event{ 25 | InvolvedObject: v1.ObjectReference{ 26 | Kind: kind, 27 | Namespace: ns, 28 | Name: name, 29 | }, 30 | } 31 | } 32 | 33 | { 34 | f := event.InvolvedFilter("pod", "a", "b") 35 | assert.True(t, f.Accept(genevt("pod", "a", "b"))) 36 | assert.False(t, f.Accept(genevt("pod", "a", "c"))) 37 | assert.False(t, f.Accept(genevt("pod", "c", "b"))) 38 | assert.False(t, f.Accept(genevt("pod", "c", "c"))) 39 | assert.False(t, f.Accept(genevt("service", "a", "b"))) 40 | } 41 | 42 | { 43 | f := event.InvolvedFilter("pod", "a", "b") 44 | assert.True(t, f.Equals(event.InvolvedFilter("pod", "a", "b"))) 45 | assert.False(t, f.Equals(event.InvolvedFilter("pod", "a", "c"))) 46 | assert.False(t, f.Equals(event.InvolvedFilter("pod", "c", "b"))) 47 | assert.False(t, f.Equals(event.InvolvedFilter("pod", "c", "c"))) 48 | assert.False(t, f.Equals(event.InvolvedFilter("service", "a", "b"))) 49 | } 50 | 51 | { 52 | f := event.InvolvedObjectFilter(genpod("a", "b")) 53 | assert.True(t, f.Accept(genevt("pod", "a", "b"))) 54 | assert.False(t, f.Accept(genevt("pod", "a", "c"))) 55 | assert.False(t, f.Accept(genevt("pod", "c", "b"))) 56 | assert.False(t, f.Accept(genevt("pod", "c", "c"))) 57 | assert.False(t, f.Accept(genevt("service", "a", "b"))) 58 | 59 | assert.True(t, f.Equals(event.InvolvedFilter("pod", "a", "b"))) 60 | assert.False(t, f.Equals(event.InvolvedFilter("pod", "a", "c"))) 61 | assert.False(t, f.Equals(event.InvolvedFilter("pod", "c", "b"))) 62 | assert.False(t, f.Equals(event.InvolvedFilter("pod", "c", "c"))) 63 | assert.False(t, f.Equals(event.InvolvedFilter("service", "a", "b"))) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /types/replicationcontroller/filter_test.go: -------------------------------------------------------------------------------- 1 | package replicationcontroller_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/replicationcontroller" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func TestPodsFilter(t *testing.T) { 13 | 14 | genpod := func(labels map[string]string) *v1.Pod { 15 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels}} 16 | } 17 | 18 | gensvc := func(ns, name string, labels map[string]string) *v1.ReplicationController { 19 | return &v1.ReplicationController{ 20 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 21 | Spec: v1.ReplicationControllerSpec{Selector: labels}, 22 | } 23 | } 24 | 25 | p1 := genpod(map[string]string{"a": "1", "b": "1", "c": "x"}) 26 | p2 := genpod(map[string]string{"a": "2", "b": "2", "c": "x"}) 27 | 28 | s1 := gensvc("a", "1", map[string]string{"a": "1"}) 29 | s2 := gensvc("a", "2", map[string]string{"b": "2"}) 30 | s3 := gensvc("c", "1", map[string]string{"c": "x"}) 31 | s4 := gensvc("d", "1", map[string]string{"a": "0"}) 32 | 33 | assert.False(t, replicationcontroller.PodsFilter().Accept(p1)) 34 | assert.True(t, replicationcontroller.PodsFilter(s1).Accept(p1)) 35 | assert.False(t, replicationcontroller.PodsFilter(s1).Accept(p2)) 36 | assert.True(t, replicationcontroller.PodsFilter(s2).Accept(p2)) 37 | assert.False(t, replicationcontroller.PodsFilter(s2).Accept(p1)) 38 | 39 | assert.True(t, replicationcontroller.PodsFilter(s1, s2).Accept(p1)) 40 | 41 | assert.True(t, replicationcontroller.PodsFilter(s3).Accept(p1)) 42 | assert.True(t, replicationcontroller.PodsFilter(s3).Accept(p2)) 43 | 44 | assert.False(t, replicationcontroller.PodsFilter(s4).Accept(p1)) 45 | assert.False(t, replicationcontroller.PodsFilter(s4).Accept(p2)) 46 | 47 | assert.True(t, replicationcontroller.PodsFilter(s1).Equals(replicationcontroller.PodsFilter(s1))) 48 | assert.True(t, replicationcontroller.PodsFilter(s1, s2).Equals(replicationcontroller.PodsFilter(s1, s2))) 49 | assert.True(t, replicationcontroller.PodsFilter(s2, s1).Equals(replicationcontroller.PodsFilter(s1, s2))) 50 | assert.True(t, replicationcontroller.PodsFilter(s4, s3).Equals(replicationcontroller.PodsFilter(s3, s4))) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /subscription.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | lifecycle "github.com/boz/go-lifecycle" 5 | logutil "github.com/boz/go-logutil" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | const ( 10 | EventBufsiz = 100 11 | ) 12 | 13 | type Subscription interface { 14 | CacheController 15 | Events() <-chan Event 16 | Close() 17 | Done() <-chan struct{} 18 | Error() error 19 | } 20 | 21 | type subscription interface { 22 | Subscription 23 | send(Event) error 24 | } 25 | 26 | type _subscription struct { 27 | outch chan Event 28 | inch chan Event 29 | 30 | readych <-chan struct{} 31 | 32 | cache CacheReader 33 | 34 | log logutil.Log 35 | lc lifecycle.Lifecycle 36 | } 37 | 38 | func newSubscription(log logutil.Log, stopch <-chan struct{}, readych <-chan struct{}, cache CacheReader) subscription { 39 | log = log.WithComponent("subscription") 40 | 41 | lc := lifecycle.New() 42 | s := &_subscription{ 43 | readych: readych, 44 | inch: make(chan Event), 45 | outch: make(chan Event, EventBufsiz), 46 | cache: cache, 47 | log: log, 48 | lc: lc, 49 | } 50 | 51 | go s.lc.WatchChannel(stopch) 52 | 53 | go s.run() 54 | return s 55 | } 56 | 57 | func (s *_subscription) Ready() <-chan struct{} { 58 | return s.readych 59 | } 60 | 61 | func (s *_subscription) Events() <-chan Event { 62 | return s.outch 63 | } 64 | 65 | func (s *_subscription) Cache() CacheReader { 66 | return s.cache 67 | } 68 | 69 | func (s *_subscription) Close() { 70 | s.lc.ShutdownAsync(nil) 71 | } 72 | 73 | func (s *_subscription) Done() <-chan struct{} { 74 | return s.lc.Done() 75 | } 76 | 77 | func (s *_subscription) Error() error { 78 | return s.lc.Error() 79 | } 80 | 81 | func (s *_subscription) send(ev Event) error { 82 | select { 83 | case s.inch <- ev: 84 | return nil 85 | case <-s.lc.ShuttingDown(): 86 | return errors.WithStack(ErrNotRunning) 87 | } 88 | } 89 | 90 | func (s *_subscription) run() { 91 | defer s.lc.ShutdownCompleted() 92 | defer close(s.outch) 93 | 94 | for { 95 | select { 96 | case err := <-s.lc.ShutdownRequest(): 97 | s.log.Debugf("shutdown requested: %v", err) 98 | s.lc.ShutdownInitiated(err) 99 | return 100 | case evt := <-s.inch: 101 | select { 102 | case s.outch <- evt: 103 | default: 104 | s.log.Warnf("event buffer overrun") 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /join/join.go: -------------------------------------------------------------------------------- 1 | package join 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/boz/kcache/types/daemonset" 7 | "github.com/boz/kcache/types/deployment" 8 | "github.com/boz/kcache/types/ingress" 9 | "github.com/boz/kcache/types/job" 10 | "github.com/boz/kcache/types/pod" 11 | "github.com/boz/kcache/types/replicaset" 12 | "github.com/boz/kcache/types/replicationcontroller" 13 | "github.com/boz/kcache/types/service" 14 | "github.com/boz/kcache/types/statefulset" 15 | ) 16 | 17 | func ServicePods(ctx context.Context, 18 | src service.Controller, dst pod.Publisher) (pod.Controller, error) { 19 | return ServicePodsWith(ctx, src, dst, service.PodsFilter) 20 | } 21 | 22 | func RCPods(ctx context.Context, 23 | src replicationcontroller.Controller, dst pod.Publisher) (pod.Controller, error) { 24 | return RCPodsWith(ctx, src, dst, replicationcontroller.PodsFilter) 25 | } 26 | 27 | func RSPods(ctx context.Context, 28 | src replicaset.Controller, dst pod.Publisher) (pod.Controller, error) { 29 | return RSPodsWith(ctx, src, dst, replicaset.PodsFilter) 30 | } 31 | 32 | func DeploymentPods(ctx context.Context, 33 | src deployment.Controller, dst pod.Publisher) (pod.Controller, error) { 34 | return DeploymentPodsWith(ctx, src, dst, deployment.PodsFilter) 35 | } 36 | 37 | func StatefulSetPods(ctx context.Context, 38 | src statefulset.Controller, dst pod.Publisher) (pod.Controller, error) { 39 | return StatefulSetPodsWith(ctx, src, dst, statefulset.PodsFilter) 40 | } 41 | 42 | func JobPods(ctx context.Context, 43 | src job.Controller, dst pod.Publisher) (pod.Controller, error) { 44 | return JobPodsWith(ctx, src, dst, job.PodsFilter) 45 | } 46 | 47 | func DaemonSetPods(ctx context.Context, 48 | src daemonset.Controller, dst pod.Publisher) (pod.Controller, error) { 49 | return DaemonSetPodsWith(ctx, src, dst, daemonset.PodsFilter) 50 | } 51 | 52 | func IngressServices(ctx context.Context, 53 | src ingress.Controller, dst service.Publisher) (service.Controller, error) { 54 | return IngressServicesWith(ctx, src, dst, ingress.ServicesFilter) 55 | } 56 | 57 | func IngressPods(ctx context.Context, srcbase ingress.Controller, svcbase service.Controller, dstbase pod.Controller) (pod.Controller, error) { 58 | svcs, err := IngressServices(ctx, srcbase, svcbase) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | pods, err := ServicePods(ctx, svcs, dstbase) 64 | if err != nil { 65 | svcs.Close() 66 | return nil, err 67 | } 68 | return pods, nil 69 | } 70 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/watch" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | type ListFn func(context.Context, metav1.ListOptions) (runtime.Object, error) 15 | type WatchFn func(context.Context, metav1.ListOptions) (watch.Interface, error) 16 | 17 | type ListClient interface { 18 | List(context.Context, metav1.ListOptions) (runtime.Object, error) 19 | } 20 | 21 | type WatchClient interface { 22 | Watch(context.Context, metav1.ListOptions) (watch.Interface, error) 23 | } 24 | 25 | type Client interface { 26 | ListClient 27 | WatchClient 28 | } 29 | 30 | type client struct { 31 | list ListFn 32 | watch WatchFn 33 | } 34 | 35 | func NewListClient(fn ListFn) ListClient { 36 | return &client{list: fn} 37 | } 38 | 39 | func NewWatchClient(fn WatchFn) WatchClient { 40 | return &client{watch: fn} 41 | } 42 | 43 | func NewClient(list ListFn, watch WatchFn) Client { 44 | return &client{list, watch} 45 | } 46 | 47 | func (c *client) List(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { 48 | return c.list(ctx, opts) 49 | } 50 | 51 | func (c *client) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 52 | return c.watch(ctx, opts) 53 | } 54 | 55 | type restRequester interface { 56 | Get() *rest.Request 57 | } 58 | 59 | func ForResource( 60 | c restRequester, res string, ns string) Client { 61 | return NewClient( 62 | makeResourceListFn(c, res, ns), 63 | makeResourceWatchFn(c, res, ns), 64 | ) 65 | } 66 | 67 | func makeResourceListFn( 68 | c restRequester, res string, ns string) ListFn { 69 | return func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { 70 | result := c.Get(). 71 | Namespace(ns). 72 | Resource(res). 73 | VersionedParams(&opts, scheme.ParameterCodec). 74 | Do(ctx) 75 | err := result.Error() 76 | if err != nil { 77 | return nil, fmt.Errorf("listing %s in %s: %w", res, ns, err) 78 | } 79 | return result.Get() 80 | } 81 | } 82 | 83 | func makeResourceWatchFn( 84 | c restRequester, res string, ns string) WatchFn { 85 | 86 | return func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 87 | return c.Get(). 88 | Prefix("watch"). 89 | Namespace(ns). 90 | Resource(res). 91 | VersionedParams(&opts, scheme.ParameterCodec). 92 | Watch(ctx) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/boz/kcache 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/boz/go-lifecycle v0.1.0 7 | github.com/boz/go-logutil v0.1.0 8 | github.com/cheekybits/genny v1.0.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/stretchr/testify v1.7.0 11 | golang.org/x/tools v0.1.5 12 | k8s.io/api v0.24.3 13 | k8s.io/apimachinery v0.24.3 14 | k8s.io/client-go v0.24.3 15 | ) 16 | 17 | require ( 18 | github.com/PuerkitoBio/purell v1.1.1 // indirect 19 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/emicklei/go-restful v2.9.5+incompatible // indirect 22 | github.com/go-logr/logr v1.2.0 // indirect 23 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 24 | github.com/go-openapi/jsonreference v0.19.5 // indirect 25 | github.com/go-openapi/swag v0.19.14 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/golang/protobuf v1.5.2 // indirect 28 | github.com/google/gnostic v0.5.7-v3refs // indirect 29 | github.com/google/gofuzz v1.1.0 // indirect 30 | github.com/imdario/mergo v0.3.5 // indirect 31 | github.com/josharian/intern v1.0.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/mailru/easyjson v0.7.6 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/stretchr/objx v0.1.1 // indirect 40 | golang.org/x/mod v0.4.2 // indirect 41 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 42 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 43 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 44 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 45 | golang.org/x/text v0.3.7 // indirect 46 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 47 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 48 | google.golang.org/appengine v1.6.7 // indirect 49 | google.golang.org/protobuf v1.27.1 // indirect 50 | gopkg.in/inf.v0 v0.9.1 // indirect 51 | gopkg.in/yaml.v2 v2.4.0 // indirect 52 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 53 | k8s.io/klog/v2 v2.60.1 // indirect 54 | k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect 55 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 56 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 58 | sigs.k8s.io/yaml v1.2.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /types/job/filter_test.go: -------------------------------------------------------------------------------- 1 | package job_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/job" 7 | "github.com/stretchr/testify/assert" 8 | v1 "k8s.io/api/core/v1" 9 | batchv1 "k8s.io/api/batch/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestPodsFilter_selector(t *testing.T) { 14 | 15 | genselector := func(ns, name string, labels map[string]string) *batchv1.Job { 16 | return &batchv1.Job{ 17 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 18 | Spec: batchv1.JobSpec{ 19 | Selector: &metav1.LabelSelector{ 20 | MatchLabels: labels, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | gentemplate := func(ns, name string, labels map[string]string) *batchv1.Job { 27 | return &batchv1.Job{ 28 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 29 | Spec: batchv1.JobSpec{ 30 | Template: v1.PodTemplateSpec{ 31 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | testPodsFilter(t, genselector, "selector") 38 | testPodsFilter(t, gentemplate, "template") 39 | } 40 | 41 | func testPodsFilter(t *testing.T, gen func(string, string, map[string]string) *batchv1.Job, ctx string) { 42 | 43 | genpod := func(ns string, labels map[string]string) *v1.Pod { 44 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels, Namespace: ns}} 45 | } 46 | 47 | p1 := genpod("a", map[string]string{"a": "1", "b": "1", "c": "x"}) 48 | p2 := genpod("a", map[string]string{"a": "2", "b": "2", "c": "x"}) 49 | 50 | s1 := gen("a", "1", map[string]string{"a": "1"}) 51 | s2 := gen("a", "2", map[string]string{"b": "2"}) 52 | s3 := gen("a", "3", map[string]string{"c": "x"}) 53 | s4 := gen("a", "4", map[string]string{"a": "0"}) 54 | s5 := gen("b", "1", map[string]string{"a": "1"}) 55 | 56 | assert.False(t, job.PodsFilter().Accept(p1), ctx) 57 | assert.True(t, job.PodsFilter(s1).Accept(p1), ctx) 58 | assert.False(t, job.PodsFilter(s1).Accept(p2), ctx) 59 | assert.True(t, job.PodsFilter(s2).Accept(p2), ctx) 60 | assert.False(t, job.PodsFilter(s2).Accept(p1), ctx) 61 | assert.False(t, job.PodsFilter(s5).Accept(p1), ctx) 62 | 63 | assert.True(t, job.PodsFilter(s1, s2).Accept(p1), ctx) 64 | 65 | assert.True(t, job.PodsFilter(s3).Accept(p1), ctx) 66 | assert.True(t, job.PodsFilter(s3).Accept(p2), ctx) 67 | 68 | assert.False(t, job.PodsFilter(s4).Accept(p1), ctx) 69 | assert.False(t, job.PodsFilter(s4).Accept(p2), ctx) 70 | 71 | assert.True(t, job.PodsFilter(s1).Equals(job.PodsFilter(s1)), ctx) 72 | assert.True(t, job.PodsFilter(s1, s2).Equals(job.PodsFilter(s1, s2)), ctx) 73 | assert.True(t, job.PodsFilter(s2, s1).Equals(job.PodsFilter(s1, s2)), ctx) 74 | assert.True(t, job.PodsFilter(s4, s3).Equals(job.PodsFilter(s3, s4)), ctx) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /join/gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "text/template" 8 | 9 | "golang.org/x/tools/imports" 10 | ) 11 | 12 | type joinDef struct { 13 | SrcName string 14 | SrcPkg string 15 | SrcType string 16 | DstName string 17 | DstPkg string 18 | } 19 | 20 | func main() { 21 | 22 | if len(os.Args) != 6 { 23 | fmt.Fprintf(os.Stderr, "USAGE: %v: \n", os.Args[0]) 24 | os.Exit(1) 25 | } 26 | 27 | def := joinDef{ 28 | SrcName: os.Args[1], 29 | SrcPkg: os.Args[2], 30 | SrcType: os.Args[3], 31 | DstName: os.Args[4], 32 | DstPkg: os.Args[5], 33 | } 34 | 35 | buf := processTemplate(&def) 36 | buf = processImports(buf) 37 | 38 | if _, err := os.Stdout.Write(buf); err != nil { 39 | panic(err.Error()) 40 | } 41 | } 42 | 43 | func processTemplate(def *joinDef) []byte { 44 | buf := &bytes.Buffer{} 45 | if err := joinTemplate.Execute(buf, &def); err != nil { 46 | panic(err) 47 | } 48 | return buf.Bytes() 49 | } 50 | 51 | func processImports(in []byte) []byte { 52 | out, err := imports.Process("./join/generated.go", in, nil) 53 | if err != nil { 54 | panic(err) 55 | } 56 | return out 57 | } 58 | 59 | var joinTemplate = template.Must(template.New("join").Parse(`/* 60 | * AUTO GENERATED - DO NOT EDIT BY HAND 61 | */ 62 | 63 | package join 64 | 65 | import ( 66 | "context" 67 | 68 | logutil "github.com/boz/go-logutil" 69 | "github.com/boz/kcache/filter" 70 | "github.com/boz/kcache/types/{{.SrcPkg}}" 71 | "github.com/boz/kcache/types/{{.DstPkg}}" 72 | ) 73 | 74 | func {{.SrcName}}{{.DstName}}sWith(ctx context.Context, 75 | srcController {{.SrcPkg}}.Controller, 76 | dstController {{.DstPkg}}.Publisher, 77 | filterFn func(...{{.SrcType}}) filter.ComparableFilter) ({{.DstPkg}}.Controller, error) { 78 | 79 | log := logutil.FromContextOrDefault(ctx) 80 | 81 | dst, err := dstController.CloneForFilter() 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | update := func(_ {{.SrcType}}) { 87 | objs, err := srcController.Cache().List() 88 | if err != nil { 89 | log.Err(err, "join({{.SrcPkg}},{{.DstPkg}}: cache list") 90 | return 91 | } 92 | dst.Refilter(filterFn(objs...)) 93 | } 94 | 95 | handler := {{.SrcPkg}}.BuildHandler(). 96 | OnInitialize(func(objs []{{.SrcType}}) { dst.Refilter(filterFn(objs...)) }). 97 | OnCreate(update). 98 | OnUpdate(update). 99 | OnDelete(update). 100 | Create() 101 | 102 | monitor, err := {{.SrcPkg}}.NewMonitor(srcController, handler) 103 | if err != nil { 104 | dst.Close() 105 | return nil, log.Err(err, "join({{.SrcPkg}},{{.DstPkg}}): monitor") 106 | } 107 | 108 | go func() { 109 | <-dst.Done() 110 | monitor.Close() 111 | }() 112 | 113 | return dst, nil 114 | }`)) 115 | -------------------------------------------------------------------------------- /types/daemonset/filter_test.go: -------------------------------------------------------------------------------- 1 | package daemonset_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/daemonset" 7 | "github.com/stretchr/testify/assert" 8 | v1beta1 "k8s.io/api/apps/v1" 9 | "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestPodsFilter_selector(t *testing.T) { 14 | 15 | genselector := func(ns, name string, labels map[string]string) *v1beta1.DaemonSet { 16 | return &v1beta1.DaemonSet{ 17 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 18 | Spec: v1beta1.DaemonSetSpec{ 19 | Selector: &metav1.LabelSelector{ 20 | MatchLabels: labels, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | gentemplate := func(ns, name string, labels map[string]string) *v1beta1.DaemonSet { 27 | return &v1beta1.DaemonSet{ 28 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 29 | Spec: v1beta1.DaemonSetSpec{ 30 | Template: v1.PodTemplateSpec{ 31 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | testPodsFilter(t, genselector, "selector") 38 | testPodsFilter(t, gentemplate, "template") 39 | } 40 | 41 | func testPodsFilter(t *testing.T, gen func(string, string, map[string]string) *v1beta1.DaemonSet, ctx string) { 42 | 43 | genpod := func(ns string, labels map[string]string) *v1.Pod { 44 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels, Namespace: ns}} 45 | } 46 | 47 | p1 := genpod("a", map[string]string{"a": "1", "b": "1", "c": "x"}) 48 | p2 := genpod("a", map[string]string{"a": "2", "b": "2", "c": "x"}) 49 | 50 | s1 := gen("a", "1", map[string]string{"a": "1"}) 51 | s2 := gen("a", "2", map[string]string{"b": "2"}) 52 | s3 := gen("a", "3", map[string]string{"c": "x"}) 53 | s4 := gen("a", "4", map[string]string{"a": "0"}) 54 | s5 := gen("b", "1", map[string]string{"a": "1"}) 55 | 56 | assert.False(t, daemonset.PodsFilter().Accept(p1), ctx) 57 | assert.True(t, daemonset.PodsFilter(s1).Accept(p1), ctx) 58 | assert.False(t, daemonset.PodsFilter(s1).Accept(p2), ctx) 59 | assert.True(t, daemonset.PodsFilter(s2).Accept(p2), ctx) 60 | assert.False(t, daemonset.PodsFilter(s2).Accept(p1), ctx) 61 | assert.False(t, daemonset.PodsFilter(s5).Accept(p1), ctx) 62 | 63 | assert.True(t, daemonset.PodsFilter(s1, s2).Accept(p1), ctx) 64 | 65 | assert.True(t, daemonset.PodsFilter(s3).Accept(p1), ctx) 66 | assert.True(t, daemonset.PodsFilter(s3).Accept(p2), ctx) 67 | 68 | assert.False(t, daemonset.PodsFilter(s4).Accept(p1), ctx) 69 | assert.False(t, daemonset.PodsFilter(s4).Accept(p2), ctx) 70 | 71 | assert.True(t, daemonset.PodsFilter(s1).Equals(daemonset.PodsFilter(s1)), ctx) 72 | assert.True(t, daemonset.PodsFilter(s1, s2).Equals(daemonset.PodsFilter(s1, s2)), ctx) 73 | assert.True(t, daemonset.PodsFilter(s2, s1).Equals(daemonset.PodsFilter(s1, s2)), ctx) 74 | assert.True(t, daemonset.PodsFilter(s4, s3).Equals(daemonset.PodsFilter(s3, s4)), ctx) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /types/deployment/filter_test.go: -------------------------------------------------------------------------------- 1 | package deployment_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/deployment" 7 | "github.com/stretchr/testify/assert" 8 | appsv1 "k8s.io/api/apps/v1" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestPodsFilter_selector(t *testing.T) { 14 | 15 | genselector := func(ns, name string, labels map[string]string) *appsv1.Deployment { 16 | return &appsv1.Deployment{ 17 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 18 | Spec: appsv1.DeploymentSpec{ 19 | Selector: &metav1.LabelSelector{ 20 | MatchLabels: labels, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | gentemplate := func(ns, name string, labels map[string]string) *appsv1.Deployment { 27 | return &appsv1.Deployment{ 28 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 29 | Spec: appsv1.DeploymentSpec{ 30 | Template: v1.PodTemplateSpec{ 31 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | testPodsFilter(t, genselector, "selector") 38 | testPodsFilter(t, gentemplate, "template") 39 | } 40 | 41 | func testPodsFilter(t *testing.T, gen func(string, string, map[string]string) *appsv1.Deployment, ctx string) { 42 | 43 | genpod := func(ns string, labels map[string]string) *v1.Pod { 44 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels, Namespace: ns}} 45 | } 46 | 47 | p1 := genpod("a", map[string]string{"a": "1", "b": "1", "c": "x"}) 48 | p2 := genpod("a", map[string]string{"a": "2", "b": "2", "c": "x"}) 49 | 50 | s1 := gen("a", "1", map[string]string{"a": "1"}) 51 | s2 := gen("a", "2", map[string]string{"b": "2"}) 52 | s3 := gen("a", "3", map[string]string{"c": "x"}) 53 | s4 := gen("a", "4", map[string]string{"a": "0"}) 54 | s5 := gen("b", "1", map[string]string{"a": "1"}) 55 | 56 | assert.False(t, deployment.PodsFilter().Accept(p1), ctx) 57 | assert.True(t, deployment.PodsFilter(s1).Accept(p1), ctx) 58 | assert.False(t, deployment.PodsFilter(s1).Accept(p2), ctx) 59 | assert.True(t, deployment.PodsFilter(s2).Accept(p2), ctx) 60 | assert.False(t, deployment.PodsFilter(s2).Accept(p1), ctx) 61 | assert.False(t, deployment.PodsFilter(s5).Accept(p1), ctx) 62 | 63 | assert.True(t, deployment.PodsFilter(s1, s2).Accept(p1), ctx) 64 | 65 | assert.True(t, deployment.PodsFilter(s3).Accept(p1), ctx) 66 | assert.True(t, deployment.PodsFilter(s3).Accept(p2), ctx) 67 | 68 | assert.False(t, deployment.PodsFilter(s4).Accept(p1), ctx) 69 | assert.False(t, deployment.PodsFilter(s4).Accept(p2), ctx) 70 | 71 | assert.True(t, deployment.PodsFilter(s1).Equals(deployment.PodsFilter(s1)), ctx) 72 | assert.True(t, deployment.PodsFilter(s1, s2).Equals(deployment.PodsFilter(s1, s2)), ctx) 73 | assert.True(t, deployment.PodsFilter(s2, s1).Equals(deployment.PodsFilter(s1, s2)), ctx) 74 | assert.True(t, deployment.PodsFilter(s4, s3).Equals(deployment.PodsFilter(s3, s4)), ctx) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /types/replicaset/filter_test.go: -------------------------------------------------------------------------------- 1 | package replicaset_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/replicaset" 7 | "github.com/stretchr/testify/assert" 8 | v1beta1 "k8s.io/api/apps/v1" 9 | "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestPodsFilter_selector(t *testing.T) { 14 | 15 | genselector := func(ns, name string, labels map[string]string) *v1beta1.ReplicaSet { 16 | return &v1beta1.ReplicaSet{ 17 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 18 | Spec: v1beta1.ReplicaSetSpec{ 19 | Selector: &metav1.LabelSelector{ 20 | MatchLabels: labels, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | gentemplate := func(ns, name string, labels map[string]string) *v1beta1.ReplicaSet { 27 | return &v1beta1.ReplicaSet{ 28 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 29 | Spec: v1beta1.ReplicaSetSpec{ 30 | Template: v1.PodTemplateSpec{ 31 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | testPodsFilter(t, genselector, "selector") 38 | testPodsFilter(t, gentemplate, "template") 39 | } 40 | 41 | func testPodsFilter(t *testing.T, gen func(string, string, map[string]string) *v1beta1.ReplicaSet, ctx string) { 42 | 43 | genpod := func(ns string, labels map[string]string) *v1.Pod { 44 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels, Namespace: ns}} 45 | } 46 | 47 | p1 := genpod("a", map[string]string{"a": "1", "b": "1", "c": "x"}) 48 | p2 := genpod("a", map[string]string{"a": "2", "b": "2", "c": "x"}) 49 | 50 | s1 := gen("a", "1", map[string]string{"a": "1"}) 51 | s2 := gen("a", "2", map[string]string{"b": "2"}) 52 | s3 := gen("a", "3", map[string]string{"c": "x"}) 53 | s4 := gen("a", "4", map[string]string{"a": "0"}) 54 | s5 := gen("b", "1", map[string]string{"a": "1"}) 55 | 56 | assert.False(t, replicaset.PodsFilter().Accept(p1), ctx) 57 | assert.True(t, replicaset.PodsFilter(s1).Accept(p1), ctx) 58 | assert.False(t, replicaset.PodsFilter(s1).Accept(p2), ctx) 59 | assert.True(t, replicaset.PodsFilter(s2).Accept(p2), ctx) 60 | assert.False(t, replicaset.PodsFilter(s2).Accept(p1), ctx) 61 | assert.False(t, replicaset.PodsFilter(s5).Accept(p1), ctx) 62 | 63 | assert.True(t, replicaset.PodsFilter(s1, s2).Accept(p1), ctx) 64 | 65 | assert.True(t, replicaset.PodsFilter(s3).Accept(p1), ctx) 66 | assert.True(t, replicaset.PodsFilter(s3).Accept(p2), ctx) 67 | 68 | assert.False(t, replicaset.PodsFilter(s4).Accept(p1), ctx) 69 | assert.False(t, replicaset.PodsFilter(s4).Accept(p2), ctx) 70 | 71 | assert.True(t, replicaset.PodsFilter(s1).Equals(replicaset.PodsFilter(s1)), ctx) 72 | assert.True(t, replicaset.PodsFilter(s1, s2).Equals(replicaset.PodsFilter(s1, s2)), ctx) 73 | assert.True(t, replicaset.PodsFilter(s2, s1).Equals(replicaset.PodsFilter(s1, s2)), ctx) 74 | assert.True(t, replicaset.PodsFilter(s4, s3).Equals(replicaset.PodsFilter(s3, s4)), ctx) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /types/statefulset/filter_test.go: -------------------------------------------------------------------------------- 1 | package statefulset_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/statefulset" 7 | "github.com/stretchr/testify/assert" 8 | appsv1 "k8s.io/api/apps/v1" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestPodsFilter_selector(t *testing.T) { 14 | 15 | genselector := func(ns, name string, labels map[string]string) *appsv1.StatefulSet { 16 | return &appsv1.StatefulSet{ 17 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 18 | Spec: appsv1.StatefulSetSpec{ 19 | Selector: &metav1.LabelSelector{ 20 | MatchLabels: labels, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | gentemplate := func(ns, name string, labels map[string]string) *appsv1.StatefulSet { 27 | return &appsv1.StatefulSet{ 28 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 29 | Spec: appsv1.StatefulSetSpec{ 30 | Template: v1.PodTemplateSpec{ 31 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | testPodsFilter(t, genselector, "selector") 38 | testPodsFilter(t, gentemplate, "template") 39 | } 40 | 41 | func testPodsFilter(t *testing.T, gen func(string, string, map[string]string) *appsv1.StatefulSet, ctx string) { 42 | 43 | genpod := func(ns string, labels map[string]string) *v1.Pod { 44 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels, Namespace: ns}} 45 | } 46 | 47 | p1 := genpod("a", map[string]string{"a": "1", "b": "1", "c": "x"}) 48 | p2 := genpod("a", map[string]string{"a": "2", "b": "2", "c": "x"}) 49 | 50 | s1 := gen("a", "1", map[string]string{"a": "1"}) 51 | s2 := gen("a", "2", map[string]string{"b": "2"}) 52 | s3 := gen("a", "3", map[string]string{"c": "x"}) 53 | s4 := gen("a", "4", map[string]string{"a": "0"}) 54 | s5 := gen("b", "1", map[string]string{"a": "1"}) 55 | 56 | assert.False(t, statefulset.PodsFilter().Accept(p1), ctx) 57 | assert.True(t, statefulset.PodsFilter(s1).Accept(p1), ctx) 58 | assert.False(t, statefulset.PodsFilter(s1).Accept(p2), ctx) 59 | assert.True(t, statefulset.PodsFilter(s2).Accept(p2), ctx) 60 | assert.False(t, statefulset.PodsFilter(s2).Accept(p1), ctx) 61 | assert.False(t, statefulset.PodsFilter(s5).Accept(p1), ctx) 62 | 63 | assert.True(t, statefulset.PodsFilter(s1, s2).Accept(p1), ctx) 64 | 65 | assert.True(t, statefulset.PodsFilter(s3).Accept(p1), ctx) 66 | assert.True(t, statefulset.PodsFilter(s3).Accept(p2), ctx) 67 | 68 | assert.False(t, statefulset.PodsFilter(s4).Accept(p1), ctx) 69 | assert.False(t, statefulset.PodsFilter(s4).Accept(p2), ctx) 70 | 71 | assert.True(t, statefulset.PodsFilter(s1).Equals(statefulset.PodsFilter(s1)), ctx) 72 | assert.True(t, statefulset.PodsFilter(s1, s2).Equals(statefulset.PodsFilter(s1, s2)), ctx) 73 | assert.True(t, statefulset.PodsFilter(s2, s1).Equals(statefulset.PodsFilter(s1, s2)), ctx) 74 | assert.True(t, statefulset.PodsFilter(s4, s3).Equals(statefulset.PodsFilter(s3, s4)), ctx) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /filter/labels_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/filter" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func TestLabels(t *testing.T) { 13 | 14 | target := map[string]string{"a": "1"} 15 | tsuper := map[string]string{"a": "1", "b": "2"} 16 | tmiss := map[string]string{"a": "2"} 17 | 18 | f := filter.Labels(target) 19 | fnil := filter.Labels(nil) 20 | 21 | gen := func(labels map[string]string) metav1.Object { 22 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels}} 23 | } 24 | 25 | assert.True(t, f.Accept(gen(target))) 26 | assert.True(t, f.Accept(gen(tsuper))) 27 | assert.False(t, f.Accept(gen(tmiss))) 28 | 29 | assert.True(t, fnil.Accept(gen(target))) 30 | assert.True(t, fnil.Accept(gen(tsuper))) 31 | assert.True(t, fnil.Accept(gen(tmiss))) 32 | 33 | assert.True(t, f.Equals(f)) 34 | assert.False(t, f.Equals(fnil)) 35 | 36 | assert.True(t, fnil.Equals(fnil)) 37 | assert.False(t, fnil.Equals(f)) 38 | 39 | fempty := filter.Labels(map[string]string{}) 40 | assert.True(t, fempty.Accept(gen(target))) 41 | assert.True(t, fempty.Accept(gen(tsuper))) 42 | assert.True(t, fempty.Accept(gen(tmiss))) 43 | assert.True(t, fempty.Accept(gen(map[string]string{}))) 44 | assert.True(t, fempty.Equals(fempty)) 45 | } 46 | 47 | func TestLabelSelector(t *testing.T) { 48 | target := map[string]string{"a": "1"} 49 | tsuper := map[string]string{"a": "1", "b": "2"} 50 | tmiss := map[string]string{"a": "2"} 51 | 52 | gen := func(labels map[string]string) metav1.Object { 53 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels}} 54 | } 55 | 56 | fnil := filter.LabelSelector(nil) 57 | fempty := filter.LabelSelector(&metav1.LabelSelector{}) 58 | fmatch := filter.LabelSelector(&metav1.LabelSelector{MatchLabels: target}) 59 | 60 | fexpr := filter.LabelSelector(&metav1.LabelSelector{ 61 | MatchExpressions: []metav1.LabelSelectorRequirement{ 62 | {Key: "a", Operator: metav1.LabelSelectorOpIn, Values: []string{"1"}}, 63 | }, 64 | }) 65 | 66 | assert.False(t, fnil.Accept(gen(target))) 67 | assert.True(t, fempty.Accept(gen(target))) 68 | assert.True(t, fmatch.Accept(gen(target))) 69 | assert.True(t, fexpr.Accept(gen(target))) 70 | 71 | assert.False(t, fnil.Accept(gen(tsuper))) 72 | assert.True(t, fempty.Accept(gen(tsuper))) 73 | assert.True(t, fmatch.Accept(gen(tsuper))) 74 | assert.True(t, fexpr.Accept(gen(tsuper))) 75 | 76 | assert.False(t, fnil.Accept(gen(tmiss))) 77 | assert.True(t, fempty.Accept(gen(tmiss))) 78 | assert.False(t, fmatch.Accept(gen(tmiss))) 79 | assert.False(t, fexpr.Accept(gen(tmiss))) 80 | 81 | assert.True(t, fnil.Equals(fnil)) 82 | assert.True(t, fempty.Equals(fempty)) 83 | assert.True(t, fmatch.Equals(fmatch)) 84 | assert.True(t, fexpr.Equals(fexpr)) 85 | 86 | assert.False(t, fnil.Equals(fempty)) 87 | assert.False(t, fempty.Equals(fnil)) 88 | 89 | assert.False(t, fnil.Equals(fmatch)) 90 | assert.False(t, fnil.Equals(fmatch)) 91 | assert.False(t, fmatch.Equals(fexpr)) 92 | assert.False(t, fexpr.Equals(fmatch)) 93 | assert.False(t, fexpr.Equals(filter.All())) 94 | } 95 | -------------------------------------------------------------------------------- /monitor_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/testutil" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | func TestMonitor(t *testing.T) { 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | defer cancel() 18 | 19 | log := logutil.Default() 20 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 21 | publisher := newPublisher(log, parent) 22 | defer parent.Close() 23 | 24 | icalled := make(chan bool) 25 | ccalled := make(chan bool) 26 | ucalled := make(chan bool) 27 | dcalled := make(chan bool) 28 | 29 | h := BuildHandler().OnInitialize(func(objs []metav1.Object) { 30 | if assert.Len(t, objs, 1) { 31 | assert.Equal(t, "a", objs[0].GetNamespace()) 32 | assert.Equal(t, "b", objs[0].GetName()) 33 | } 34 | close(icalled) 35 | }).OnCreate(func(obj metav1.Object) { 36 | assert.Equal(t, "b", obj.GetNamespace()) 37 | assert.Equal(t, "b", obj.GetName()) 38 | close(ccalled) 39 | }).OnUpdate(func(obj metav1.Object) { 40 | assert.Equal(t, "b", obj.GetNamespace()) 41 | assert.Equal(t, "b", obj.GetName()) 42 | close(ucalled) 43 | }).OnDelete(func(obj metav1.Object) { 44 | assert.Equal(t, "b", obj.GetNamespace()) 45 | assert.Equal(t, "b", obj.GetName()) 46 | close(dcalled) 47 | }).Create() 48 | 49 | cache.sync([]metav1.Object{testGenPod("a", "b", "1")}) 50 | 51 | m, err := NewMonitor(publisher, h) 52 | assert.NoError(t, err) 53 | 54 | close(readych) 55 | 56 | parent.send(testGenEvent(EventTypeCreate, "b", "b", "1")) 57 | parent.send(testGenEvent(EventTypeUpdate, "b", "b", "2")) 58 | parent.send(testGenEvent(EventTypeDelete, "b", "b", "3")) 59 | 60 | select { 61 | case <-icalled: 62 | case <-testutil.AsyncWaitch(ctx): 63 | assert.Fail(t, "initialize not called") 64 | } 65 | 66 | select { 67 | case <-ccalled: 68 | case <-testutil.AsyncWaitch(ctx): 69 | assert.Fail(t, "create not called") 70 | } 71 | 72 | select { 73 | case <-ucalled: 74 | case <-testutil.AsyncWaitch(ctx): 75 | assert.Fail(t, "update not called") 76 | } 77 | 78 | select { 79 | case <-dcalled: 80 | case <-testutil.AsyncWaitch(ctx): 81 | assert.Fail(t, "delete not called") 82 | } 83 | 84 | m.Close() 85 | testutil.AssertDone(t, "monitor", m) 86 | } 87 | 88 | func TestMonitor_lifecycle_close_early(t *testing.T) { 89 | ctx, cancel := context.WithCancel(context.Background()) 90 | defer cancel() 91 | 92 | log := logutil.Default() 93 | parent, _, readych := testNewSubscription(t, log, filter.Null()) 94 | publisher := newPublisher(log, parent) 95 | 96 | calledch := make(chan bool) 97 | 98 | h := BuildHandler().OnInitialize(func(_ []metav1.Object) { 99 | close(calledch) 100 | }).Create() 101 | 102 | m, err := NewMonitor(publisher, h) 103 | require.NoError(t, err) 104 | 105 | publisher.Close() 106 | 107 | testutil.AssertDone(t, "publisher", m) 108 | 109 | close(readych) 110 | 111 | select { 112 | case <-calledch: 113 | assert.Fail(t, "initialize called") 114 | case <-testutil.AsyncWaitch(ctx): 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/boz/kcache/nsname" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | type Filter interface { 11 | 12 | // Accept() should return true if the given object passes the filter. 13 | Accept(metav1.Object) bool 14 | } 15 | 16 | type ComparableFilter interface { 17 | Filter 18 | Equals(Filter) bool 19 | } 20 | 21 | // Null() returns a filter whose Accept() is always true. 22 | func Null() ComparableFilter { 23 | return nullFilter{} 24 | } 25 | 26 | type nullFilter struct{} 27 | 28 | func (nullFilter) Accept(_ metav1.Object) bool { 29 | return true 30 | } 31 | 32 | func (nullFilter) Equals(other Filter) bool { 33 | _, ok := other.(nullFilter) 34 | return ok 35 | } 36 | 37 | type allFilter struct{} 38 | 39 | // All() returns a filter whose Accept() is always false. 40 | func All() ComparableFilter { 41 | return allFilter{} 42 | } 43 | 44 | func (allFilter) Accept(_ metav1.Object) bool { 45 | return false 46 | } 47 | 48 | func (allFilter) Equals(other Filter) bool { 49 | _, ok := other.(allFilter) 50 | return ok 51 | } 52 | 53 | func Not(child Filter) ComparableFilter { 54 | return ¬Filter{child} 55 | } 56 | 57 | type notFilter struct { 58 | child Filter 59 | } 60 | 61 | func (f *notFilter) Accept(obj metav1.Object) bool { 62 | return !f.child.Accept(obj) 63 | } 64 | 65 | func (f *notFilter) Equals(other Filter) bool { 66 | if other, ok := other.(*notFilter); ok { 67 | if child, ok := f.child.(ComparableFilter); ok { 68 | return child.Equals(other.child) 69 | } 70 | } 71 | return false 72 | } 73 | 74 | // NSName() returns a filter whose Accept() returns true 75 | // if the object's namespace and name matches one of the given 76 | // NSNames. 77 | func NSName(ids ...nsname.NSName) ComparableFilter { 78 | fullset := make(map[nsname.NSName]bool) 79 | var partials []nsname.NSName 80 | 81 | for _, id := range ids { 82 | if id.Namespace != "" && id.Name != "" { 83 | fullset[id] = true 84 | } else { 85 | partials = append(partials, id) 86 | } 87 | } 88 | return nsNameFilter{fullset, partials} 89 | } 90 | 91 | type nsNameFilter struct { 92 | fullset map[nsname.NSName]bool 93 | partials []nsname.NSName 94 | } 95 | 96 | func (f nsNameFilter) Accept(obj metav1.Object) bool { 97 | key := nsname.ForObject(obj) 98 | 99 | if _, ok := f.fullset[key]; ok { 100 | return true 101 | } 102 | 103 | for _, id := range f.partials { 104 | switch { 105 | case id.Namespace == "": 106 | if id.Name == key.Name { 107 | return true 108 | } 109 | case id.Name == "": 110 | if id.Namespace == key.Namespace { 111 | return true 112 | } 113 | } 114 | } 115 | return false 116 | } 117 | 118 | func (f nsNameFilter) Equals(other Filter) bool { 119 | return reflect.DeepEqual(f, other) 120 | } 121 | 122 | func FiltersEqual(f1, f2 Filter) bool { 123 | if f1 == nil && f2 == nil { 124 | return true 125 | } 126 | 127 | if f1 == nil || f2 == nil { 128 | return false 129 | } 130 | 131 | if f1, ok := f1.(ComparableFilter); ok { 132 | return f1.Equals(f2) 133 | } 134 | 135 | return false 136 | } 137 | 138 | func FN(fn func(metav1.Object) bool) Filter { 139 | return fnFilter(fn) 140 | } 141 | 142 | type fnFilter func(metav1.Object) bool 143 | 144 | func (f fnFilter) Accept(obj metav1.Object) bool { 145 | return f(obj) 146 | } 147 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/clientcmd" 14 | 15 | logutil "github.com/boz/go-logutil" 16 | "github.com/boz/kcache" 17 | "github.com/boz/kcache/types/pod" 18 | 19 | lr "github.com/boz/go-logutil/logrus" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | func main() { 24 | logger := logrus.New() 25 | logger.Level = logrus.DebugLevel 26 | 27 | log := lr.New(logger) 28 | ctx := context.Background() 29 | 30 | cs := getClientset(log) 31 | 32 | client := pod.NewClient(cs, metav1.NamespaceAll) 33 | 34 | controller, err := kcache.NewController(ctx, log, client) 35 | if err != nil { 36 | log.ErrFatal(err, "kcache.NewController()") 37 | } 38 | 39 | go watchSignals(log, controller) 40 | 41 | defer controller.Close() 42 | 43 | subscription, err := controller.Subscribe() 44 | if err != nil { 45 | log.ErrFatal(err, "subscribe") 46 | } 47 | 48 | select { 49 | case <-subscription.Ready(): 50 | case <-subscription.Done(): 51 | return 52 | } 53 | 54 | list, err := subscription.Cache().List() 55 | if err != nil { 56 | log.ErrFatal(err, "Cache().List()") 57 | } 58 | 59 | for _, pod := range list { 60 | fmt.Printf("%v/%v: %v\n", pod.GetNamespace(), pod.GetName(), pod.GetResourceVersion()) 61 | } 62 | 63 | for { 64 | select { 65 | case ev, ok := <-subscription.Events(): 66 | if !ok { 67 | return 68 | } 69 | obj := ev.Resource() 70 | fmt.Printf("event: %v: %v/%v[%v]\n", ev.Type(), obj.GetNamespace(), obj.GetName(), obj.GetResourceVersion()) 71 | 72 | cnobj, err := subscription.Cache().Get(obj.GetNamespace(), obj.GetName()) 73 | if err != nil { 74 | log.ErrWarn(err, "Get()") 75 | continue 76 | } 77 | 78 | if ev.Type() == kcache.EventTypeDelete { 79 | if cnobj != nil { 80 | log.Warnf("Get(deleted) != nil") 81 | } 82 | continue 83 | } 84 | 85 | if cnobj == nil { 86 | log.Warnf("Get() -> nil") 87 | continue 88 | } 89 | 90 | fmt.Printf("Get: %v/%v[%v]\n", cnobj.GetNamespace(), cnobj.GetName(), cnobj.GetResourceVersion()) 91 | } 92 | } 93 | } 94 | 95 | func getRESTClient(log logutil.Log) rest.Interface { 96 | clientset := getClientset(log) 97 | 98 | client := clientset.CoreV1().RESTClient() 99 | return client 100 | } 101 | 102 | func getClientset(log logutil.Log) *kubernetes.Clientset { 103 | kconfig, err := getKubeRESTConfig() 104 | if err != nil { 105 | log.ErrFatal(err, "can't get kube client") 106 | } 107 | 108 | clientset, err := kubernetes.NewForConfig(kconfig) 109 | if err != nil { 110 | log.ErrFatal(err, "can't get clientset") 111 | } 112 | return clientset 113 | } 114 | 115 | func getKubeRESTConfig() (*rest.Config, error) { 116 | /* 117 | config, err := rest.InClusterConfig() 118 | if err == nil { 119 | return config, err 120 | } 121 | */ 122 | 123 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 124 | clientcmd.NewDefaultClientConfigLoadingRules(), 125 | &clientcmd.ConfigOverrides{}, 126 | ).ClientConfig() 127 | } 128 | 129 | func watchSignals(log logutil.Log, controller kcache.Controller) { 130 | sigch := make(chan os.Signal, 1) 131 | signal.Notify(sigch, syscall.SIGINT, syscall.SIGQUIT) 132 | 133 | select { 134 | case <-controller.Done(): 135 | case <-sigch: 136 | controller.Close() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lister.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | 12 | lifecycle "github.com/boz/go-lifecycle" 13 | logutil "github.com/boz/go-logutil" 14 | "github.com/boz/kcache/client" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | var ( 19 | errInvalidType = fmt.Errorf("Invalid type") 20 | ) 21 | 22 | const ( 23 | defaultRefreshPeriod = time.Minute 24 | defaultRefreshFuzz = 0.10 25 | ) 26 | 27 | type lister interface { 28 | Result() <-chan listResult 29 | Done() <-chan struct{} 30 | Error() error 31 | } 32 | 33 | type listResult struct { 34 | list runtime.Object 35 | err error 36 | } 37 | 38 | type _lister struct { 39 | client client.ListClient 40 | period time.Duration 41 | resultch chan listResult 42 | 43 | log logutil.Log 44 | lc lifecycle.Lifecycle 45 | ctx context.Context 46 | } 47 | 48 | func newLister(ctx context.Context, log logutil.Log, stopch <-chan struct{}, period time.Duration, client client.ListClient) *_lister { 49 | log = log.WithComponent("lister") 50 | 51 | l := &_lister{ 52 | client: client, 53 | period: period, 54 | resultch: make(chan listResult), 55 | log: log, 56 | lc: lifecycle.New(), 57 | ctx: ctx, 58 | } 59 | 60 | go l.lc.WatchContext(ctx) 61 | go l.lc.WatchChannel(stopch) 62 | 63 | go l.run() 64 | 65 | return l 66 | } 67 | 68 | func (l *_lister) Result() <-chan listResult { 69 | return l.resultch 70 | } 71 | 72 | func (l *_lister) Done() <-chan struct{} { 73 | return l.lc.Done() 74 | } 75 | 76 | func (l *_lister) Error() error { 77 | return l.lc.Error() 78 | } 79 | 80 | func (l *_lister) run() { 81 | defer l.lc.ShutdownCompleted() 82 | 83 | var resultch chan listResult 84 | var result listResult 85 | 86 | runch, donech := l.list() 87 | 88 | ticker := newTicker(l.period, defaultRefreshFuzz) 89 | var tickch <-chan int 90 | 91 | mainloop: 92 | for { 93 | select { 94 | case <-tickch: 95 | runch, donech = l.list() 96 | tickch = nil 97 | 98 | case result = <-runch: 99 | resultch = l.resultch 100 | runch = nil 101 | 102 | case resultch <- result: 103 | ticker.Reset() 104 | resultch = nil 105 | tickch = ticker.Next() 106 | 107 | case err := <-l.lc.ShutdownRequest(): 108 | l.lc.ShutdownInitiated(err) 109 | break mainloop 110 | } 111 | } 112 | 113 | ticker.Stop() 114 | <-ticker.Done() 115 | <-donech 116 | } 117 | 118 | func (l *_lister) list() (<-chan listResult, <-chan struct{}) { 119 | runch := make(chan listResult, 1) 120 | donech := make(chan struct{}) 121 | ctx, cancel := context.WithCancel(l.ctx) 122 | 123 | go func() { 124 | defer cancel() 125 | select { 126 | case <-l.lc.ShuttingDown(): 127 | case <-donech: 128 | } 129 | }() 130 | 131 | go func() { 132 | defer close(donech) 133 | runch <- l.executeList(ctx) 134 | }() 135 | 136 | return runch, donech 137 | } 138 | 139 | func (l *_lister) executeList(ctx context.Context) listResult { 140 | list, err := l.client.List(ctx, v1.ListOptions{}) 141 | 142 | if err != nil { 143 | if err != context.Canceled { 144 | l.log.Errorf("client list: %v", err) 145 | } 146 | return listResult{nil, errors.Wrap(err, "client list")} 147 | } 148 | 149 | if _, ok := list.(meta.List); !ok { 150 | l.log.Errorf("invalid type: %T", list) 151 | return listResult{nil, errors.WithStack(errInvalidType)} 152 | } 153 | 154 | return listResult{list, nil} 155 | } 156 | -------------------------------------------------------------------------------- /types/service/filter_test.go: -------------------------------------------------------------------------------- 1 | package service_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/boz/kcache/types/service" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func TestSelectorMatchFilter(t *testing.T) { 13 | target := map[string]string{"a": "1"} 14 | tsuper := map[string]string{"a": "1", "b": "2"} 15 | tmiss := map[string]string{"a": "2"} 16 | 17 | gensvc := func(labels map[string]string) metav1.Object { 18 | return &v1.Service{Spec: v1.ServiceSpec{Selector: labels}} 19 | } 20 | 21 | { 22 | f := service.SelectorMatchFilter(target) 23 | assert.True(t, f.Accept(gensvc(target))) 24 | assert.False(t, f.Accept(gensvc(tsuper))) 25 | assert.False(t, f.Accept(gensvc(tmiss))) 26 | assert.False(t, f.Accept(gensvc(nil))) 27 | 28 | assert.False(t, f.Accept(&v1.Pod{})) 29 | } 30 | 31 | { 32 | f := service.SelectorMatchFilter(tsuper) 33 | assert.True(t, f.Accept(gensvc(target))) 34 | assert.True(t, f.Accept(gensvc(tsuper))) 35 | assert.False(t, f.Accept(gensvc(tmiss))) 36 | } 37 | 38 | { 39 | f := service.SelectorMatchFilter(nil) 40 | assert.False(t, f.Accept(gensvc(target))) 41 | } 42 | 43 | { 44 | f := service.SelectorMatchFilter(target) 45 | fsuper := service.SelectorMatchFilter(tsuper) 46 | fnil := service.SelectorMatchFilter(nil) 47 | 48 | assert.True(t, f.Equals(f)) 49 | assert.False(t, f.Equals(fsuper)) 50 | assert.False(t, f.Equals(fnil)) 51 | 52 | assert.True(t, fnil.Equals(fnil)) 53 | assert.False(t, fnil.Equals(fsuper)) 54 | assert.False(t, fnil.Equals(f)) 55 | 56 | assert.True(t, fsuper.Equals(fsuper)) 57 | assert.False(t, fsuper.Equals(fnil)) 58 | assert.False(t, fsuper.Equals(f)) 59 | } 60 | } 61 | 62 | func TestPodsFilter(t *testing.T) { 63 | 64 | genpod := func(ns string, labels map[string]string) *v1.Pod { 65 | return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: labels, Namespace: ns}} 66 | } 67 | 68 | gensvc := func(ns, name string, labels map[string]string) *v1.Service { 69 | return &v1.Service{ 70 | ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, 71 | Spec: v1.ServiceSpec{Selector: labels}, 72 | } 73 | } 74 | 75 | p1 := genpod("a", map[string]string{"a": "1", "b": "1", "c": "x"}) 76 | p2 := genpod("a", map[string]string{"a": "2", "b": "2", "c": "x"}) 77 | 78 | s1 := gensvc("a", "1", map[string]string{"a": "1"}) 79 | s2 := gensvc("a", "2", map[string]string{"b": "2"}) 80 | s3 := gensvc("a", "3", map[string]string{"c": "x"}) 81 | s4 := gensvc("a", "4", map[string]string{"a": "0"}) 82 | s5 := gensvc("b", "1", map[string]string{"a": "1"}) 83 | 84 | assert.False(t, service.PodsFilter().Accept(p1)) 85 | assert.True(t, service.PodsFilter(s1).Accept(p1)) 86 | assert.False(t, service.PodsFilter(s1).Accept(p2)) 87 | assert.True(t, service.PodsFilter(s2).Accept(p2)) 88 | assert.False(t, service.PodsFilter(s2).Accept(p1)) 89 | assert.False(t, service.PodsFilter(s5).Accept(p1)) 90 | 91 | assert.True(t, service.PodsFilter(s1, s2).Accept(p1)) 92 | 93 | assert.True(t, service.PodsFilter(s3).Accept(p1)) 94 | assert.True(t, service.PodsFilter(s3).Accept(p2)) 95 | 96 | assert.False(t, service.PodsFilter(s4).Accept(p1)) 97 | assert.False(t, service.PodsFilter(s4).Accept(p2)) 98 | 99 | assert.True(t, service.PodsFilter(s1).Equals(service.PodsFilter(s1))) 100 | assert.True(t, service.PodsFilter(s1, s2).Equals(service.PodsFilter(s1, s2))) 101 | assert.True(t, service.PodsFilter(s2, s1).Equals(service.PodsFilter(s1, s2))) 102 | assert.True(t, service.PodsFilter(s4, s3).Equals(service.PodsFilter(s3, s4))) 103 | 104 | } 105 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | lifecycle "github.com/boz/go-lifecycle" 8 | logutil "github.com/boz/go-logutil" 9 | "github.com/boz/kcache/client" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | const ( 14 | watchRetryDelay = time.Second 15 | ) 16 | 17 | type watcher interface { 18 | reset(string) error 19 | events() <-chan Event 20 | 21 | Done() <-chan struct{} 22 | Error() error 23 | } 24 | 25 | type _watcher struct { 26 | version string 27 | 28 | client client.WatchClient 29 | 30 | resetch chan string 31 | evtch chan chan (<-chan Event) 32 | 33 | log logutil.Log 34 | lc lifecycle.Lifecycle 35 | ctx context.Context 36 | } 37 | 38 | func newWatcher(ctx context.Context, log logutil.Log, stopch <-chan struct{}, client client.WatchClient) watcher { 39 | log = log.WithComponent("watcher") 40 | lc := lifecycle.New() 41 | 42 | w := &_watcher{ 43 | client: client, 44 | resetch: make(chan string), 45 | evtch: make(chan chan (<-chan Event)), 46 | log: log, 47 | lc: lc, 48 | ctx: ctx, 49 | } 50 | 51 | go w.lc.WatchContext(ctx) 52 | go w.lc.WatchChannel(stopch) 53 | go w.run() 54 | 55 | return w 56 | } 57 | 58 | func (w *_watcher) reset(vsn string) error { 59 | select { 60 | case w.resetch <- vsn: 61 | return nil 62 | case <-w.lc.ShuttingDown(): 63 | return errors.WithStack(ErrNotRunning) 64 | } 65 | } 66 | 67 | func (w *_watcher) events() <-chan Event { 68 | req := make(chan (<-chan Event), 1) 69 | select { 70 | case w.evtch <- req: 71 | return <-req 72 | case <-w.lc.ShuttingDown(): 73 | return nil 74 | } 75 | } 76 | 77 | func (w *_watcher) Done() <-chan struct{} { 78 | return w.lc.Done() 79 | } 80 | 81 | func (w *_watcher) Error() error { 82 | return w.lc.Error() 83 | } 84 | 85 | func (w *_watcher) run() { 86 | defer w.lc.ShutdownCompleted() 87 | 88 | ctx, cancel := context.WithCancel(w.ctx) 89 | defer cancel() 90 | 91 | var session watchSession = nullWatchSession{} 92 | var outch chan Event 93 | 94 | var curVersion string 95 | 96 | var retry *time.Timer 97 | 98 | mainloop: 99 | for { 100 | 101 | select { 102 | case err := <-w.lc.ShutdownRequest(): 103 | w.log.Debugf("shutdown request: %v", err) 104 | w.lc.ShutdownInitiated(err) 105 | break mainloop 106 | 107 | case vsn := <-w.resetch: 108 | w.log.Debugf("ressetting to version %v", vsn) 109 | 110 | if retry != nil { 111 | retry.Stop() 112 | retry = nil 113 | } 114 | 115 | session.stop() 116 | session = newWatchSession(ctx, w.log, w.client, vsn) 117 | outch = make(chan Event, EventBufsiz) 118 | curVersion = vsn 119 | 120 | case <-session.done(): 121 | w.log.Debugf("session done. retrying version %v in %v", curVersion, watchRetryDelay) 122 | 123 | session.stop() 124 | session = nullWatchSession{} 125 | outch = nil 126 | retry = w.scheduleRetry(w.resetch, curVersion) 127 | 128 | case evt := <-session.events(): 129 | 130 | select { 131 | case outch <- evt: 132 | default: 133 | w.log.Errorf("output buffer full") 134 | } 135 | 136 | curVersion = evt.Resource().GetResourceVersion() 137 | 138 | w.log.Debugf("session event: %v version: %v", evt, curVersion) 139 | 140 | case reqch := <-w.evtch: 141 | reqch <- outch 142 | } 143 | } 144 | 145 | cancel() 146 | if retry != nil { 147 | retry.Stop() 148 | } 149 | 150 | if donech := session.done(); donech != nil { 151 | <-donech 152 | } 153 | } 154 | 155 | func (w *_watcher) scheduleRetry(ch chan string, vsn string) *time.Timer { 156 | return time.AfterFunc(watchRetryDelay, func() { 157 | select { 158 | case ch <- vsn: 159 | case <-w.lc.ShuttingDown(): 160 | } 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /monitor.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | lifecycle "github.com/boz/go-lifecycle" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | type Monitor interface { 9 | Close() 10 | Done() <-chan struct{} 11 | Error() error 12 | } 13 | 14 | type Handler interface { 15 | OnInitialize([]metav1.Object) 16 | OnCreate(metav1.Object) 17 | OnUpdate(metav1.Object) 18 | OnDelete(metav1.Object) 19 | } 20 | 21 | type HandlerBuilder interface { 22 | OnInitialize(func([]metav1.Object)) HandlerBuilder 23 | OnCreate(func(metav1.Object)) HandlerBuilder 24 | OnUpdate(func(metav1.Object)) HandlerBuilder 25 | OnDelete(func(metav1.Object)) HandlerBuilder 26 | Create() Handler 27 | } 28 | 29 | func BuildHandler() HandlerBuilder { 30 | return &handlerBuilder{} 31 | } 32 | 33 | type handler struct { 34 | onInitialize func([]metav1.Object) 35 | onCreate func(metav1.Object) 36 | onUpdate func(metav1.Object) 37 | onDelete func(metav1.Object) 38 | } 39 | 40 | type handlerBuilder handler 41 | 42 | func (hb *handlerBuilder) OnInitialize(fn func([]metav1.Object)) HandlerBuilder { 43 | hb.onInitialize = fn 44 | return hb 45 | } 46 | 47 | func (hb *handlerBuilder) OnCreate(fn func(metav1.Object)) HandlerBuilder { 48 | hb.onCreate = fn 49 | return hb 50 | } 51 | 52 | func (hb *handlerBuilder) OnUpdate(fn func(metav1.Object)) HandlerBuilder { 53 | hb.onUpdate = fn 54 | return hb 55 | } 56 | 57 | func (hb *handlerBuilder) OnDelete(fn func(metav1.Object)) HandlerBuilder { 58 | hb.onDelete = fn 59 | return hb 60 | } 61 | 62 | func (hb *handlerBuilder) Create() Handler { 63 | return handler(*hb) 64 | } 65 | 66 | func (h handler) OnInitialize(objs []metav1.Object) { 67 | if h.onInitialize != nil { 68 | h.onInitialize(objs) 69 | } 70 | } 71 | 72 | func (h handler) OnCreate(obj metav1.Object) { 73 | if h.onCreate != nil { 74 | h.onCreate(obj) 75 | } 76 | } 77 | 78 | func (h handler) OnUpdate(obj metav1.Object) { 79 | if h.onUpdate != nil { 80 | h.onUpdate(obj) 81 | } 82 | } 83 | 84 | func (h handler) OnDelete(obj metav1.Object) { 85 | if h.onDelete != nil { 86 | h.onDelete(obj) 87 | } 88 | } 89 | 90 | func NewMonitor(publisher Publisher, handler Handler) (Monitor, error) { 91 | sub, err := publisher.Subscribe() 92 | if err != nil { 93 | return nil, err 94 | } 95 | m := &monitor{sub, handler, lifecycle.New()} 96 | go m.run() 97 | return m, nil 98 | } 99 | 100 | type monitor struct { 101 | sub Subscription 102 | handler Handler 103 | lc lifecycle.Lifecycle 104 | } 105 | 106 | func (m *monitor) run() { 107 | defer m.lc.ShutdownCompleted() 108 | 109 | select { 110 | case <-m.sub.Done(): 111 | m.lc.ShutdownInitiated(nil) 112 | return 113 | case <-m.sub.Ready(): 114 | objs, err := m.sub.Cache().List() 115 | if err != nil { 116 | m.lc.ShutdownInitiated(err) 117 | m.sub.Close() 118 | <-m.sub.Done() 119 | return 120 | } 121 | m.handler.OnInitialize(objs) 122 | } 123 | 124 | for { 125 | select { 126 | case <-m.sub.Done(): 127 | m.lc.ShutdownInitiated(nil) 128 | return 129 | case ev, ok := <-m.sub.Events(): 130 | if !ok { 131 | m.lc.ShutdownInitiated(nil) 132 | <-m.sub.Done() 133 | return 134 | } 135 | switch ev.Type() { 136 | case EventTypeCreate: 137 | m.handler.OnCreate(ev.Resource()) 138 | case EventTypeUpdate: 139 | m.handler.OnUpdate(ev.Resource()) 140 | case EventTypeDelete: 141 | m.handler.OnDelete(ev.Resource()) 142 | } 143 | } 144 | } 145 | 146 | } 147 | 148 | func (m *monitor) Close() { 149 | m.sub.Close() 150 | } 151 | 152 | func (m *monitor) Done() <-chan struct{} { 153 | return m.lc.Done() 154 | } 155 | 156 | func (m *monitor) Error() error { 157 | if err := m.lc.Error(); err != nil { 158 | return err 159 | } 160 | return m.sub.Error() 161 | } 162 | -------------------------------------------------------------------------------- /watch_session.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | 6 | lifecycle "github.com/boz/go-lifecycle" 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/client" 9 | "github.com/pkg/errors" 10 | "k8s.io/apimachinery/pkg/api/meta" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/watch" 13 | ) 14 | 15 | type watchSession interface { 16 | events() <-chan Event 17 | done() <-chan struct{} 18 | stop() 19 | Error() error 20 | } 21 | 22 | type nullWatchSession struct{} 23 | 24 | func (nullWatchSession) events() <-chan Event { return nil } 25 | func (nullWatchSession) done() <-chan struct{} { return nil } 26 | func (nullWatchSession) stop() {} 27 | func (nullWatchSession) Error() error { return nil } 28 | 29 | type _watchSession struct { 30 | client client.WatchClient 31 | version string 32 | 33 | outch chan Event 34 | 35 | ctx context.Context 36 | cancel context.CancelFunc 37 | log logutil.Log 38 | lc lifecycle.Lifecycle 39 | } 40 | 41 | func newWatchSession(ctx context.Context, log logutil.Log, client client.WatchClient, version string) watchSession { 42 | lc := lifecycle.New() 43 | 44 | ctx, cancel := context.WithCancel(ctx) 45 | 46 | s := &_watchSession{ 47 | client: client, 48 | version: version, 49 | outch: make(chan Event, EventBufsiz), 50 | ctx: ctx, 51 | cancel: cancel, 52 | log: log.WithComponent("watch-session"), 53 | lc: lc, 54 | } 55 | 56 | go lc.WatchContext(ctx) 57 | go s.run() 58 | return s 59 | } 60 | 61 | func (s *_watchSession) done() <-chan struct{} { 62 | return s.lc.Done() 63 | } 64 | 65 | func (s *_watchSession) stop() { 66 | s.lc.ShutdownAsync(nil) 67 | } 68 | 69 | func (s *_watchSession) Error() error { 70 | return s.lc.Error() 71 | } 72 | 73 | func (s *_watchSession) events() <-chan Event { 74 | return s.outch 75 | } 76 | 77 | func (s *_watchSession) run() { 78 | defer s.lc.ShutdownCompleted() 79 | defer s.cancel() 80 | 81 | conn, err := s.connect() 82 | if err != nil { 83 | s.log.Debugf("connecting to server: %v", err) 84 | s.lc.ShutdownInitiated(errors.Wrap(err, "connecting to server")) 85 | return 86 | } 87 | 88 | defer conn.Stop() 89 | 90 | for { 91 | select { 92 | 93 | case err := <-s.lc.ShutdownRequest(): 94 | 95 | s.lc.ShutdownInitiated(err) 96 | return 97 | 98 | case kevt, ok := <-conn.ResultChan(): 99 | 100 | if !ok { 101 | s.lc.ShutdownInitiated(nil) 102 | return 103 | } 104 | 105 | if status, ok := kevt.Object.(*metav1.Status); ok { 106 | s.logStatus(status) 107 | continue 108 | } 109 | 110 | obj, err := meta.Accessor(kevt.Object) 111 | if err != nil { 112 | s.lc.ShutdownInitiated(errors.Wrap(err, "meta accessor")) 113 | return 114 | } 115 | 116 | var evt Event 117 | 118 | switch kevt.Type { 119 | case watch.Added: 120 | evt = NewEvent(EventTypeCreate, obj) 121 | case watch.Modified: 122 | evt = NewEvent(EventTypeUpdate, obj) 123 | case watch.Deleted: 124 | evt = NewEvent(EventTypeDelete, obj) 125 | } 126 | 127 | if evt == nil { 128 | s.log.Debugf("unknown event type: %v", kevt.Type) 129 | continue 130 | } 131 | 132 | select { 133 | case s.outch <- evt: 134 | default: 135 | s.log.Warnf("output buffer full; event missed.") 136 | } 137 | 138 | } 139 | } 140 | } 141 | 142 | func (s *_watchSession) connect() (watch.Interface, error) { 143 | response, err := s.client.Watch(s.ctx, metav1.ListOptions{ 144 | ResourceVersion: s.version, 145 | Watch: true, 146 | }) 147 | return response, err 148 | } 149 | 150 | func (s *_watchSession) logStatus(status *metav1.Status) { 151 | s.log.Debugf("STATUS: %v %v %v [code: %v vsn: %v]", status.Status, status.Message, status.Reason, status.Code, status.GetResourceVersion()) 152 | } 153 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO := GO111MODULE=on go 2 | 3 | build: 4 | $(GO) build ./... 5 | 6 | test: 7 | $(GO) test ./... 8 | 9 | test-full: example 10 | $(GO) test -race ./... 11 | 12 | test-cover: 13 | $(GO) test -coverprofile=coverage.txt -covermode=count -coverpkg="./..." $$(go list ./... | grep -v join/gen | grep -v types/gen ) 14 | curl -s https://codecov.io/bash | bash 15 | 16 | 17 | install-deps: 18 | $(GO) mod download 19 | 20 | generate: generate-types generate-type-tests generate-joins 21 | 22 | generate-types: 23 | genny -in=types/gen/template.go -out=types/pod/generated.go -pkg=pod gen 'ObjectType=*corev1.Pod' 24 | genny -in=types/gen/template.go -out=types/ingress/generated.go -pkg=ingress gen 'ObjectType=*networkingv1beta1.Ingress' 25 | genny -in=types/gen/template.go -out=types/secret/generated.go -pkg=secret gen 'ObjectType=*corev1.Secret' 26 | genny -in=types/gen/template.go -out=types/service/generated.go -pkg=service gen 'ObjectType=*corev1.Service' 27 | genny -in=types/gen/template.go -out=types/event/generated.go -pkg=event gen 'ObjectType=*corev1.Event' 28 | genny -in=types/gen/template.go -out=types/node/generated.go -pkg=node gen 'ObjectType=*corev1.Node' 29 | genny -in=types/gen/template.go -out=types/replicationcontroller/generated.go -pkg=replicationcontroller gen 'ObjectType=*corev1.ReplicationController' 30 | genny -in=types/gen/template.go -out=types/replicaset/generated.go -pkg=replicaset gen 'ObjectType=*appsv1.ReplicaSet' 31 | genny -in=types/gen/template.go -out=types/deployment/generated.go -pkg=deployment gen 'ObjectType=*appsv1.Deployment' 32 | genny -in=types/gen/template.go -out=types/job/generated.go -pkg=job gen 'ObjectType=*batchv1.Job' 33 | genny -in=types/gen/template.go -out=types/daemonset/generated.go -pkg=daemonset gen 'ObjectType=*appsv1.DaemonSet' 34 | genny -in=types/gen/template.go -out=types/statefulset/generated.go -pkg=statefulset gen 'ObjectType=*appsv1.StatefulSet' 35 | goimports -w types/**/generated.go 36 | $(GO) build ./types/... 37 | 38 | generate-type-tests: 39 | $(GO) build -o ./types/gen/gen ./types/gen 40 | ./types/gen/gen corev1.Pod > types/pod/generated_test.go 41 | ./types/gen/gen networkingv1beta1.Ingress > types/ingress/generated_test.go 42 | ./types/gen/gen corev1.Secret > types/secret/generated_test.go 43 | ./types/gen/gen corev1.Service > types/service/generated_test.go 44 | ./types/gen/gen corev1.Event > types/event/generated_test.go 45 | ./types/gen/gen corev1.Node > types/node/generated_test.go 46 | ./types/gen/gen corev1.ReplicationController > types/replicationcontroller/generated_test.go 47 | ./types/gen/gen appsv1.ReplicaSet > types/replicaset/generated_test.go 48 | ./types/gen/gen appsv1.Deployment > types/deployment/generated_test.go 49 | ./types/gen/gen batchv1.Job > types/job/generated_test.go 50 | ./types/gen/gen appsv1.DaemonSet > types/daemonset/generated_test.go 51 | ./types/gen/gen appsv1.StatefulSet > types/statefulset/generated_test.go 52 | $(GO) test ./types/... 53 | 54 | generate-joins: 55 | go build -o ./join/gen/gen ./join/gen 56 | ./join/gen/gen Service service '*corev1.Service' Pod pod > ./join/generated_service_pod.go 57 | ./join/gen/gen RC replicationcontroller '*corev1.ReplicationController' Pod pod > ./join/generated_rc_pod.go 58 | ./join/gen/gen RS replicaset '*appsv1.ReplicaSet' Pod pod > ./join/generated_rs_pod.go 59 | ./join/gen/gen Deployment deployment '*appsv1.Deployment' Pod pod > ./join/generated_deployment_pod.go 60 | ./join/gen/gen Job job '*batchv1.Job' Pod pod > ./join/generated_job_pod.go 61 | ./join/gen/gen DaemonSet daemonset '*appsv1.DaemonSet' Pod pod > ./join/generated_daemonset_pod.go 62 | ./join/gen/gen Ingress ingress '*networkingv1beta1.Ingress' Service service > ./join/generated_ingress_service.go 63 | ./join/gen/gen StatefulSet statefulset '*appsv1.StatefulSet' Pod pod > ./join/generated_statefulset_pod.go 64 | $(GO) build ./join 65 | 66 | example: 67 | $(GO) build -o _example/example ./_example 68 | 69 | clean: 70 | rm join/gen/gen types/gen/gen _example/example 2>/dev/null || true 71 | 72 | .PHONY: build test test-full install-libs \ 73 | generate generate-types generate-type-tests generate-joins \ 74 | example clean 75 | -------------------------------------------------------------------------------- /filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | 10 | "github.com/boz/kcache/filter" 11 | "github.com/boz/kcache/nsname" 12 | ) 13 | 14 | func TestNullFilter(t *testing.T) { 15 | f := filter.Null() 16 | 17 | assert.True(t, f.Accept(&v1.Pod{})) 18 | assert.True(t, f.Accept(&v1.Service{})) 19 | assert.True(t, f.Accept(&v1.Secret{})) 20 | 21 | assert.True(t, f.Equals(filter.Null())) 22 | assert.False(t, f.Equals(nil)) 23 | assert.False(t, f.Equals(filter.All())) 24 | } 25 | 26 | func TestAllFilter(t *testing.T) { 27 | f := filter.All() 28 | 29 | assert.False(t, f.Accept(&v1.Pod{})) 30 | assert.False(t, f.Accept(&v1.Service{})) 31 | assert.False(t, f.Accept(&v1.Secret{})) 32 | 33 | assert.True(t, f.Equals(filter.All())) 34 | assert.False(t, f.Equals(nil)) 35 | assert.False(t, f.Equals(filter.Null())) 36 | } 37 | 38 | func TestNotFilter(t *testing.T) { 39 | f1 := filter.Not(filter.All()) 40 | f2 := filter.Not(filter.Null()) 41 | 42 | assert.True(t, f1.Accept(&v1.Pod{})) 43 | assert.False(t, f2.Accept(&v1.Pod{})) 44 | 45 | assert.True(t, f1.Equals(f1)) 46 | assert.False(t, f1.Equals(f2)) 47 | } 48 | 49 | func TestNSName_fullset(t *testing.T) { 50 | n1 := nsname.New("a", "1") 51 | n2 := nsname.New("a", "2") 52 | n3 := nsname.New("b", "2") 53 | 54 | o1 := metav1.ObjectMeta{Namespace: n1.Namespace, Name: n1.Name} 55 | o2 := metav1.ObjectMeta{Namespace: n2.Namespace, Name: n2.Name} 56 | o3 := metav1.ObjectMeta{Namespace: n3.Namespace, Name: n3.Name} 57 | 58 | assert.True(t, filter.NSName(n1).Accept(&v1.Pod{ObjectMeta: o1})) 59 | assert.False(t, filter.NSName(n1).Accept(&v1.Pod{ObjectMeta: o2})) 60 | assert.False(t, filter.NSName(n1).Accept(&v1.Pod{ObjectMeta: o3})) 61 | 62 | assert.True(t, filter.NSName(n1, n2).Accept(&v1.Service{ObjectMeta: o1})) 63 | assert.True(t, filter.NSName(n1, n2).Accept(&v1.Service{ObjectMeta: o2})) 64 | assert.False(t, filter.NSName(n1, n2).Accept(&v1.Service{ObjectMeta: o3})) 65 | 66 | assert.False(t, filter.NSName().Accept(&v1.Service{ObjectMeta: o1})) 67 | 68 | assert.True(t, filter.NSName().Equals(filter.NSName())) 69 | assert.True(t, filter.NSName(n1).Equals(filter.NSName(n1))) 70 | assert.False(t, filter.NSName(n1).Equals(filter.NSName(n2))) 71 | assert.False(t, filter.NSName(n1).Equals(nil)) 72 | assert.False(t, filter.NSName().Equals(nil)) 73 | } 74 | 75 | func TestNSName_partials(t *testing.T) { 76 | n1 := nsname.New("", "1") 77 | n2 := nsname.New("b", "") 78 | 79 | o1 := metav1.ObjectMeta{Namespace: "a", Name: "1"} 80 | o2 := metav1.ObjectMeta{Namespace: "b", Name: "2"} 81 | o3 := metav1.ObjectMeta{Namespace: "c", Name: "3"} 82 | 83 | assert.True(t, filter.NSName(n1).Accept(&v1.Pod{ObjectMeta: o1})) 84 | assert.False(t, filter.NSName(n1).Accept(&v1.Pod{ObjectMeta: o2})) 85 | assert.False(t, filter.NSName(n1).Accept(&v1.Pod{ObjectMeta: o3})) 86 | 87 | assert.True(t, filter.NSName(n1, n2).Accept(&v1.Service{ObjectMeta: o1})) 88 | assert.True(t, filter.NSName(n1, n2).Accept(&v1.Service{ObjectMeta: o2})) 89 | assert.False(t, filter.NSName(n1, n2).Accept(&v1.Service{ObjectMeta: o3})) 90 | 91 | assert.True(t, filter.NSName(n1).Equals(filter.NSName(n1))) 92 | assert.False(t, filter.NSName(n1).Equals(filter.NSName(n2))) 93 | assert.True(t, filter.NSName(n1, n2).Equals(filter.NSName(n1, n2))) 94 | assert.False(t, filter.NSName(n1, n2).Equals(filter.NSName(n2, n1))) 95 | } 96 | 97 | func TestFiltersEqual(t *testing.T) { 98 | 99 | assert.True(t, filter.FiltersEqual(nil, nil)) 100 | assert.True(t, filter.FiltersEqual(filter.Null(), filter.Null())) 101 | assert.False(t, filter.FiltersEqual(filter.Null(), nil)) 102 | assert.False(t, filter.FiltersEqual(filter.Null(), filter.All())) 103 | assert.False(t, filter.FiltersEqual(filter.All(), filter.Null())) 104 | 105 | assert.True(t, filter.FiltersEqual(filter.NSName(nsname.New("a", "1")), filter.NSName(nsname.New("a", "1")))) 106 | assert.False(t, filter.FiltersEqual(filter.NSName(nsname.New("a", "1")), filter.NSName(nsname.New("a", "2")))) 107 | } 108 | 109 | func TestFN(t *testing.T) { 110 | o1 := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "a", Name: "1"}} 111 | o2 := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "b", Name: "1"}} 112 | 113 | f1 := filter.FN(func(obj metav1.Object) bool { 114 | return obj.GetNamespace() == "a" 115 | }) 116 | 117 | assert.True(t, f1.Accept(o1)) 118 | assert.False(t, f1.Accept(o2)) 119 | assert.False(t, filter.FiltersEqual(f1, f1)) 120 | assert.False(t, filter.FiltersEqual(f1, filter.All())) 121 | } 122 | -------------------------------------------------------------------------------- /controller.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | builtin_errors "errors" 6 | 7 | lifecycle "github.com/boz/go-lifecycle" 8 | logutil "github.com/boz/go-logutil" 9 | "github.com/boz/kcache/client" 10 | "github.com/boz/kcache/filter" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | var ( 15 | ErrNotRunning = builtin_errors.New("Not running") 16 | ) 17 | 18 | type Publisher interface { 19 | Subscribe() (Subscription, error) 20 | SubscribeWithFilter(filter.Filter) (FilterSubscription, error) 21 | SubscribeForFilter() (FilterSubscription, error) 22 | Clone() (Controller, error) 23 | CloneWithFilter(filter.Filter) (FilterController, error) 24 | CloneForFilter() (FilterController, error) 25 | } 26 | 27 | type CacheController interface { 28 | Cache() CacheReader 29 | Ready() <-chan struct{} 30 | } 31 | 32 | type Controller interface { 33 | CacheController 34 | Publisher 35 | Done() <-chan struct{} 36 | Close() 37 | Error() error 38 | } 39 | 40 | func NewController(ctx context.Context, log logutil.Log, client client.Client) (Controller, error) { 41 | return NewBuilder(). 42 | Context(ctx). 43 | Log(log). 44 | Client(client). 45 | Create() 46 | } 47 | 48 | type controller struct { 49 | 50 | // closed when initialization complete 51 | readych chan struct{} 52 | 53 | watcher watcher 54 | lister lister 55 | cache cache 56 | 57 | subscription subscription 58 | publisher Publisher 59 | 60 | log logutil.Log 61 | lc lifecycle.Lifecycle 62 | ctx context.Context 63 | } 64 | 65 | func (c *controller) Ready() <-chan struct{} { 66 | return c.readych 67 | } 68 | 69 | func (c *controller) Close() { 70 | c.lc.Shutdown(nil) 71 | } 72 | 73 | func (c *controller) Done() <-chan struct{} { 74 | return c.lc.Done() 75 | } 76 | 77 | func (c *controller) Error() error { 78 | return c.lc.Error() 79 | } 80 | 81 | func (c *controller) Cache() CacheReader { 82 | return c.cache 83 | } 84 | 85 | func (c *controller) Subscribe() (Subscription, error) { 86 | return c.publisher.Subscribe() 87 | } 88 | 89 | func (c *controller) SubscribeWithFilter(f filter.Filter) (FilterSubscription, error) { 90 | return c.publisher.SubscribeWithFilter(f) 91 | } 92 | 93 | func (c *controller) SubscribeForFilter() (FilterSubscription, error) { 94 | return c.publisher.SubscribeForFilter() 95 | } 96 | 97 | func (c *controller) Clone() (Controller, error) { 98 | return c.publisher.Clone() 99 | } 100 | 101 | func (c *controller) CloneWithFilter(f filter.Filter) (FilterController, error) { 102 | return c.publisher.CloneWithFilter(f) 103 | } 104 | 105 | func (c *controller) CloneForFilter() (FilterController, error) { 106 | return c.publisher.CloneForFilter() 107 | } 108 | 109 | func (c *controller) run() { 110 | defer c.lc.ShutdownCompleted() 111 | initialized := false 112 | 113 | mainloop: 114 | for { 115 | select { 116 | 117 | case err := <-c.lc.ShutdownRequest(): 118 | 119 | c.log.Debugf("shutdown request: %v", err) 120 | c.lc.ShutdownInitiated(err) 121 | break mainloop 122 | 123 | case <-c.lister.Done(): 124 | 125 | err := c.lister.Error() 126 | c.log.Debugf("lister complete: %v", err) 127 | c.lc.ShutdownInitiated(errors.Wrap(err, "lister complete")) 128 | break mainloop 129 | 130 | case <-c.watcher.Done(): 131 | 132 | err := c.watcher.Error() 133 | c.log.Debugf("watcher complete: %v", err) 134 | c.lc.ShutdownInitiated(errors.Wrap(err, "watcher complete")) 135 | break mainloop 136 | 137 | case <-c.cache.Done(): 138 | 139 | err := c.cache.Error() 140 | c.log.Debugf("cache complete: %v", err) 141 | c.lc.ShutdownInitiated(errors.Wrap(err, "cache complete")) 142 | break mainloop 143 | 144 | case result := <-c.lister.Result(): 145 | 146 | if result.err != nil { 147 | c.log.Errorf("lister error: %v", result.err) 148 | c.lc.ShutdownInitiated(errors.Wrap(result.err, "lister result")) 149 | break mainloop 150 | } 151 | 152 | version, err := listResourceVersion(result.list) 153 | if err != nil { 154 | c.log.Errorf("resource version error: %v", err) 155 | c.lc.ShutdownInitiated(errors.Wrap(err, "listing resource version")) 156 | break mainloop 157 | } 158 | 159 | c.log.Debugf("list version: %v", version) 160 | 161 | list, err := extractList(result.list) 162 | if err != nil { 163 | c.log.Errorf("extract list error: %v", err) 164 | c.lc.ShutdownInitiated(errors.Wrap(err, "extracting list")) 165 | break mainloop 166 | } 167 | 168 | events, err := c.cache.sync(list) 169 | if err != nil { 170 | c.log.Errorf("cache sync error: %v", err) 171 | c.lc.ShutdownInitiated(err) 172 | break mainloop 173 | } 174 | 175 | c.log.Debugf("list complete: version: %v, items: %v, events: %v", 176 | version, len(list), len(events)) 177 | 178 | if !initialized { 179 | c.log.Debugf("ready") 180 | initialized = true 181 | close(c.readych) 182 | } else { 183 | c.distributeEvents(events) 184 | } 185 | 186 | if err := c.watcher.reset(version); err != nil { 187 | c.log.Errorf("watcher reset error: %v", err) 188 | c.lc.ShutdownInitiated(errors.Wrap(err, "watcher reset")) 189 | break mainloop 190 | } 191 | 192 | case evt := <-c.watcher.events(): 193 | c.log.Debugf("update event: %v", evt) 194 | 195 | events, err := c.cache.update(evt) 196 | if err != nil { 197 | c.log.Errorf("update event: cache update error %v", err) 198 | c.lc.ShutdownInitiated(errors.Wrap(err, "updating cache")) 199 | break mainloop 200 | } 201 | c.distributeEvents(events) 202 | } 203 | } 204 | 205 | <-c.cache.Done() 206 | <-c.watcher.Done() 207 | <-c.lister.Done() 208 | } 209 | 210 | func (c *controller) distributeEvents(events []Event) { 211 | for _, evt := range events { 212 | c.subscription.send(evt) 213 | } 214 | c.log.Debugf("distribute events: %v events", len(events)) 215 | } 216 | -------------------------------------------------------------------------------- /subscription_filter.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | 6 | lifecycle "github.com/boz/go-lifecycle" 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type FilterSubscription interface { 13 | Subscription 14 | Refilter(filter.Filter) error 15 | } 16 | 17 | type filterSubscription struct { 18 | parent Subscription 19 | 20 | deferReady bool 21 | refilterch chan filter.Filter 22 | 23 | outch chan Event 24 | readych chan struct{} 25 | 26 | filter filter.Filter 27 | cache cache 28 | 29 | lc lifecycle.Lifecycle 30 | log logutil.Log 31 | } 32 | 33 | func newFilterSubscription(log logutil.Log, parent Subscription, f filter.Filter, deferReady bool) FilterSubscription { 34 | 35 | ctx := context.Background() 36 | lc := lifecycle.New() 37 | 38 | s := &filterSubscription{ 39 | parent: parent, 40 | refilterch: make(chan filter.Filter), 41 | outch: make(chan Event, EventBufsiz), 42 | readych: make(chan struct{}), 43 | deferReady: deferReady, 44 | filter: f, 45 | cache: newCache(ctx, log, lc.ShuttingDown(), f), 46 | lc: lc, 47 | log: log, 48 | } 49 | 50 | go s.run() 51 | 52 | return s 53 | } 54 | 55 | func (s *filterSubscription) Cache() CacheReader { 56 | return s.cache 57 | } 58 | func (s *filterSubscription) Ready() <-chan struct{} { 59 | return s.readych 60 | } 61 | func (s *filterSubscription) Events() <-chan Event { 62 | return s.outch 63 | } 64 | func (s *filterSubscription) Close() { 65 | s.parent.Close() 66 | } 67 | func (s *filterSubscription) Done() <-chan struct{} { 68 | return s.lc.Done() 69 | } 70 | func (s *filterSubscription) Error() error { 71 | if err := s.lc.Error(); err != nil { 72 | return err 73 | } 74 | return s.parent.Error() 75 | } 76 | 77 | func (s *filterSubscription) Refilter(filter filter.Filter) error { 78 | select { 79 | case s.refilterch <- filter: 80 | return nil 81 | case <-s.lc.ShuttingDown(): 82 | return errors.WithStack(ErrNotRunning) 83 | } 84 | } 85 | 86 | func (s *filterSubscription) run() { 87 | defer s.lc.ShutdownCompleted() 88 | 89 | preadych := s.parent.Ready() 90 | 91 | pending := false 92 | ready := false 93 | 94 | loop: 95 | for { 96 | select { 97 | case err := <-s.lc.ShutdownRequest(): 98 | s.log.Debugf("shutdown requested: %v", err) 99 | s.lc.ShutdownInitiated(err) 100 | break loop 101 | 102 | case <-preadych: 103 | 104 | preadych = nil 105 | 106 | if s.deferReady && !pending { 107 | s.log.Debugf("parent ready: deferring ready") 108 | continue 109 | } 110 | 111 | list, err := s.parent.Cache().List() 112 | if err != nil { 113 | s.log.Debugf("parent ready: cache list error: %v", err) 114 | s.lc.ShutdownInitiated(errors.Wrap(err, "parent ready: cache list")) 115 | break loop 116 | } 117 | 118 | if _, err := s.cache.sync(list); err != nil { 119 | s.log.Debugf("parent ready: cache sync error: %v", err) 120 | s.lc.ShutdownInitiated(errors.Wrap(err, "parent ready: cache sync")) 121 | break loop 122 | } 123 | 124 | s.log.Debugf("parent ready: making ready") 125 | close(s.readych) 126 | ready = true 127 | 128 | case f := <-s.refilterch: 129 | s.log.Debugf("refiltering...") 130 | 131 | isNew := !filter.FiltersEqual(s.filter, f) 132 | 133 | switch { 134 | 135 | case preadych != nil && !isNew: 136 | s.log.Debugf("refilter: deferring ready (filter unchanged)") 137 | pending = true 138 | continue 139 | 140 | case preadych != nil && isNew: 141 | if _, err := s.cache.refilter(nil, f); err != nil { 142 | s.log.Debugf("refilter: cache refilter (not ready): %v", err) 143 | s.lc.ShutdownInitiated(errors.Wrap(err, "refilter: cache refilter (not ready)")) 144 | break loop 145 | } 146 | s.log.Debugf("refilter: deferring ready (filter changed)") 147 | s.filter = f 148 | pending = true 149 | continue 150 | 151 | case ready && !isNew: 152 | s.log.Debugf("refilter: filter unchanged") 153 | continue 154 | 155 | case !ready && !isNew: 156 | s.log.Debugf("refilter: making ready (filter unchanged)") 157 | close(s.readych) 158 | ready = true 159 | continue 160 | 161 | } 162 | 163 | // pready == nil && isNew 164 | 165 | list, err := s.parent.Cache().List() 166 | if err != nil { 167 | s.log.Debugf("refilter: cache list error: %v", err) 168 | s.lc.ShutdownInitiated(errors.Wrap(err, "refilter: cache list")) 169 | break loop 170 | } 171 | 172 | events, err := s.cache.refilter(list, f) 173 | if err != nil { 174 | s.log.Debugf("refilter: cache refilter error: %v", err) 175 | s.lc.ShutdownInitiated(errors.Wrap(err, "refilter: cache refilter")) 176 | break loop 177 | } 178 | s.filter = f 179 | 180 | if !ready { 181 | s.log.Debugf("refilter: making ready (filter changed)") 182 | close(s.readych) 183 | ready = true 184 | continue 185 | } 186 | 187 | s.log.Debugf("refilter: %v events", len(events)) 188 | 189 | s.distributeEvents(events) 190 | 191 | case evt, ok := <-s.parent.Events(): 192 | 193 | switch { 194 | case !ok: 195 | s.log.Debugf("update: parent closed") 196 | s.lc.ShutdownInitiated(nil) 197 | break loop 198 | case !ready: 199 | continue 200 | } 201 | 202 | events, err := s.cache.update(evt) 203 | if err != nil { 204 | s.log.Debugf("update: cache update error %v", err) 205 | s.lc.ShutdownInitiated(nil) 206 | break loop 207 | } 208 | 209 | s.log.Debugf("update: %v events", len(events)) 210 | 211 | s.distributeEvents(events) 212 | 213 | } 214 | } 215 | 216 | s.parent.Close() 217 | 218 | close(s.outch) 219 | 220 | <-s.parent.Done() 221 | } 222 | 223 | func (s *filterSubscription) distributeEvents(events []Event) { 224 | for _, evt := range events { 225 | select { 226 | case s.outch <- evt: 227 | default: 228 | s.log.Warnf("event buffer overrun") 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /publisher.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | lifecycle "github.com/boz/go-lifecycle" 5 | logutil "github.com/boz/go-logutil" 6 | "github.com/boz/kcache/filter" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type FilterController interface { 11 | Controller 12 | Refilter(filter.Filter) error 13 | } 14 | 15 | type publisher struct { 16 | parent Subscription 17 | 18 | subscribech chan chan<- Subscription 19 | unsubscribech chan subscription 20 | subscriptions map[subscription]struct{} 21 | 22 | lc lifecycle.Lifecycle 23 | log logutil.Log 24 | } 25 | 26 | func newPublisher(log logutil.Log, parent Subscription) Controller { 27 | s := &publisher{ 28 | parent: parent, 29 | subscribech: make(chan chan<- Subscription), 30 | unsubscribech: make(chan subscription), 31 | subscriptions: make(map[subscription]struct{}), 32 | lc: lifecycle.New(), 33 | log: log.WithComponent("publisher"), 34 | } 35 | 36 | go s.run() 37 | 38 | return s 39 | } 40 | 41 | func (s *publisher) Ready() <-chan struct{} { 42 | return s.parent.Ready() 43 | } 44 | 45 | func (s *publisher) Cache() CacheReader { 46 | return s.parent.Cache() 47 | } 48 | 49 | func (s *publisher) Close() { 50 | s.parent.Close() 51 | } 52 | 53 | func (s *publisher) Done() <-chan struct{} { 54 | return s.lc.Done() 55 | } 56 | 57 | func (s *publisher) Error() error { 58 | return s.lc.Error() 59 | } 60 | 61 | func (s *publisher) Subscribe() (Subscription, error) { 62 | resultch := make(chan Subscription, 1) 63 | select { 64 | case <-s.lc.ShuttingDown(): 65 | return nil, errors.WithStack(ErrNotRunning) 66 | case s.subscribech <- resultch: 67 | return <-resultch, nil 68 | } 69 | } 70 | 71 | func (s *publisher) SubscribeWithFilter(f filter.Filter) (FilterSubscription, error) { 72 | sub, err := s.Subscribe() 73 | if err != nil { 74 | return nil, err 75 | } 76 | return newFilterSubscription(s.log, sub, f, false), nil 77 | } 78 | 79 | func (s *publisher) SubscribeForFilter() (FilterSubscription, error) { 80 | sub, err := s.Subscribe() 81 | if err != nil { 82 | return nil, err 83 | } 84 | return newFilterSubscription(s.log, sub, filter.All(), true), nil 85 | } 86 | 87 | func (s *publisher) Clone() (Controller, error) { 88 | sub, err := s.Subscribe() 89 | if err != nil { 90 | return nil, err 91 | } 92 | return newPublisher(s.log, sub), nil 93 | } 94 | 95 | func (s *publisher) CloneWithFilter(f filter.Filter) (FilterController, error) { 96 | sub, err := s.SubscribeWithFilter(f) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return newFilterPublisher(s.log, sub), nil 101 | } 102 | 103 | func (s *publisher) CloneForFilter() (FilterController, error) { 104 | sub, err := s.SubscribeForFilter() 105 | if err != nil { 106 | return nil, err 107 | } 108 | return newFilterPublisher(s.log, sub), nil 109 | } 110 | 111 | func (s *publisher) run() { 112 | defer s.lc.ShutdownCompleted() 113 | 114 | loop: 115 | for { 116 | select { 117 | case evt, ok := <-s.parent.Events(): 118 | if !ok { 119 | s.log.Debugf("parent events closed") 120 | s.lc.ShutdownInitiated(nil) 121 | break loop 122 | } 123 | s.distributeEvent(evt) 124 | case resultch := <-s.subscribech: 125 | resultch <- s.createSubscription() 126 | case sub := <-s.unsubscribech: 127 | delete(s.subscriptions, sub) 128 | } 129 | } 130 | 131 | for len(s.subscriptions) > 0 { 132 | s.log.Debugf("draining: %v subscriptions", len(s.subscriptions)) 133 | select { 134 | case sub := <-s.unsubscribech: 135 | delete(s.subscriptions, sub) 136 | } 137 | } 138 | 139 | <-s.parent.Done() 140 | } 141 | 142 | func (s *publisher) distributeEvent(evt Event) { 143 | s.log.Debugf("distribute event: sending %v to %v subscriptions", evt, len(s.subscriptions)) 144 | 145 | for sub := range s.subscriptions { 146 | sub.send(evt) 147 | } 148 | } 149 | 150 | func (s *publisher) createSubscription() Subscription { 151 | s.log.Debugf("create subscription: current count %v", len(s.subscriptions)) 152 | 153 | sub := newSubscription(s.log, s.lc.ShuttingDown(), s.parent.Ready(), s.parent.Cache()) 154 | 155 | s.subscriptions[sub] = struct{}{} 156 | 157 | go func() { 158 | select { 159 | case <-sub.Done(): 160 | s.log.Debugf("create subscription: subscription done") 161 | case <-s.lc.ShuttingDown(): 162 | s.log.Debugf("create subscription: shut down, killing subscription") 163 | sub.Close() 164 | <-sub.Done() 165 | } 166 | s.unsubscribech <- sub 167 | s.log.Debugf("create subscription: unsubscribed") 168 | }() 169 | 170 | return sub 171 | } 172 | 173 | func newFilterPublisher(log logutil.Log, subscription FilterSubscription) FilterController { 174 | return &filterController{subscription, newPublisher(log, subscription)} 175 | } 176 | 177 | type filterController struct { 178 | subscription FilterSubscription 179 | parent Controller 180 | } 181 | 182 | func (c *filterController) Cache() CacheReader { 183 | return c.parent.Cache() 184 | } 185 | 186 | func (c *filterController) Ready() <-chan struct{} { 187 | return c.parent.Ready() 188 | } 189 | 190 | func (c *filterController) Subscribe() (Subscription, error) { 191 | return c.parent.Subscribe() 192 | } 193 | 194 | func (c *filterController) SubscribeWithFilter(f filter.Filter) (FilterSubscription, error) { 195 | return c.parent.SubscribeWithFilter(f) 196 | } 197 | 198 | func (c *filterController) SubscribeForFilter() (FilterSubscription, error) { 199 | return c.parent.SubscribeForFilter() 200 | } 201 | 202 | func (c *filterController) Clone() (Controller, error) { 203 | return c.parent.Clone() 204 | } 205 | 206 | func (c *filterController) CloneWithFilter(f filter.Filter) (FilterController, error) { 207 | return c.parent.CloneWithFilter(f) 208 | } 209 | 210 | func (c *filterController) CloneForFilter() (FilterController, error) { 211 | return c.parent.CloneForFilter() 212 | } 213 | 214 | func (c *filterController) Done() <-chan struct{} { 215 | return c.parent.Done() 216 | } 217 | 218 | func (c *filterController) Close() { 219 | c.parent.Close() 220 | } 221 | 222 | func (c *filterController) Error() error { 223 | return c.parent.Error() 224 | } 225 | 226 | func (c *filterController) Refilter(filter filter.Filter) error { 227 | return c.subscription.Refilter(filter) 228 | } 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kcache: kubernetes object cache [![Build Status](https://travis-ci.org/boz/kcache.svg?branch=master)](https://travis-ci.org/boz/kcache) [![codecov](https://codecov.io/gh/boz/kcache/branch/master/graph/badge.svg)](https://codecov.io/gh/boz/kcache) 2 | 3 | Kcache is a [kubernetes](https://github.com/kubernetes/kubernetes) object data source similar to [k8s.io/client-go/tools/cache](https://github.com/kubernetes/client-go/tree/master/tools/cache) which uses channels to create a flexible event-based toolkit. Features include [typed producers](#types), [joining between multiple producers](#joins), and [(re)filtering](#filtering). 4 | 5 | * [Usage](#usage) 6 | * [Controllers](#controllers) 7 | * [Channels](#channels) 8 | * [Callbacks](#callbacks) 9 | * [Types](#types) 10 | * [Joins](#joins) 11 | * [Filtering](#filters) 12 | 13 | Kcache was originally created to drive a Kubernetes monitoring application and it currently powers [kail](https://github.com/boz/kail). 14 | 15 | ## Usage 16 | 17 | Using kcache involves creating [controllers](#controllers) to manage dynamic object sets with the kubernetes API. The monitored objects are cached and events about changing state are broadcast to subscribers. 18 | 19 | ### Controllers 20 | 21 | Each controller represents a single kubernetes watch stream. There can be any number of subscribers to 22 | each controller, and subscribers can be publishers themselves. 23 | 24 | ```go 25 | controller, err := kcache.NewController(ctx,log,client) 26 | 27 | // wait for the initial sync to be complete 28 | <-controller.Ready() 29 | 30 | fmt.Println("controller has been synced") 31 | ``` 32 | 33 | Controllers maintain a cache of the objects being watched. 34 | 35 | ```go 36 | // fetch the pod named 'pod-1' in the namespace 'default' from the cache. 37 | pod, err := controller.Cache().Get("default","pod-1") 38 | ``` 39 | 40 | ### Channels 41 | 42 | There are many ways to subscribe to a controller's events, the most basic is a simple channel-based subscription: 43 | 44 | ```go 45 | sub, err := controller.Subscribe() 46 | <-sub.Ready() 47 | 48 | // fetch cached list of objects 49 | sub.Cache().List() 50 | 51 | for event := range sub.Events() { 52 | // handle add/update/delete event for objects 53 | } 54 | ``` 55 | 56 | ### Callbacks 57 | 58 | In addition to [channels](#channels), callbacks can be used to handle events 59 | 60 | ```go 61 | handler := kcache.BuildHandler(). 62 | OnInitialize(func(objs []metav1.Object) { /* ... */ }). 63 | OnCreate(func(obj metav1.Object){ /* ... */ }). 64 | OnUpdate(func(obj metav1.Object){ /* ... */ }). 65 | OnDelete(func(obj metav1.Object){ /* ... */ }). 66 | Create() 67 | controller 68 | 69 | kcache.NewMonitor(controller,handler) 70 | ``` 71 | 72 | ### Types 73 | 74 | Typed controllers and subscribers are available to reduce the need for casting objects. Each type has all of the features of the untyped system (channels,callbacks, filtering, caches, etc...) 75 | 76 | ```go 77 | controller, err := pod.NewController(ctx,log,client,"default") 78 | sub, err := controller.Subescribe() 79 | ... 80 | ``` 81 | 82 | Currently implemented types are: 83 | 84 | * Pod 85 | * Node 86 | * Event 87 | * Secret 88 | * Service 89 | * Ingress 90 | * Daemonset 91 | * ReplicaSet 92 | * Deployment 93 | * ReplicationController 94 | 95 | ### Filtering 96 | 97 | The cache and events that are be exposed to a subscription can be limited by a filter object 98 | 99 | The following will return a subscription that only sees the pod named "default/pod-1" pod in its cache and events: 100 | 101 | ```go 102 | sub, err := controller.SubscribeWithFilter(filter.NSName("default","pod-1")) 103 | ``` 104 | 105 | Additionally, new publishers can be created with filters. In the following example, 106 | `sub_a` will only receive events about "default/pod-1" and `sub_b` will only receive events about "default/pod-2" 107 | 108 | ```go 109 | pub_a, err := controller.CloneWithFilter(filter.NSName("default","pod-1")) 110 | pub_b, err := controller.CloneWithFilter(filter.NSName("default","pod-2")) 111 | 112 | sub_a, err := pub_a.Subscribe() 113 | sub_b, err := pub_b.Subscribe() 114 | ``` 115 | 116 | ### Refiltering 117 | 118 | The filter used for filtered publishers and subscribers can be changed at any time. The cache for each will readjust and `CREATE`, `DELETE` events will be emitted as necessary. 119 | 120 | In the example below, if the pods "default/pod-1" and "default/pod-2" exist, `sub_a` will receive a delete event for "default/pod-1" and a create event for "default/pod-2" 121 | 122 | ```go 123 | pub_a, err := controller.CloneWithFilter(filter.NSName("default","pod-1")) 124 | 125 | sub_a, err := pub_a.Subscribe() 126 | 127 | <-sub_a.Ready() 128 | 129 | go func() { 130 | for evt := sub_a.Events() { 131 | fmt.Println(evt) 132 | } 133 | }() 134 | 135 | pub_a.Refilter(filter.NSName("default","pod-2")) 136 | ``` 137 | 138 | ### Joins 139 | 140 | [Refiltering](#refiltering) allows for joining between different publishers. The join is dynamic -- as the objects of the joined 141 | publisher changes, so does the set of objects in the resulting publisher. 142 | 143 | In the example below, `sub` will only know about pods that are targeted by the "default/frontend" service. 144 | 145 | ```go 146 | pods, err := pod.NewController(/*...*/) 147 | services, err := service.NewController(/*...*/) 148 | 149 | frontend, err := services.CloneWithFilter(filter.NSName("default","frontend")) 150 | 151 | sub, err := join.ServicePods(ctx,frontend,pods) 152 | 153 | <- sub.Ready() 154 | 155 | for evt := range sub.Events() { 156 | /* ... */ 157 | } 158 | ``` 159 | 160 | Joining can be done by hand but there are a number of utility joins available: 161 | 162 | * `ServicePods()` - restrict pods to those that match the services available in the given publisher. 163 | * `RCPods()` - restrict pods to those that match the replication controllers in the given publisher. 164 | * `RSPods()` - restrict pods to those that match the replica sets in the given publisher. 165 | * `DeploymentPods()` - restrict pods to those that match the deployments in the given publisher. 166 | * `DaemonSetPods()` - restrict pods to those that match the daemonsets in the given publisher. 167 | * `IngressServices()` - restrict services to those that match the ingresses in the given publisher. 168 | * `IngressPods()` - restrict pods to those that match the services which match the ingresses in the given publisher (_double join_) 169 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | lifecycle "github.com/boz/go-lifecycle" 8 | logutil "github.com/boz/go-logutil" 9 | "github.com/boz/kcache/filter" 10 | "github.com/pkg/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | type CacheReader interface { 15 | GetObject(obj metav1.Object) (metav1.Object, error) 16 | Get(ns string, name string) (metav1.Object, error) 17 | List() ([]metav1.Object, error) 18 | } 19 | 20 | type cache interface { 21 | CacheReader 22 | sync([]metav1.Object) ([]Event, error) 23 | update(Event) ([]Event, error) 24 | refilter([]metav1.Object, filter.Filter) ([]Event, error) 25 | Done() <-chan struct{} 26 | Error() error 27 | } 28 | 29 | type cacheKey struct { 30 | namespace string 31 | name string 32 | } 33 | 34 | type cacheEntry struct { 35 | version int 36 | object metav1.Object 37 | } 38 | 39 | type syncRequest struct { 40 | list []metav1.Object 41 | resultch chan<- []Event 42 | } 43 | 44 | type getRequest struct { 45 | key cacheKey 46 | resultch chan<- metav1.Object 47 | } 48 | 49 | type updateRequest struct { 50 | evt Event 51 | resultch chan<- []Event 52 | } 53 | 54 | type refilterRequest struct { 55 | list []metav1.Object 56 | filter filter.Filter 57 | resultch chan<- []Event 58 | } 59 | 60 | type _cache struct { 61 | filter filter.Filter 62 | syncch chan syncRequest 63 | updatech chan updateRequest 64 | refilterch chan refilterRequest 65 | 66 | getch chan getRequest 67 | listch chan chan []metav1.Object 68 | 69 | items map[cacheKey]cacheEntry 70 | 71 | log logutil.Log 72 | lc lifecycle.Lifecycle 73 | ctx context.Context 74 | } 75 | 76 | func newCache(ctx context.Context, log logutil.Log, stopch <-chan struct{}, filter filter.Filter) cache { 77 | log = log.WithComponent("cache") 78 | 79 | c := &_cache{ 80 | filter: filter, 81 | syncch: make(chan syncRequest), 82 | updatech: make(chan updateRequest), 83 | getch: make(chan getRequest), 84 | refilterch: make(chan refilterRequest), 85 | listch: make(chan chan []metav1.Object), 86 | items: make(map[cacheKey]cacheEntry), 87 | log: log, 88 | lc: lifecycle.New(), 89 | ctx: ctx, 90 | } 91 | 92 | go c.lc.WatchContext(ctx) 93 | go c.lc.WatchChannel(stopch) 94 | go c.run() 95 | 96 | return c 97 | } 98 | 99 | func (c *_cache) sync(list []metav1.Object) ([]Event, error) { 100 | resultch := make(chan []Event, 1) 101 | request := syncRequest{list, resultch} 102 | 103 | select { 104 | case <-c.lc.ShuttingDown(): 105 | return nil, errors.WithStack(ErrNotRunning) 106 | case c.syncch <- request: 107 | } 108 | 109 | return <-resultch, nil 110 | } 111 | 112 | func (c *_cache) update(evt Event) ([]Event, error) { 113 | 114 | resultch := make(chan []Event, 1) 115 | request := updateRequest{evt, resultch} 116 | 117 | select { 118 | case <-c.lc.ShuttingDown(): 119 | return nil, errors.WithStack(ErrNotRunning) 120 | case c.updatech <- request: 121 | } 122 | 123 | return <-resultch, nil 124 | 125 | } 126 | 127 | func (c *_cache) refilter(list []metav1.Object, filter filter.Filter) ([]Event, error) { 128 | resultch := make(chan []Event, 1) 129 | request := refilterRequest{list, filter, resultch} 130 | 131 | select { 132 | case <-c.lc.ShuttingDown(): 133 | return nil, errors.WithStack(ErrNotRunning) 134 | case c.refilterch <- request: 135 | } 136 | 137 | return <-resultch, nil 138 | } 139 | 140 | func (c *_cache) Done() <-chan struct{} { 141 | return c.lc.Done() 142 | } 143 | 144 | func (c *_cache) Error() error { 145 | return c.lc.Error() 146 | } 147 | 148 | func (c *_cache) List() ([]metav1.Object, error) { 149 | resultch := make(chan []metav1.Object, 1) 150 | 151 | select { 152 | case <-c.lc.ShuttingDown(): 153 | return nil, errors.WithStack(ErrNotRunning) 154 | case c.listch <- resultch: 155 | } 156 | 157 | return <-resultch, nil 158 | } 159 | 160 | func (c *_cache) GetObject(obj metav1.Object) (metav1.Object, error) { 161 | return c.Get(obj.GetNamespace(), obj.GetName()) 162 | } 163 | 164 | func (c *_cache) Get(ns, name string) (metav1.Object, error) { 165 | resultch := make(chan metav1.Object, 1) 166 | key := cacheKey{ns, name} 167 | request := getRequest{key, resultch} 168 | select { 169 | case <-c.lc.ShuttingDown(): 170 | return nil, errors.WithStack(ErrNotRunning) 171 | case c.getch <- request: 172 | } 173 | return <-resultch, nil 174 | } 175 | 176 | func (c *_cache) run() { 177 | defer c.lc.ShutdownCompleted() 178 | for { 179 | select { 180 | case request := <-c.syncch: 181 | request.resultch <- c.doSync(request.list) 182 | case request := <-c.updatech: 183 | request.resultch <- c.doUpdate(request.evt) 184 | case request := <-c.refilterch: 185 | request.resultch <- c.doRefilter(request.list, request.filter) 186 | case request := <-c.listch: 187 | request <- c.doList() 188 | case request := <-c.getch: 189 | if entry, ok := c.items[request.key]; ok { 190 | request.resultch <- entry.object 191 | } else { 192 | request.resultch <- nil 193 | } 194 | case err := <-c.lc.ShutdownRequest(): 195 | c.lc.ShutdownInitiated(err) 196 | return 197 | } 198 | } 199 | } 200 | 201 | func (c *_cache) doList() []metav1.Object { 202 | result := make([]metav1.Object, 0, len(c.items)) 203 | for _, obj := range c.items { 204 | result = append(result, obj.object) 205 | } 206 | return result 207 | } 208 | 209 | func (c *_cache) doSync(list []metav1.Object) []Event { 210 | 211 | var events []Event 212 | set := make(map[cacheKey]cacheEntry) 213 | 214 | for _, obj := range list { 215 | 216 | key, err := c.createKey(obj) 217 | if err != nil { 218 | c.log.ErrWarn(err, "createKey(%T)", obj) 219 | continue 220 | } 221 | 222 | entry, err := c.createEntry(obj) 223 | if err != nil { 224 | c.log.ErrWarn(err, "createEntry(%T)", obj) 225 | continue 226 | } 227 | 228 | current, found := c.items[key] 229 | 230 | accept := c.filter.Accept(entry.object) 231 | 232 | switch { 233 | case accept && !found: 234 | events = append(events, NewEvent(EventTypeCreate, entry.object)) 235 | c.items[key] = entry 236 | case accept && current.version < entry.version: 237 | events = append(events, NewEvent(EventTypeUpdate, entry.object)) 238 | c.items[key] = entry 239 | case current.version >= entry.version: 240 | if !c.filter.Accept(current.object) { 241 | continue 242 | } 243 | default: 244 | // don't add to working new working set of objects 245 | continue 246 | } 247 | 248 | set[key] = entry 249 | } 250 | 251 | for k, current := range c.items { 252 | if _, ok := set[k]; !ok { 253 | events = append(events, NewEvent(EventTypeDelete, current.object)) 254 | delete(c.items, k) 255 | } 256 | } 257 | 258 | return events 259 | } 260 | 261 | func (c *_cache) doRefilter(list []metav1.Object, filter filter.Filter) []Event { 262 | c.filter = filter 263 | return c.doSync(list) 264 | } 265 | 266 | func (c *_cache) doUpdate(evt Event) []Event { 267 | events := make([]Event, 0, 1) 268 | 269 | obj := evt.Resource() 270 | 271 | version, err := strconv.Atoi(obj.GetResourceVersion()) 272 | if err != nil { 273 | c.log.ErrWarn(err, "resource version %v", obj.GetResourceVersion()) 274 | return events 275 | } 276 | 277 | key := cacheKey{obj.GetNamespace(), obj.GetName()} 278 | entry := cacheEntry{version, obj} 279 | 280 | current, found := c.items[key] 281 | 282 | accept := c.filter.Accept(entry.object) 283 | 284 | switch evt.Type() { 285 | case EventTypeDelete: 286 | if found { 287 | events = append(events, evt) 288 | delete(c.items, key) 289 | } 290 | default: 291 | switch { 292 | case !accept && !found: 293 | // do nothing 294 | case accept && !found: 295 | // create 296 | events = append(events, NewEvent(EventTypeCreate, obj)) 297 | c.items[key] = entry 298 | case accept && current.version < entry.version: 299 | // update 300 | events = append(events, NewEvent(EventTypeUpdate, obj)) 301 | c.items[key] = entry 302 | case !accept && current.version < entry.version: 303 | // filter-delete 304 | events = append(events, NewEvent(EventTypeDelete, obj)) 305 | delete(c.items, key) 306 | } 307 | } 308 | 309 | return events 310 | } 311 | 312 | func (c *_cache) createKey(obj metav1.Object) (cacheKey, error) { 313 | ns := obj.GetNamespace() 314 | name := obj.GetName() 315 | 316 | return cacheKey{ns, name}, nil 317 | } 318 | 319 | func (c *_cache) createEntry(obj metav1.Object) (cacheEntry, error) { 320 | version, err := strconv.Atoi(obj.GetResourceVersion()) 321 | if err != nil { 322 | return cacheEntry{}, err 323 | } 324 | return cacheEntry{version, obj}, nil 325 | } 326 | -------------------------------------------------------------------------------- /controller_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/boz/kcache/client/mocks" 9 | "github.com/boz/kcache/filter" 10 | "github.com/boz/kcache/nsname" 11 | "github.com/boz/kcache/testutil" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/stretchr/testify/require" 15 | 16 | "k8s.io/api/core/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/apimachinery/pkg/watch" 19 | ) 20 | 21 | func TestController(t *testing.T) { 22 | ctx, cancel := context.WithCancel(context.Background()) 23 | defer cancel() 24 | 25 | eventch := make(chan watch.Event, 10) 26 | listch := make(chan time.Time, 1) 27 | 28 | mwatch := &mocks.WatchInterface{} 29 | mwatch.On("ResultChan").Return(eventch) 30 | mwatch.On("Stop").Return() 31 | 32 | obj_a := testGenPod("ns", "a", "1") 33 | obj_b := testGenPod("ns", "b", "2") 34 | obj_c := testGenPod("ns", "c", "3") 35 | obj_a_2 := testGenPod("ns", "a", "4") 36 | 37 | fltr := filter.NSName(nsname.New(obj_a.GetNamespace(), obj_a.GetName())) 38 | 39 | list := &v1.PodList{ 40 | TypeMeta: metav1.TypeMeta{ 41 | Kind: "PodList", 42 | APIVersion: "1", 43 | }, 44 | ListMeta: metav1.ListMeta{ 45 | ResourceVersion: "1", 46 | }, 47 | Items: []v1.Pod{ 48 | *obj_a, 49 | *obj_b, 50 | }, 51 | } 52 | 53 | client := &mocks.Client{} 54 | 55 | client.On("Watch", mock.Anything, mock.AnythingOfType("v1.ListOptions")).Return(mwatch, nil) 56 | client.On("List", mock.Anything, mock.AnythingOfType("v1.ListOptions")). 57 | WaitUntil(listch). 58 | Return(list, nil) 59 | 60 | controller, err := NewBuilder(). 61 | Context(ctx). 62 | Client(client). 63 | Create() 64 | 65 | require.NoError(t, err) 66 | 67 | sub, err := controller.Subscribe() 68 | require.NoError(t, err) 69 | sub_wf, err := controller.SubscribeWithFilter(fltr) 70 | require.NoError(t, err) 71 | sub_ff, err := controller.SubscribeForFilter() 72 | require.NoError(t, err) 73 | 74 | clone, err := controller.Clone() 75 | require.NoError(t, err) 76 | clone_wf, err := controller.CloneWithFilter(fltr) 77 | require.NoError(t, err) 78 | clone_ff, err := controller.CloneForFilter() 79 | require.NoError(t, err) 80 | 81 | csub, err := clone.Subscribe() 82 | require.NoError(t, err) 83 | csub_wf, err := clone_wf.Subscribe() 84 | require.NoError(t, err) 85 | csub_ff, err := clone_ff.Subscribe() 86 | require.NoError(t, err) 87 | 88 | testutil.AssertNotReady(t, "controller", controller) 89 | 90 | testutil.AssertNotReady(t, "sub", sub) 91 | testutil.AssertNotReady(t, "sub_wf", sub_wf) 92 | testutil.AssertNotReady(t, "sub_ff", sub_ff) 93 | 94 | testutil.AssertNotReady(t, "clone", clone) 95 | testutil.AssertNotReady(t, "clone_wf", clone_wf) 96 | testutil.AssertNotReady(t, "clone_ff", clone_ff) 97 | 98 | testutil.AssertNotReady(t, "csub", csub) 99 | testutil.AssertNotReady(t, "csub_wf", csub_wf) 100 | testutil.AssertNotReady(t, "csub_ff", csub_ff) 101 | 102 | listch <- time.Now() 103 | 104 | testutil.AssertReady(t, "controller", controller) 105 | 106 | testutil.AssertReady(t, "sub", sub) 107 | testutil.AssertReady(t, "sub_wf", sub_wf) 108 | testutil.AssertNotReady(t, "sub_ff", sub_ff) 109 | 110 | testutil.AssertReady(t, "clone", clone) 111 | testutil.AssertReady(t, "clone_wf", clone_wf) 112 | testutil.AssertNotReady(t, "clone_ff", clone_ff) 113 | 114 | testutil.AssertReady(t, "csub", csub) 115 | testutil.AssertReady(t, "csub_wf", csub_wf) 116 | testutil.AssertNotReady(t, "csub_ff", csub_ff) 117 | 118 | fullcache := func(name string, c CacheController) { 119 | 120 | slist, err := c.Cache().List() 121 | assert.NoError(t, err, name) 122 | assert.Len(t, slist, 2, name) 123 | 124 | obj, err := c.Cache().Get(obj_a.GetNamespace(), obj_a.GetName()) 125 | if assert.NoError(t, err, name) && assert.NotNil(t, obj, name) { 126 | assert.Equal(t, obj_a.GetNamespace(), obj.GetNamespace(), name) 127 | assert.Equal(t, obj_a.GetName(), obj.GetName(), name) 128 | } 129 | 130 | obj, err = c.Cache().Get(obj_b.GetNamespace(), obj_b.GetName()) 131 | if assert.NoError(t, err, name) && assert.NotNil(t, obj, name) { 132 | assert.Equal(t, obj_b.GetNamespace(), obj.GetNamespace(), name) 133 | assert.Equal(t, obj_b.GetName(), obj.GetName(), name) 134 | } 135 | 136 | } 137 | 138 | halfcache := func(name string, c CacheController) { 139 | 140 | slist, err := c.Cache().List() 141 | assert.NoError(t, err, name) 142 | 143 | if assert.Len(t, slist, 1) { 144 | assert.Equal(t, obj_a.GetNamespace(), slist[0].GetNamespace(), name) 145 | assert.Equal(t, obj_a.GetName(), slist[0].GetName(), name) 146 | } 147 | 148 | obj, err := c.Cache().Get(obj_a.GetNamespace(), obj_a.GetName()) 149 | if assert.NoError(t, err, name) && assert.NotNil(t, obj, name) { 150 | assert.Equal(t, obj_a.GetNamespace(), obj.GetNamespace(), name) 151 | assert.Equal(t, obj_a.GetName(), obj.GetName(), name) 152 | } 153 | 154 | obj, err = c.Cache().Get(obj_b.GetNamespace(), obj_b.GetName()) 155 | assert.NoError(t, err, name) 156 | assert.Nil(t, obj, name) 157 | 158 | } 159 | 160 | fullcache("controller", controller) 161 | fullcache("sub", sub) 162 | fullcache("clone", clone) 163 | 164 | halfcache("sub_wf", sub_wf) 165 | halfcache("clone_wf", clone_wf) 166 | 167 | fullcache("csub", csub) 168 | halfcache("csub_wf", csub_wf) 169 | 170 | sub_ff.Refilter(fltr) 171 | clone_ff.Refilter(fltr) 172 | 173 | testutil.AssertReady(t, "sub_ff", sub_ff) 174 | testutil.AssertReady(t, "clone_ff", clone_ff) 175 | testutil.AssertReady(t, "csub_ff", csub_ff) 176 | 177 | halfcache("sub_ff", sub_ff) 178 | halfcache("clone_ff", clone_ff) 179 | halfcache("csub_ff", sub_ff) 180 | 181 | eventch <- watch.Event{ 182 | Type: watch.Added, 183 | Object: obj_c, 184 | } 185 | eventch <- watch.Event{ 186 | Type: watch.Modified, 187 | Object: obj_a_2, 188 | } 189 | 190 | fullevt := func(name string, sub Subscription) { 191 | ctx, cancel := context.WithCancel(context.TODO()) 192 | defer cancel() 193 | 194 | atimes := 0 195 | ctimes := 0 196 | 197 | select { 198 | case ev, ok := <-sub.Events(): 199 | if !assert.True(t, ok, name) { 200 | return 201 | } 202 | switch ev.Resource().GetName() { 203 | case obj_a.GetName(): 204 | atimes++ 205 | assert.Equal(t, EventTypeUpdate, ev.Type(), name) 206 | case obj_c.GetName(): 207 | ctimes++ 208 | assert.Equal(t, EventTypeCreate, ev.Type(), name) 209 | default: 210 | assert.Fail(t, "unknown event %v: %#v", ev) 211 | } 212 | case <-testutil.AsyncWaitch(ctx): 213 | assert.Fail(t, "no first event", name) 214 | return 215 | } 216 | 217 | select { 218 | case ev, ok := <-sub.Events(): 219 | if !assert.True(t, ok, name) { 220 | return 221 | } 222 | switch ev.Resource().GetName() { 223 | case obj_a.GetName(): 224 | atimes++ 225 | assert.Equal(t, EventTypeUpdate, ev.Type(), name) 226 | case obj_c.GetName(): 227 | ctimes++ 228 | assert.Equal(t, EventTypeCreate, ev.Type(), name) 229 | default: 230 | assert.Fail(t, "unknown event %v: %#v", ev) 231 | } 232 | case <-testutil.AsyncWaitch(ctx): 233 | assert.Fail(t, "no second event", name) 234 | return 235 | } 236 | 237 | assert.Equal(t, 1, atimes, name) 238 | assert.Equal(t, 1, ctimes, name) 239 | } 240 | 241 | halfevt := func(name string, sub Subscription) { 242 | ctx, cancel := context.WithCancel(context.TODO()) 243 | defer cancel() 244 | 245 | select { 246 | case evt, ok := <-sub.Events(): 247 | 248 | if !assert.True(t, ok, name) { 249 | return 250 | } 251 | 252 | assert.Equal(t, EventTypeUpdate, evt.Type(), name) 253 | assert.Equal(t, obj_a.GetNamespace(), evt.Resource().GetNamespace()) 254 | assert.Equal(t, obj_a.GetName(), evt.Resource().GetName()) 255 | 256 | case <-testutil.AsyncWaitch(ctx): 257 | assert.Fail(t, "no events", name) 258 | return 259 | } 260 | 261 | select { 262 | case <-sub.Events(): 263 | assert.Fail(t, "too many events", name) 264 | case <-testutil.AsyncWaitch(ctx): 265 | } 266 | 267 | } 268 | 269 | fullevt("sub", sub) 270 | halfevt("sub_wf", sub_wf) 271 | halfevt("sub_ff", sub_ff) 272 | 273 | fullevt("csub", csub) 274 | halfevt("csub_wf", csub_wf) 275 | halfevt("csub_ff", csub_ff) 276 | 277 | controller.Close() 278 | 279 | testutil.AssertDone(t, "controller", controller) 280 | testutil.AssertDone(t, "sub", sub) 281 | testutil.AssertDone(t, "sub_wf", sub_wf) 282 | testutil.AssertDone(t, "sub_ff", sub_ff) 283 | testutil.AssertDone(t, "clone", clone) 284 | testutil.AssertDone(t, "clone_wf", clone_wf) 285 | testutil.AssertDone(t, "clone_ff", clone_ff) 286 | testutil.AssertDone(t, "csub", csub) 287 | testutil.AssertDone(t, "csub_wf", csub_wf) 288 | testutil.AssertDone(t, "csub_ff", csub_ff) 289 | 290 | } 291 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/testutil" 10 | "github.com/pkg/errors" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | ) 16 | 17 | func TestCache_Sync(t *testing.T) { 18 | initial := []metav1.Object{ 19 | testGenPod("default", "pod-1", "1"), 20 | testGenPod("default", "pod-2", "2"), 21 | } 22 | 23 | secondary := []metav1.Object{ 24 | testGenPod("default", "pod-1", "3"), 25 | testGenPod("default", "pod-3", "4"), 26 | } 27 | 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | stopch := make(chan struct{}) 32 | defer close(stopch) 33 | 34 | log := logutil.Default() 35 | filter := filter.Null() 36 | 37 | cache := newCache(ctx, log, stopch, filter) 38 | 39 | evs, err := cache.sync(initial) 40 | assert.NoError(t, err) 41 | assert.Len(t, evs, len(initial)) 42 | 43 | events, err := cache.sync(secondary) 44 | assert.NoError(t, err) 45 | require.Len(t, events, 3) 46 | 47 | found := make(map[string]bool) 48 | 49 | for _, evt := range events { 50 | name := evt.Resource().GetName() 51 | switch name { 52 | case "pod-1": 53 | if assert.Equal(t, EventTypeUpdate, evt.Type()) { 54 | found[name] = true 55 | } 56 | case "pod-2": 57 | if assert.Equal(t, EventTypeDelete, evt.Type()) { 58 | found[name] = true 59 | } 60 | case "pod-3": 61 | if assert.Equal(t, EventTypeCreate, evt.Type()) { 62 | found[name] = true 63 | } 64 | default: 65 | t.Errorf("unknown pod name: %v", name) 66 | } 67 | } 68 | require.Equal(t, 3, len(found)) 69 | 70 | list, err := cache.List() 71 | require.NoError(t, err) 72 | require.Len(t, list, 2) 73 | 74 | found = make(map[string]bool) 75 | for _, obj := range list { 76 | name := obj.GetName() 77 | switch name { 78 | case "pod-1": 79 | found[name] = true 80 | case "pod-2": 81 | assert.Failf(t, "found unexpected pod in list", name) 82 | case "pod-3": 83 | found[name] = true 84 | } 85 | } 86 | 87 | require.Equal(t, 2, len(found)) 88 | } 89 | 90 | func TestCache_update(t *testing.T) { 91 | initial := []metav1.Object{ 92 | testGenPod("default", "pod-1", "1"), 93 | testGenPod("default", "pod-2", "2"), 94 | } 95 | 96 | ctx, cancel := context.WithCancel(context.Background()) 97 | defer cancel() 98 | 99 | stopch := make(chan struct{}) 100 | defer close(stopch) 101 | 102 | log := logutil.Default() 103 | filter := filter.Null() 104 | 105 | cache := newCache(ctx, log, stopch, filter) 106 | 107 | // first sync returns zero events 108 | evs, err := cache.sync(initial) 109 | assert.NoError(t, err) 110 | assert.NotEmpty(t, evs) 111 | 112 | { 113 | events, err := cache.update(testGenEvent(EventTypeUpdate, "default", "pod-1", "3")) 114 | assert.NoError(t, err) 115 | require.Len(t, events, 1) 116 | assert.Equal(t, EventTypeUpdate, events[0].Type()) 117 | assert.Equal(t, "pod-1", events[0].Resource().GetName()) 118 | } 119 | 120 | { 121 | events, err := cache.update(testGenEvent(EventTypeDelete, "default", "pod-2", "4")) 122 | assert.NoError(t, err) 123 | require.Len(t, events, 1) 124 | assert.Equal(t, EventTypeDelete, events[0].Type()) 125 | assert.Equal(t, "pod-2", events[0].Resource().GetName()) 126 | } 127 | 128 | { 129 | events, err := cache.update(testGenEvent(EventTypeCreate, "default", "pod-3", "5")) 130 | assert.NoError(t, err) 131 | require.Len(t, events, 1) 132 | assert.Equal(t, EventTypeCreate, events[0].Type()) 133 | assert.Equal(t, "pod-3", events[0].Resource().GetName()) 134 | } 135 | 136 | list, err := cache.List() 137 | require.NoError(t, err) 138 | assert.Len(t, list, 2) 139 | 140 | found := make(map[string]bool) 141 | for _, obj := range list { 142 | name := obj.GetName() 143 | switch name { 144 | case "pod-1": 145 | found[name] = true 146 | case "pod-2": 147 | assert.Failf(t, "found unexpected pod in list", name) 148 | case "pod-3": 149 | found[name] = true 150 | } 151 | } 152 | require.Equal(t, 2, len(found)) 153 | } 154 | 155 | func TestCache_refilter(t *testing.T) { 156 | initial := []metav1.Object{ 157 | testGenPod("default", "pod-1", "1"), 158 | testGenPod("default", "pod-2", "2"), 159 | } 160 | 161 | ctx, cancel := context.WithCancel(context.Background()) 162 | defer cancel() 163 | 164 | stopch := make(chan struct{}) 165 | defer close(stopch) 166 | 167 | log := logutil.Default() 168 | 169 | cache := newCache(ctx, log, stopch, filter.Null()) 170 | 171 | // first sync returns zero events 172 | evts, err := cache.sync(initial) 173 | assert.NoError(t, err) 174 | assert.NotEmpty(t, evts) 175 | 176 | filter := filter.FN(func(obj metav1.Object) bool { 177 | return obj.GetNamespace() == "default" && 178 | obj.GetName() == "pod-1" && 179 | obj.GetResourceVersion() < "5" 180 | }) 181 | 182 | events, err := cache.refilter(initial, filter) 183 | assert.NoError(t, err) 184 | require.Len(t, events, 1) 185 | 186 | evt := events[0] 187 | assert.Equal(t, EventTypeDelete, evt.Type()) 188 | assert.Equal(t, "pod-2", evt.Resource().GetName()) 189 | 190 | list, err := cache.List() 191 | require.NoError(t, err) 192 | require.Len(t, list, 1) 193 | obj := list[0] 194 | require.Equal(t, "pod-1", obj.GetName()) 195 | 196 | evts, err = cache.update(NewEvent(EventTypeDelete, testGenPod("default", "pod-2", "3"))) 197 | assert.NoError(t, err) 198 | assert.Empty(t, evts) 199 | evts, err = cache.update(NewEvent(EventTypeUpdate, testGenPod("default", "pod-2", "3"))) 200 | assert.NoError(t, err) 201 | assert.Empty(t, evts) 202 | evts, err = cache.update(NewEvent(EventTypeCreate, testGenPod("default", "pod-2", "3"))) 203 | assert.NoError(t, err) 204 | assert.Empty(t, evts) 205 | 206 | evts, err = cache.update(NewEvent(EventTypeDelete, testGenPod("default", "pod-3", "3"))) 207 | assert.NoError(t, err) 208 | assert.Empty(t, evts) 209 | evts, err = cache.update(NewEvent(EventTypeUpdate, testGenPod("default", "pod-3", "3"))) 210 | assert.NoError(t, err) 211 | assert.Empty(t, evts) 212 | evts, err = cache.update(NewEvent(EventTypeCreate, testGenPod("default", "pod-3", "3"))) 213 | assert.NoError(t, err) 214 | assert.Empty(t, evts) 215 | 216 | evts, err = cache.update(NewEvent(EventTypeUpdate, testGenPod("default", "pod-1", "5"))) 217 | assert.NoError(t, err) 218 | assert.Len(t, evts, 1) 219 | assert.Equal(t, EventTypeDelete, evts[0].Type()) 220 | assert.Equal(t, "default", evts[0].Resource().GetNamespace()) 221 | assert.Equal(t, "pod-1", evts[0].Resource().GetName()) 222 | 223 | } 224 | 225 | func TestCache_lifecycle_ctx(t *testing.T) { 226 | ctx, cancel := context.WithCancel(context.Background()) 227 | 228 | log := logutil.Default() 229 | 230 | cache := newCache(ctx, log, nil, filter.Null()) 231 | 232 | evts, err := cache.sync([]metav1.Object{testGenPod("a", "b", "1")}) 233 | assert.NoError(t, err) 234 | assert.Len(t, evts, 1) 235 | 236 | obj, err := cache.Get("a", "b") 237 | assert.NoError(t, err) 238 | require.NotNil(t, obj) 239 | assert.Equal(t, "a", obj.GetNamespace()) 240 | assert.Equal(t, "b", obj.GetName()) 241 | 242 | list, err := cache.List() 243 | assert.NoError(t, err) 244 | assert.Len(t, list, 1) 245 | 246 | cancel() 247 | 248 | testutil.AssertDone(t, "cache", cache) 249 | 250 | evts, err = cache.sync([]metav1.Object{testGenPod("a", "b", "1")}) 251 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 252 | assert.Nil(t, evts) 253 | 254 | evts, err = cache.update(testGenEvent(EventTypeCreate, "a", "b", "2")) 255 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 256 | assert.Nil(t, evts) 257 | 258 | evts, err = cache.refilter([]metav1.Object{testGenPod("a", "c", "3")}, filter.All()) 259 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 260 | assert.Nil(t, evts) 261 | 262 | list, err = cache.List() 263 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 264 | assert.Empty(t, list) 265 | 266 | obj, err = cache.Get("a", "b") 267 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 268 | assert.Nil(t, obj) 269 | } 270 | 271 | func TestCache_lifecycle_stopch(t *testing.T) { 272 | ctx, cancel := context.WithCancel(context.Background()) 273 | defer cancel() 274 | 275 | stopch := make(chan struct{}) 276 | 277 | log := logutil.Default() 278 | 279 | cache := newCache(ctx, log, stopch, filter.Null()) 280 | 281 | close(stopch) 282 | testutil.AssertDone(t, "cache", cache) 283 | 284 | evts, err := cache.sync([]metav1.Object{testGenPod("a", "b", "1")}) 285 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 286 | assert.Nil(t, evts) 287 | 288 | evts, err = cache.update(testGenEvent(EventTypeCreate, "a", "b", "2")) 289 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 290 | assert.Nil(t, evts) 291 | 292 | evts, err = cache.refilter([]metav1.Object{testGenPod("a", "c", "3")}, filter.All()) 293 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 294 | assert.Nil(t, evts) 295 | 296 | list, err := cache.List() 297 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 298 | assert.Empty(t, list) 299 | 300 | obj, err := cache.Get("a", "b") 301 | assert.Equal(t, ErrNotRunning, errors.Cause(err)) 302 | assert.Nil(t, obj) 303 | } 304 | -------------------------------------------------------------------------------- /subscription_filter_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/nsname" 10 | "github.com/boz/kcache/testutil" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestFilterSubscriptionReady_immediate(t *testing.T) { 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | defer cancel() 18 | 19 | log := logutil.Default() 20 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 21 | sub := newFilterSubscription(log, parent, filter.Null(), false) 22 | defer parent.Close() 23 | 24 | testDoFilterSubscriptionReady(t, "immediate", parent, sub, cache) 25 | 26 | close(readych) 27 | 28 | testutil.AssertReady(t, "immediate", sub) 29 | 30 | list, err := sub.Cache().List() 31 | assert.NoError(t, err) 32 | assert.NotEmpty(t, list) 33 | 34 | evt := testGenEvent(EventTypeCreate, "a", "c", "1") 35 | parent.send(evt) 36 | 37 | select { 38 | case <-sub.Events(): 39 | case <-testutil.AsyncWaitch(ctx): 40 | assert.Fail(t, "deferred") 41 | } 42 | 43 | testDoTestFilterSubscriptionShutdown(t, "immediate", parent, sub) 44 | 45 | } 46 | 47 | func TestFilterSubscriptionReady_deferred(t *testing.T) { 48 | ctx, cancel := context.WithCancel(context.Background()) 49 | defer cancel() 50 | 51 | log := logutil.Default() 52 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 53 | sub := newFilterSubscription(log, parent, filter.Null(), true) 54 | defer parent.Close() 55 | 56 | testDoFilterSubscriptionReady(t, "deferred", parent, sub, cache) 57 | 58 | close(readych) 59 | 60 | testutil.AssertNotReady(t, "deferred", sub) 61 | 62 | list, err := sub.Cache().List() 63 | assert.NoError(t, err) 64 | assert.Empty(t, list) 65 | 66 | evt := testGenEvent(EventTypeCreate, "a", "c", "1") 67 | parent.send(evt) 68 | 69 | select { 70 | case <-sub.Events(): 71 | assert.Fail(t, "deferred") 72 | case <-testutil.AsyncWaitch(ctx): 73 | } 74 | 75 | testDoTestFilterSubscriptionShutdown(t, "deferred", parent, sub) 76 | 77 | } 78 | 79 | func TestFilterSubscriptionRefilter_immediate_refilter_before_ready(t *testing.T) { 80 | ctx, cancel := context.WithCancel(context.Background()) 81 | defer cancel() 82 | 83 | log := logutil.Default() 84 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 85 | sub := newFilterSubscription(log, parent, filter.Null(), false) 86 | defer parent.Close() 87 | 88 | cache.update(testGenEvent(EventTypeCreate, "a", "b", "1")) 89 | cache.update(testGenEvent(EventTypeCreate, "a", "c", "2")) 90 | 91 | f := filter.NSName(nsname.New("a", "c")) 92 | 93 | sub.Refilter(f) 94 | sub.Refilter(f) 95 | 96 | testutil.AssertNotReady(t, "ready", sub) 97 | 98 | parent.send(testGenEvent(EventTypeCreate, "a", "d", "3")) 99 | 100 | select { 101 | case <-sub.Events(): 102 | assert.Fail(t, "events before ready") 103 | case <-testutil.AsyncWaitch(ctx): 104 | } 105 | 106 | close(readych) 107 | 108 | testutil.AssertReady(t, "ready", sub) 109 | 110 | select { 111 | case <-sub.Events(): 112 | assert.Fail(t, "events after ready") 113 | case <-testutil.AsyncWaitch(ctx): 114 | } 115 | 116 | list, err := sub.Cache().List() 117 | assert.NoError(t, err) 118 | assert.Len(t, list, 1) 119 | 120 | assert.Equal(t, "a", list[0].GetNamespace()) 121 | assert.Equal(t, "c", list[0].GetName()) 122 | 123 | parent.send(testGenEvent(EventTypeCreate, "a", "c", "4")) 124 | select { 125 | case evt, ok := <-sub.Events(): 126 | require.True(t, ok) 127 | require.NotNil(t, evt) 128 | assert.Equal(t, EventTypeUpdate, evt.Type()) 129 | assert.Equal(t, "a", evt.Resource().GetNamespace()) 130 | assert.Equal(t, "c", evt.Resource().GetName()) 131 | case <-testutil.AsyncWaitch(ctx): 132 | assert.Fail(t, "events after ready") 133 | } 134 | 135 | parent.send(testGenEvent(EventTypeCreate, "b", "c", "4")) 136 | select { 137 | case <-sub.Events(): 138 | assert.Fail(t, "filtered event") 139 | case <-testutil.AsyncWaitch(ctx): 140 | } 141 | 142 | sub.Refilter(f) 143 | select { 144 | case <-sub.Events(): 145 | assert.Fail(t, "events for unchanged refilter") 146 | case <-testutil.AsyncWaitch(ctx): 147 | } 148 | 149 | sub.Refilter(filter.All()) 150 | select { 151 | case evt, ok := <-sub.Events(): 152 | assert.True(t, ok) 153 | assert.Equal(t, EventTypeDelete, evt.Type()) 154 | assert.Equal(t, "a", evt.Resource().GetNamespace()) 155 | assert.Equal(t, "c", evt.Resource().GetName()) 156 | case <-testutil.AsyncWaitch(ctx): 157 | assert.Fail(t, "events for unchanged refilter") 158 | } 159 | 160 | sub.Close() 161 | testutil.AssertDone(t, "subscription", sub) 162 | } 163 | 164 | func TestFilterSubscriptionRefilter_immediate_refilter_after_ready(t *testing.T) { 165 | ctx, cancel := context.WithCancel(context.Background()) 166 | defer cancel() 167 | 168 | log := logutil.Default() 169 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 170 | sub := newFilterSubscription(log, parent, filter.Null(), false) 171 | defer parent.Close() 172 | 173 | cache.update(testGenEvent(EventTypeCreate, "a", "b", "1")) 174 | cache.update(testGenEvent(EventTypeCreate, "a", "c", "2")) 175 | 176 | f := filter.NSName(nsname.New("a", "c")) 177 | 178 | close(readych) 179 | 180 | testutil.AssertReady(t, "ready", sub) 181 | 182 | sub.Refilter(f) 183 | 184 | select { 185 | case evt, ok := <-sub.Events(): 186 | require.True(t, ok) 187 | require.NotNil(t, evt) 188 | assert.Equal(t, EventTypeDelete, evt.Type()) 189 | assert.Equal(t, "a", evt.Resource().GetNamespace()) 190 | assert.Equal(t, "b", evt.Resource().GetName()) 191 | case <-testutil.AsyncWaitch(ctx): 192 | assert.Fail(t, "events after refilter") 193 | } 194 | 195 | list, err := sub.Cache().List() 196 | assert.NoError(t, err) 197 | assert.Len(t, list, 1) 198 | assert.Equal(t, "a", list[0].GetNamespace()) 199 | assert.Equal(t, "c", list[0].GetName()) 200 | 201 | parent.send(testGenEvent(EventTypeCreate, "a", "c", "4")) 202 | select { 203 | case evt, ok := <-sub.Events(): 204 | require.True(t, ok) 205 | require.NotNil(t, evt) 206 | assert.Equal(t, EventTypeUpdate, evt.Type()) 207 | assert.Equal(t, "a", evt.Resource().GetNamespace()) 208 | assert.Equal(t, "c", evt.Resource().GetName()) 209 | case <-testutil.AsyncWaitch(ctx): 210 | assert.Fail(t, "events after ready") 211 | } 212 | 213 | parent.send(testGenEvent(EventTypeCreate, "b", "c", "4")) 214 | select { 215 | case <-sub.Events(): 216 | assert.Fail(t, "filtered event") 217 | case <-testutil.AsyncWaitch(ctx): 218 | } 219 | 220 | } 221 | 222 | func TestFilterSubscriptionRefilter_deferred_refilter_before_ready(t *testing.T) { 223 | ctx, cancel := context.WithCancel(context.Background()) 224 | defer cancel() 225 | 226 | log := logutil.Default() 227 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 228 | sub := newFilterSubscription(log, parent, filter.Null(), true) 229 | defer parent.Close() 230 | 231 | cache.update(testGenEvent(EventTypeCreate, "a", "b", "1")) 232 | cache.update(testGenEvent(EventTypeCreate, "a", "c", "2")) 233 | 234 | f := filter.NSName(nsname.New("a", "c")) 235 | 236 | sub.Refilter(f) 237 | 238 | testutil.AssertNotReady(t, "ready before refilter", sub) 239 | 240 | close(readych) 241 | 242 | testutil.AssertReady(t, "sub after refilter", sub) 243 | 244 | select { 245 | case <-sub.Events(): 246 | assert.Fail(t, "events after refilter") 247 | case <-testutil.AsyncWaitch(ctx): 248 | } 249 | 250 | list, err := sub.Cache().List() 251 | assert.NoError(t, err) 252 | assert.Len(t, list, 1) 253 | assert.Equal(t, "a", list[0].GetNamespace()) 254 | assert.Equal(t, "c", list[0].GetName()) 255 | 256 | parent.send(testGenEvent(EventTypeCreate, "a", "c", "4")) 257 | select { 258 | case evt, ok := <-sub.Events(): 259 | require.True(t, ok) 260 | require.NotNil(t, evt) 261 | assert.Equal(t, EventTypeUpdate, evt.Type()) 262 | assert.Equal(t, "a", evt.Resource().GetNamespace()) 263 | assert.Equal(t, "c", evt.Resource().GetName()) 264 | case <-testutil.AsyncWaitch(ctx): 265 | assert.Fail(t, "events after ready") 266 | } 267 | 268 | parent.send(testGenEvent(EventTypeCreate, "b", "c", "4")) 269 | select { 270 | case <-sub.Events(): 271 | assert.Fail(t, "filtered event") 272 | case <-testutil.AsyncWaitch(ctx): 273 | } 274 | 275 | sub.Close() 276 | testutil.AssertDone(t, "subscription", sub) 277 | } 278 | 279 | func TestFilterSubscriptionRefilter_deferred_refilter_after_ready(t *testing.T) { 280 | ctx, cancel := context.WithCancel(context.Background()) 281 | defer cancel() 282 | 283 | log := logutil.Default() 284 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 285 | sub := newFilterSubscription(log, parent, filter.Null(), true) 286 | defer parent.Close() 287 | 288 | cache.update(testGenEvent(EventTypeCreate, "a", "b", "1")) 289 | cache.update(testGenEvent(EventTypeCreate, "a", "c", "2")) 290 | 291 | f := filter.NSName(nsname.New("a", "c")) 292 | 293 | close(readych) 294 | 295 | testutil.AssertNotReady(t, "sub before refilter", sub) 296 | 297 | sub.Refilter(f) 298 | 299 | testutil.AssertReady(t, "sub after refilter", sub) 300 | 301 | select { 302 | case <-sub.Events(): 303 | assert.Fail(t, "events after refilter") 304 | case <-testutil.AsyncWaitch(ctx): 305 | } 306 | 307 | list, err := sub.Cache().List() 308 | assert.NoError(t, err) 309 | assert.Len(t, list, 1) 310 | assert.Equal(t, "a", list[0].GetNamespace()) 311 | assert.Equal(t, "c", list[0].GetName()) 312 | 313 | parent.send(testGenEvent(EventTypeCreate, "a", "c", "4")) 314 | select { 315 | case evt, ok := <-sub.Events(): 316 | require.True(t, ok) 317 | require.NotNil(t, evt) 318 | assert.Equal(t, EventTypeUpdate, evt.Type()) 319 | assert.Equal(t, "a", evt.Resource().GetNamespace()) 320 | assert.Equal(t, "c", evt.Resource().GetName()) 321 | case <-testutil.AsyncWaitch(ctx): 322 | assert.Fail(t, "events after ready") 323 | } 324 | 325 | parent.send(testGenEvent(EventTypeCreate, "b", "c", "4")) 326 | select { 327 | case <-sub.Events(): 328 | assert.Fail(t, "filtered event") 329 | case <-testutil.AsyncWaitch(ctx): 330 | } 331 | 332 | sub.Close() 333 | testutil.AssertDone(t, "subscription", sub) 334 | } 335 | 336 | func testDoFilterSubscriptionReady(t *testing.T, name string, parent subscription, sub FilterSubscription, c cache) { 337 | 338 | ctx, cancel := context.WithCancel(context.Background()) 339 | defer cancel() 340 | 341 | testutil.AssertNotDone(t, name, sub) 342 | testutil.AssertNotReady(t, name, sub) 343 | 344 | evt := testGenEvent(EventTypeCreate, "a", "b", "1") 345 | c.update(evt) 346 | parent.send(evt) 347 | 348 | testutil.AssertNotReady(t, name, sub) 349 | 350 | list, err := sub.Cache().List() 351 | assert.NoError(t, err, name) 352 | assert.Empty(t, list, name) 353 | 354 | select { 355 | case <-sub.Events(): 356 | assert.Fail(t, name) 357 | case <-testutil.AsyncWaitch(ctx): 358 | } 359 | 360 | } 361 | 362 | func testDoTestFilterSubscriptionShutdown(t *testing.T, name string, parent subscription, sub FilterSubscription) { 363 | 364 | ctx, cancel := context.WithCancel(context.Background()) 365 | defer cancel() 366 | 367 | parent.Close() 368 | testutil.AssertDone(t, name, sub) 369 | 370 | select { 371 | case _, ok := <-sub.Events(): 372 | assert.False(t, ok, name) 373 | case <-testutil.AsyncWaitch(ctx): 374 | assert.Fail(t, name) 375 | } 376 | 377 | } 378 | -------------------------------------------------------------------------------- /publisher_test.go: -------------------------------------------------------------------------------- 1 | package kcache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache/filter" 9 | "github.com/boz/kcache/nsname" 10 | "github.com/boz/kcache/testutil" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestPublisher_lifecycle(t *testing.T) { 16 | log := logutil.Default() 17 | parent, _, readych := testNewSubscription(t, log, filter.Null()) 18 | publisher := newPublisher(log, parent) 19 | defer parent.Close() 20 | 21 | sub, err := publisher.Subscribe() 22 | require.NoError(t, err) 23 | require.NotNil(t, sub) 24 | 25 | sub_wf, err := publisher.SubscribeWithFilter(filter.Null()) 26 | require.NoError(t, err) 27 | require.NotNil(t, sub_wf) 28 | 29 | sub_ff, err := publisher.SubscribeForFilter() 30 | require.NoError(t, err) 31 | require.NotNil(t, sub_ff) 32 | 33 | clone, err := publisher.Clone() 34 | require.NoError(t, err) 35 | require.NotNil(t, clone) 36 | 37 | clone_wf, err := publisher.CloneWithFilter(filter.Null()) 38 | require.NoError(t, err) 39 | require.NotNil(t, clone_wf) 40 | 41 | clone_ff, err := publisher.CloneForFilter() 42 | require.NoError(t, err) 43 | require.NotNil(t, clone_ff) 44 | 45 | testutil.AssertNotReady(t, "publisher", publisher) 46 | testutil.AssertNotReady(t, "sub", sub) 47 | testutil.AssertNotReady(t, "sub_wf", sub_wf) 48 | testutil.AssertNotReady(t, "sub_ff", sub_ff) 49 | testutil.AssertNotReady(t, "clone", clone) 50 | testutil.AssertNotReady(t, "clone_wf", clone_wf) 51 | testutil.AssertNotReady(t, "clone_ff", clone_ff) 52 | 53 | close(readych) 54 | 55 | testutil.AssertReady(t, "publisher", publisher) 56 | testutil.AssertReady(t, "sub", sub) 57 | testutil.AssertReady(t, "sub_wf", sub_wf) 58 | testutil.AssertNotReady(t, "sub_ff", sub_ff) 59 | testutil.AssertReady(t, "clone", clone) 60 | testutil.AssertReady(t, "clone_wf", clone_wf) 61 | testutil.AssertNotReady(t, "clone_ff", clone_ff) 62 | 63 | publisher.Close() 64 | 65 | testutil.AssertDone(t, "parent", parent) 66 | testutil.AssertDone(t, "publisher", publisher) 67 | testutil.AssertDone(t, "sub", sub) 68 | testutil.AssertDone(t, "sub_wf", sub_wf) 69 | testutil.AssertDone(t, "sub_ff", sub_ff) 70 | testutil.AssertDone(t, "clone", clone) 71 | testutil.AssertDone(t, "clone_wf", clone_wf) 72 | testutil.AssertDone(t, "clone_ff", clone_ff) 73 | } 74 | 75 | func TestPublisher_Subscribe(t *testing.T) { 76 | log := logutil.Default() 77 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 78 | publisher := newPublisher(log, parent) 79 | defer parent.Close() 80 | 81 | doTestPublisherSubscribe(t, parent, cache, publisher, readych) 82 | } 83 | 84 | func TestFilterPublisher_Subscribe(t *testing.T) { 85 | log := logutil.Default() 86 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 87 | publisher := newPublisher(log, parent) 88 | defer parent.Close() 89 | 90 | fpublisher, err := publisher.CloneWithFilter(filter.Null()) 91 | require.NoError(t, err) 92 | 93 | doTestPublisherSubscribe(t, parent, cache, fpublisher, readych) 94 | } 95 | 96 | func doTestPublisherSubscribe(t *testing.T, 97 | parent subscription, cache cache, publisher Controller, readych chan struct{}) { 98 | 99 | close(readych) 100 | 101 | sub, err := publisher.Subscribe() 102 | require.NoError(t, err) 103 | 104 | testutil.AssertReady(t, "subscriber", sub) 105 | 106 | testPublisherSubscriber(t, parent, cache, sub) 107 | 108 | publisher.Close() 109 | testutil.AssertDone(t, "publisher", publisher) 110 | testutil.AssertDone(t, "sub", sub) 111 | } 112 | 113 | func TestPublisher_SubscribeWithFilter(t *testing.T) { 114 | log := logutil.Default() 115 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 116 | publisher := newPublisher(log, parent) 117 | defer parent.Close() 118 | 119 | doTestPublisherSubscribeWithFilter(t, parent, cache, publisher, readych) 120 | } 121 | 122 | func TestFilterPublisher_SubscribeWithFilter(t *testing.T) { 123 | log := logutil.Default() 124 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 125 | publisher := newPublisher(log, parent) 126 | defer parent.Close() 127 | 128 | fpublisher, err := publisher.CloneWithFilter(filter.Null()) 129 | require.NoError(t, err) 130 | 131 | doTestPublisherSubscribeWithFilter(t, parent, cache, fpublisher, readych) 132 | } 133 | 134 | func doTestPublisherSubscribeWithFilter(t *testing.T, 135 | parent subscription, cache cache, publisher Controller, readych chan struct{}) { 136 | 137 | close(readych) 138 | 139 | f := filter.NSName(nsname.New("a", "c")) 140 | sub, err := publisher.SubscribeWithFilter(f) 141 | require.NoError(t, err) 142 | 143 | testutil.AssertReady(t, "sub", sub) 144 | 145 | testPublisherFilteredSubscriber(t, parent, cache, sub) 146 | 147 | publisher.Close() 148 | testutil.AssertDone(t, "publisher", publisher) 149 | } 150 | 151 | func TestPublisher_SubscribeForFilter(t *testing.T) { 152 | log := logutil.Default() 153 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 154 | publisher := newPublisher(log, parent) 155 | defer parent.Close() 156 | doTestPublisherSubscribeForFilter(t, parent, cache, publisher, readych) 157 | } 158 | 159 | func TestFilterPublisher_SubscribeForFilter(t *testing.T) { 160 | log := logutil.Default() 161 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 162 | publisher := newPublisher(log, parent) 163 | defer parent.Close() 164 | 165 | fpublisher, err := publisher.CloneWithFilter(filter.Null()) 166 | require.NoError(t, err) 167 | 168 | doTestPublisherSubscribeForFilter(t, parent, cache, fpublisher, readych) 169 | } 170 | 171 | func doTestPublisherSubscribeForFilter(t *testing.T, 172 | parent subscription, cache cache, publisher Controller, readych chan struct{}) { 173 | 174 | close(readych) 175 | 176 | sub, err := publisher.SubscribeForFilter() 177 | require.NoError(t, err) 178 | 179 | testutil.AssertNotReady(t, "before refilter", sub) 180 | 181 | f := filter.NSName(nsname.New("a", "c")) 182 | sub.Refilter(f) 183 | 184 | testutil.AssertReady(t, "after refilter", sub) 185 | 186 | testPublisherFilteredSubscriber(t, parent, cache, sub) 187 | 188 | publisher.Close() 189 | testutil.AssertDone(t, "publisher", publisher) 190 | } 191 | 192 | func TestPublisher_Clone(t *testing.T) { 193 | log := logutil.Default() 194 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 195 | publisher := newPublisher(log, parent) 196 | defer parent.Close() 197 | 198 | doTestPublisherClone(t, parent, cache, publisher, readych) 199 | } 200 | 201 | func TestFilterPublisher_Clone(t *testing.T) { 202 | log := logutil.Default() 203 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 204 | publisher := newPublisher(log, parent) 205 | defer parent.Close() 206 | 207 | fpublisher, err := publisher.CloneWithFilter(filter.Null()) 208 | require.NoError(t, err) 209 | 210 | doTestPublisherClone(t, parent, cache, fpublisher, readych) 211 | } 212 | 213 | func doTestPublisherClone(t *testing.T, 214 | parent subscription, cache cache, publisher Controller, readych chan struct{}) { 215 | 216 | close(readych) 217 | 218 | ppublisher, err := publisher.Clone() 219 | require.NoError(t, err) 220 | 221 | sub, err := ppublisher.Subscribe() 222 | require.NoError(t, err) 223 | 224 | testPublisherSubscriber(t, parent, cache, sub) 225 | 226 | publisher.Close() 227 | testutil.AssertDone(t, "publisher", publisher) 228 | testutil.AssertDone(t, "sub", sub) 229 | } 230 | 231 | func TestPublisher_CloneWithFilter(t *testing.T) { 232 | log := logutil.Default() 233 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 234 | publisher := newPublisher(log, parent) 235 | defer parent.Close() 236 | 237 | doTestPublisherCloneWithFilter(t, parent, cache, publisher, readych) 238 | } 239 | 240 | func TestFilterPublisher_CloneWithFilter(t *testing.T) { 241 | log := logutil.Default() 242 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 243 | publisher := newPublisher(log, parent) 244 | defer parent.Close() 245 | 246 | fpublisher, err := publisher.CloneWithFilter(filter.Null()) 247 | require.NoError(t, err) 248 | 249 | doTestPublisherCloneWithFilter(t, parent, cache, fpublisher, readych) 250 | } 251 | 252 | func doTestPublisherCloneWithFilter(t *testing.T, 253 | parent subscription, cache cache, publisher Controller, readych chan struct{}) { 254 | 255 | close(readych) 256 | 257 | f := filter.NSName(nsname.New("a", "c")) 258 | ppublisher, err := publisher.CloneWithFilter(f) 259 | require.NoError(t, err) 260 | 261 | sub, err := ppublisher.Subscribe() 262 | require.NoError(t, err) 263 | 264 | testPublisherFilteredSubscriber(t, parent, cache, sub) 265 | 266 | publisher.Close() 267 | testutil.AssertDone(t, "publisher", publisher) 268 | } 269 | 270 | func TestPublisher_CloneForFilter(t *testing.T) { 271 | log := logutil.Default() 272 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 273 | publisher := newPublisher(log, parent) 274 | defer parent.Close() 275 | 276 | doTestPublisherCloneForFilter(t, parent, cache, publisher, readych) 277 | } 278 | 279 | func TestFilterPublisher_CloneForFilter(t *testing.T) { 280 | log := logutil.Default() 281 | parent, cache, readych := testNewSubscription(t, log, filter.Null()) 282 | publisher := newPublisher(log, parent) 283 | defer parent.Close() 284 | 285 | fpublisher, err := publisher.CloneWithFilter(filter.Null()) 286 | require.NoError(t, err) 287 | 288 | doTestPublisherCloneForFilter(t, parent, cache, fpublisher, readych) 289 | } 290 | 291 | func doTestPublisherCloneForFilter(t *testing.T, 292 | parent subscription, cache cache, publisher Controller, readych chan struct{}) { 293 | 294 | close(readych) 295 | 296 | ppublisher, err := publisher.CloneForFilter() 297 | require.NoError(t, err) 298 | 299 | sub, err := ppublisher.Subscribe() 300 | require.NoError(t, err) 301 | 302 | testutil.AssertNotReady(t, "ppublisher", ppublisher) 303 | testutil.AssertNotReady(t, "sub", sub) 304 | 305 | f := filter.NSName(nsname.New("a", "c")) 306 | ppublisher.Refilter(f) 307 | 308 | testutil.AssertReady(t, "ppublisher", ppublisher) 309 | testutil.AssertReady(t, "sub", sub) 310 | 311 | testPublisherFilteredSubscriber(t, parent, cache, sub) 312 | 313 | publisher.Close() 314 | testutil.AssertDone(t, "publisher", publisher) 315 | } 316 | 317 | func testPublisherSubscriber(t *testing.T, parent subscription, cache cache, sub Subscription) { 318 | ctx, cancel := context.WithCancel(context.Background()) 319 | defer cancel() 320 | 321 | evt := testGenEvent(EventTypeCreate, "a", "b", "1") 322 | cache.update(evt) 323 | parent.send(evt) 324 | 325 | select { 326 | case ev, ok := <-sub.Events(): 327 | assert.True(t, ok) 328 | assert.NotNil(t, ev) 329 | assert.Equal(t, EventTypeCreate, ev.Type()) 330 | assert.Equal(t, "a", ev.Resource().GetNamespace()) 331 | assert.Equal(t, "b", ev.Resource().GetName()) 332 | case <-testutil.AsyncWaitch(ctx): 333 | assert.Fail(t, "no event") 334 | } 335 | 336 | list, err := sub.Cache().List() 337 | assert.NoError(t, err) 338 | require.Len(t, list, 1) 339 | assert.Equal(t, "a", list[0].GetNamespace()) 340 | assert.Equal(t, "b", list[0].GetName()) 341 | } 342 | 343 | func testPublisherFilteredSubscriber(t *testing.T, parent subscription, cache cache, sub Subscription) { 344 | ctx, cancel := context.WithCancel(context.Background()) 345 | defer cancel() 346 | 347 | evt := testGenEvent(EventTypeCreate, "a", "b", "1") 348 | cache.update(evt) 349 | parent.send(evt) 350 | 351 | select { 352 | case <-sub.Events(): 353 | assert.Fail(t, "filtered event") 354 | case <-testutil.AsyncWaitch(ctx): 355 | } 356 | 357 | evt = testGenEvent(EventTypeCreate, "a", "c", "1") 358 | cache.update(evt) 359 | parent.send(evt) 360 | 361 | select { 362 | case ev, ok := <-sub.Events(): 363 | assert.True(t, ok) 364 | assert.NotNil(t, ev) 365 | assert.Equal(t, EventTypeCreate, ev.Type()) 366 | assert.Equal(t, "a", ev.Resource().GetNamespace()) 367 | assert.Equal(t, "c", ev.Resource().GetName()) 368 | case <-testutil.AsyncWaitch(ctx): 369 | assert.Fail(t, "no event") 370 | } 371 | 372 | list, err := sub.Cache().List() 373 | assert.NoError(t, err) 374 | require.Len(t, list, 1) 375 | assert.Equal(t, "a", list[0].GetNamespace()) 376 | assert.Equal(t, "c", list[0].GetName()) 377 | } 378 | -------------------------------------------------------------------------------- /types/gen/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | logutil "github.com/boz/go-logutil" 8 | "github.com/boz/kcache" 9 | "github.com/boz/kcache/client" 10 | "github.com/boz/kcache/filter" 11 | "github.com/cheekybits/genny/generic" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/kubernetes" 14 | 15 | appsv1 "k8s.io/api/apps/v1" 16 | ) 17 | 18 | var ( 19 | ErrInvalidType = fmt.Errorf("invalid type") 20 | adapter = _adapter{} 21 | ) 22 | 23 | var _ = appsv1.Deployment{} 24 | 25 | type ObjectType generic.Type 26 | 27 | type Event interface { 28 | Type() kcache.EventType 29 | Resource() ObjectType 30 | } 31 | 32 | type CacheReader interface { 33 | Get(ns string, name string) (ObjectType, error) 34 | List() ([]ObjectType, error) 35 | } 36 | 37 | type CacheController interface { 38 | Cache() CacheReader 39 | Ready() <-chan struct{} 40 | } 41 | 42 | type Subscription interface { 43 | CacheController 44 | Events() <-chan Event 45 | Close() 46 | Done() <-chan struct{} 47 | } 48 | 49 | type Publisher interface { 50 | Subscribe() (Subscription, error) 51 | SubscribeWithFilter(filter.Filter) (FilterSubscription, error) 52 | SubscribeForFilter() (FilterSubscription, error) 53 | Clone() (Controller, error) 54 | CloneWithFilter(filter.Filter) (FilterController, error) 55 | CloneForFilter() (FilterController, error) 56 | } 57 | 58 | type Controller interface { 59 | CacheController 60 | Publisher 61 | Done() <-chan struct{} 62 | Close() 63 | Error() error 64 | } 65 | 66 | type FilterSubscription interface { 67 | Subscription 68 | Refilter(filter.Filter) error 69 | } 70 | 71 | type FilterController interface { 72 | Controller 73 | Refilter(filter.Filter) error 74 | } 75 | 76 | type BaseHandler interface { 77 | OnCreate(ObjectType) 78 | OnUpdate(ObjectType) 79 | OnDelete(ObjectType) 80 | } 81 | 82 | type Handler interface { 83 | BaseHandler 84 | OnInitialize([]ObjectType) 85 | } 86 | 87 | type HandlerBuilder interface { 88 | OnInitialize(func([]ObjectType)) HandlerBuilder 89 | OnCreate(func(ObjectType)) HandlerBuilder 90 | OnUpdate(func(ObjectType)) HandlerBuilder 91 | OnDelete(func(ObjectType)) HandlerBuilder 92 | Create() Handler 93 | } 94 | 95 | type UnitaryHandler interface { 96 | BaseHandler 97 | OnInitialize(ObjectType) 98 | } 99 | 100 | type UnitaryHandlerBuilder interface { 101 | OnInitialize(func(ObjectType)) UnitaryHandlerBuilder 102 | OnCreate(func(ObjectType)) UnitaryHandlerBuilder 103 | OnUpdate(func(ObjectType)) UnitaryHandlerBuilder 104 | OnDelete(func(ObjectType)) UnitaryHandlerBuilder 105 | Create() UnitaryHandler 106 | } 107 | 108 | type _adapter struct{} 109 | 110 | func (_adapter) adaptObject(obj metav1.Object) (ObjectType, error) { 111 | if obj, ok := obj.(ObjectType); ok { 112 | return obj, nil 113 | } 114 | return nil, ErrInvalidType 115 | } 116 | 117 | func (a _adapter) adaptList(objs []metav1.Object) ([]ObjectType, error) { 118 | var ret []ObjectType 119 | for _, orig := range objs { 120 | adapted, err := a.adaptObject(orig) 121 | if err != nil { 122 | continue 123 | } 124 | ret = append(ret, adapted) 125 | } 126 | return ret, nil 127 | } 128 | 129 | func newCache(parent kcache.CacheReader) CacheReader { 130 | return &cache{parent} 131 | } 132 | 133 | type cache struct { 134 | parent kcache.CacheReader 135 | } 136 | 137 | func (c *cache) Get(ns string, name string) (ObjectType, error) { 138 | obj, err := c.parent.Get(ns, name) 139 | switch { 140 | case err != nil: 141 | return nil, err 142 | case obj == nil: 143 | return nil, nil 144 | default: 145 | return adapter.adaptObject(obj) 146 | } 147 | } 148 | 149 | func (c *cache) List() ([]ObjectType, error) { 150 | objs, err := c.parent.List() 151 | if err != nil { 152 | return nil, err 153 | } 154 | return adapter.adaptList(objs) 155 | } 156 | 157 | type event struct { 158 | etype kcache.EventType 159 | resource ObjectType 160 | } 161 | 162 | func wrapEvent(evt kcache.Event) (Event, error) { 163 | obj, err := adapter.adaptObject(evt.Resource()) 164 | if err != nil { 165 | return nil, err 166 | } 167 | return event{evt.Type(), obj}, nil 168 | } 169 | 170 | func (e event) Type() kcache.EventType { 171 | return e.etype 172 | } 173 | 174 | func (e event) Resource() ObjectType { 175 | return e.resource 176 | } 177 | 178 | type subscription struct { 179 | parent kcache.Subscription 180 | cache CacheReader 181 | outch chan Event 182 | } 183 | 184 | func newSubscription(parent kcache.Subscription) *subscription { 185 | s := &subscription{ 186 | parent: parent, 187 | cache: newCache(parent.Cache()), 188 | outch: make(chan Event, kcache.EventBufsiz), 189 | } 190 | go s.run() 191 | return s 192 | } 193 | 194 | func (s *subscription) run() { 195 | defer close(s.outch) 196 | for pevt := range s.parent.Events() { 197 | evt, err := wrapEvent(pevt) 198 | if err != nil { 199 | continue 200 | } 201 | select { 202 | case s.outch <- evt: 203 | default: 204 | } 205 | } 206 | } 207 | 208 | func (s *subscription) Cache() CacheReader { 209 | return s.cache 210 | } 211 | 212 | func (s *subscription) Ready() <-chan struct{} { 213 | return s.parent.Ready() 214 | } 215 | 216 | func (s *subscription) Events() <-chan Event { 217 | return s.outch 218 | } 219 | 220 | func (s *subscription) Close() { 221 | s.parent.Close() 222 | } 223 | 224 | func (s *subscription) Done() <-chan struct{} { 225 | return s.parent.Done() 226 | } 227 | 228 | func NewController(ctx context.Context, log logutil.Log, cs kubernetes.Interface, ns string) (Controller, error) { 229 | client := NewClient(cs, ns) 230 | return BuildController(ctx, log, client) 231 | } 232 | 233 | func BuildController(ctx context.Context, log logutil.Log, client client.Client) (Controller, error) { 234 | parent, err := kcache.NewController(ctx, log, client) 235 | if err != nil { 236 | return nil, err 237 | } 238 | return newController(parent), nil 239 | } 240 | 241 | func newController(parent kcache.Controller) *controller { 242 | return &controller{parent, newCache(parent.Cache())} 243 | } 244 | 245 | type controller struct { 246 | parent kcache.Controller 247 | cache CacheReader 248 | } 249 | 250 | func (c *controller) Close() { 251 | c.parent.Close() 252 | } 253 | 254 | func (c *controller) Ready() <-chan struct{} { 255 | return c.parent.Ready() 256 | } 257 | 258 | func (c *controller) Done() <-chan struct{} { 259 | return c.parent.Done() 260 | } 261 | 262 | func (c *controller) Error() error { 263 | return c.parent.Error() 264 | } 265 | 266 | func (c *controller) Cache() CacheReader { 267 | return c.cache 268 | } 269 | 270 | func (c *controller) Subscribe() (Subscription, error) { 271 | parent, err := c.parent.Subscribe() 272 | if err != nil { 273 | return nil, err 274 | } 275 | return newSubscription(parent), nil 276 | } 277 | 278 | func (c *controller) SubscribeWithFilter(f filter.Filter) (FilterSubscription, error) { 279 | parent, err := c.parent.SubscribeWithFilter(f) 280 | if err != nil { 281 | return nil, err 282 | } 283 | return newFilterSubscription(parent), nil 284 | } 285 | 286 | func (c *controller) SubscribeForFilter() (FilterSubscription, error) { 287 | parent, err := c.parent.SubscribeForFilter() 288 | if err != nil { 289 | return nil, err 290 | } 291 | return newFilterSubscription(parent), nil 292 | } 293 | 294 | func (c *controller) Clone() (Controller, error) { 295 | parent, err := c.parent.Clone() 296 | if err != nil { 297 | return nil, err 298 | } 299 | return newController(parent), nil 300 | } 301 | 302 | func (c *controller) CloneWithFilter(f filter.Filter) (FilterController, error) { 303 | parent, err := c.parent.CloneWithFilter(f) 304 | if err != nil { 305 | return nil, err 306 | } 307 | return newFilterController(parent), nil 308 | } 309 | 310 | func (c *controller) CloneForFilter() (FilterController, error) { 311 | parent, err := c.parent.CloneForFilter() 312 | if err != nil { 313 | return nil, err 314 | } 315 | return newFilterController(parent), nil 316 | } 317 | 318 | type filterController struct { 319 | controller 320 | filterParent kcache.FilterController 321 | } 322 | 323 | func newFilterController(parent kcache.FilterController) FilterController { 324 | return &filterController{ 325 | controller: controller{parent, newCache(parent.Cache())}, 326 | filterParent: parent, 327 | } 328 | } 329 | 330 | func (c *filterController) Refilter(f filter.Filter) error { 331 | return c.filterParent.Refilter(f) 332 | } 333 | 334 | type filterSubscription struct { 335 | subscription 336 | filterParent kcache.FilterSubscription 337 | } 338 | 339 | func newFilterSubscription(parent kcache.FilterSubscription) FilterSubscription { 340 | return &filterSubscription{ 341 | subscription: *newSubscription(parent), 342 | filterParent: parent, 343 | } 344 | } 345 | 346 | func (s *filterSubscription) Refilter(f filter.Filter) error { 347 | return s.filterParent.Refilter(f) 348 | } 349 | 350 | func NewMonitor(publisher Publisher, handler Handler) (kcache.Monitor, error) { 351 | phandler := kcache.BuildHandler(). 352 | OnInitialize(func(objs []metav1.Object) { 353 | aobjs, _ := adapter.adaptList(objs) 354 | handler.OnInitialize(aobjs) 355 | }). 356 | OnCreate(func(obj metav1.Object) { 357 | aobj, _ := adapter.adaptObject(obj) 358 | handler.OnCreate(aobj) 359 | }). 360 | OnUpdate(func(obj metav1.Object) { 361 | aobj, _ := adapter.adaptObject(obj) 362 | handler.OnUpdate(aobj) 363 | }). 364 | OnDelete(func(obj metav1.Object) { 365 | aobj, _ := adapter.adaptObject(obj) 366 | handler.OnDelete(aobj) 367 | }).Create() 368 | 369 | switch obj := publisher.(type) { 370 | case *controller: 371 | return kcache.NewMonitor(obj.parent, phandler) 372 | case *filterController: 373 | return kcache.NewMonitor(obj.parent, phandler) 374 | default: 375 | panic(fmt.Sprintf("Invalid publisher type: %T is not a *controller", publisher)) 376 | } 377 | } 378 | 379 | func ToUnitary(log logutil.Log, delegate UnitaryHandler) Handler { 380 | return BuildHandler(). 381 | OnInitialize(func(objs []ObjectType) { 382 | if count := len(objs); count > 1 { 383 | log.Warnf("initialized with invalid count: %v", count) 384 | return 385 | } 386 | if count := len(objs); count == 0 { 387 | log.Debugf("initialized with empty result, ignoring") 388 | return 389 | } 390 | delegate.OnInitialize(objs[0]) 391 | }). 392 | OnCreate(func(obj ObjectType) { 393 | delegate.OnCreate(obj) 394 | }). 395 | OnUpdate(func(obj ObjectType) { 396 | delegate.OnUpdate(obj) 397 | }). 398 | OnDelete(func(obj ObjectType) { 399 | delegate.OnDelete(obj) 400 | }).Create() 401 | } 402 | 403 | func BuildHandler() HandlerBuilder { 404 | return &handlerBuilder{} 405 | } 406 | 407 | func BuildUnitaryHandler() UnitaryHandlerBuilder { 408 | return &unitaryHandlerBuilder{} 409 | } 410 | 411 | type baseHandler struct { 412 | onCreate func(ObjectType) 413 | onUpdate func(ObjectType) 414 | onDelete func(ObjectType) 415 | } 416 | 417 | type handler struct { 418 | baseHandler 419 | onInitialize func([]ObjectType) 420 | } 421 | type handlerBuilder handler 422 | 423 | type unitaryHandler struct { 424 | baseHandler 425 | onInitialize func(ObjectType) 426 | } 427 | type unitaryHandlerBuilder unitaryHandler 428 | 429 | func (hb *handlerBuilder) OnInitialize(fn func([]ObjectType)) HandlerBuilder { 430 | hb.onInitialize = fn 431 | return hb 432 | } 433 | 434 | func (hb *handlerBuilder) OnCreate(fn func(ObjectType)) HandlerBuilder { 435 | hb.onCreate = fn 436 | return hb 437 | } 438 | 439 | func (hb *handlerBuilder) OnUpdate(fn func(ObjectType)) HandlerBuilder { 440 | hb.onUpdate = fn 441 | return hb 442 | } 443 | 444 | func (hb *handlerBuilder) OnDelete(fn func(ObjectType)) HandlerBuilder { 445 | hb.onDelete = fn 446 | return hb 447 | } 448 | 449 | func (hb *handlerBuilder) Create() Handler { 450 | return handler(*hb) 451 | } 452 | 453 | func (h handler) OnInitialize(objs []ObjectType) { 454 | if h.onInitialize != nil { 455 | h.onInitialize(objs) 456 | } 457 | } 458 | 459 | func (hb *unitaryHandlerBuilder) OnInitialize(fn func(ObjectType)) UnitaryHandlerBuilder { 460 | hb.onInitialize = fn 461 | return hb 462 | } 463 | 464 | func (hb *unitaryHandlerBuilder) OnCreate(fn func(ObjectType)) UnitaryHandlerBuilder { 465 | hb.onCreate = fn 466 | return hb 467 | } 468 | 469 | func (hb *unitaryHandlerBuilder) OnUpdate(fn func(ObjectType)) UnitaryHandlerBuilder { 470 | hb.onUpdate = fn 471 | return hb 472 | } 473 | 474 | func (hb *unitaryHandlerBuilder) OnDelete(fn func(ObjectType)) UnitaryHandlerBuilder { 475 | hb.onDelete = fn 476 | return hb 477 | } 478 | 479 | func (hb *unitaryHandlerBuilder) Create() UnitaryHandler { 480 | return unitaryHandler(*hb) 481 | } 482 | 483 | func (h unitaryHandler) OnInitialize(obj ObjectType) { 484 | if h.onInitialize != nil { 485 | h.onInitialize(obj) 486 | } 487 | } 488 | 489 | func (h baseHandler) OnCreate(obj ObjectType) { 490 | if h.onCreate != nil { 491 | h.onCreate(obj) 492 | } 493 | } 494 | 495 | func (h baseHandler) OnUpdate(obj ObjectType) { 496 | if h.onUpdate != nil { 497 | h.onUpdate(obj) 498 | } 499 | } 500 | 501 | func (h baseHandler) OnDelete(obj ObjectType) { 502 | if h.onDelete != nil { 503 | h.onDelete(obj) 504 | } 505 | } 506 | --------------------------------------------------------------------------------