diff --git a/README.md b/README.md
index 27bb24622..533627258 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,9 @@ vSphere API from Kubernetes objects, e.g. a `Job`.
- `VSphereSource` to create VMware vSphere (vCenter) event sources
- `VSphereBinding` to inject VMware vSphere (vCenter) credentials
+- `HorizonSource` to create VMware Horizon event sources
-## Install Tanzu Sources for Knative
+## Install Tanzu Sources CRDs for Knative
### Install via Release (`latest`)
@@ -38,15 +39,15 @@ Install the CRD providing the control / dataplane for the various `Sources` and
```shell
# define environment variables accordingly, e.g. when using kind
-# export KIND_CLUSTER_NAME=horizon
-# export KO_DOCKER_REPO=kind.local
+export KIND_CLUSTER_NAME=vmware
+export KO_DOCKER_REPO=kind.local
ko apply -BRf config
```
## Examples
-To see examples of the Source and Binding in action, check out our
+To see examples of the `Sources` and `Bindings` in action, check out our
[samples](./samples/README.md) directory.
## Basic `VSphereSource` Example
@@ -587,8 +588,8 @@ kubectl get vspheresource
NAME SOURCE SINK READY REASON
example-vc-source https://my-vc.corp.local http://broker-ingress.knative-eventing.svc.cluster.local/default/example-broker True
-kubectl rollout restart deployment/example-vc-source-deployment
-deployment.apps/example-vc-source-deployment restarted
+kubectl rollout restart deployment/example-vc-source-adapter
+deployment.apps/example-vc-source-adapter restarted
```
⚠️ **Note:** To avoid losing events due to this (brief) downtime, consider
diff --git a/cmd/horizon-adapter/kodata/HEAD b/cmd/horizon-adapter/kodata/HEAD
new file mode 120000
index 000000000..8f63681d3
--- /dev/null
+++ b/cmd/horizon-adapter/kodata/HEAD
@@ -0,0 +1 @@
+../../../.git/HEAD
\ No newline at end of file
diff --git a/cmd/horizon-adapter/kodata/LICENSE b/cmd/horizon-adapter/kodata/LICENSE
new file mode 120000
index 000000000..5853aaea5
--- /dev/null
+++ b/cmd/horizon-adapter/kodata/LICENSE
@@ -0,0 +1 @@
+../../../LICENSE
\ No newline at end of file
diff --git a/cmd/horizon-adapter/kodata/refs b/cmd/horizon-adapter/kodata/refs
new file mode 120000
index 000000000..739d35bf9
--- /dev/null
+++ b/cmd/horizon-adapter/kodata/refs
@@ -0,0 +1 @@
+../../../.git/refs
\ No newline at end of file
diff --git a/cmd/horizon-adapter/main.go b/cmd/horizon-adapter/main.go
new file mode 100644
index 000000000..4d0b375e7
--- /dev/null
+++ b/cmd/horizon-adapter/main.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package main
+
+import (
+ "knative.dev/eventing/pkg/adapter/v2"
+
+ myadapter "github.com/vmware-tanzu/sources-for-knative/pkg/horizon"
+)
+
+const (
+ adapterName = "horizon-source-adapter"
+)
+
+func main() {
+ adapter.Main(adapterName, myadapter.NewEnv, myadapter.NewAdapter)
+}
diff --git a/cmd/horizon-controller/kodata/HEAD b/cmd/horizon-controller/kodata/HEAD
new file mode 120000
index 000000000..8f63681d3
--- /dev/null
+++ b/cmd/horizon-controller/kodata/HEAD
@@ -0,0 +1 @@
+../../../.git/HEAD
\ No newline at end of file
diff --git a/cmd/horizon-controller/kodata/LICENSE b/cmd/horizon-controller/kodata/LICENSE
new file mode 120000
index 000000000..5853aaea5
--- /dev/null
+++ b/cmd/horizon-controller/kodata/LICENSE
@@ -0,0 +1 @@
+../../../LICENSE
\ No newline at end of file
diff --git a/cmd/horizon-controller/kodata/refs b/cmd/horizon-controller/kodata/refs
new file mode 120000
index 000000000..739d35bf9
--- /dev/null
+++ b/cmd/horizon-controller/kodata/refs
@@ -0,0 +1 @@
+../../../.git/refs
\ No newline at end of file
diff --git a/cmd/horizon-controller/main.go b/cmd/horizon-controller/main.go
new file mode 100644
index 000000000..22060ccf4
--- /dev/null
+++ b/cmd/horizon-controller/main.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package main
+
+import (
+ // The set of controllers this controller process runs.
+ "github.com/vmware-tanzu/sources-for-knative/pkg/reconciler/horizonsource"
+
+ // This defines the shared main for injected controllers.
+ "knative.dev/pkg/injection/sharedmain"
+)
+
+const (
+ controllerName = "horizon-source-controller"
+)
+
+func main() {
+ sharedmain.Main(controllerName, horizonsource.NewController)
+}
diff --git a/cmd/horizon-webhook/kodata/HEAD b/cmd/horizon-webhook/kodata/HEAD
new file mode 120000
index 000000000..8f63681d3
--- /dev/null
+++ b/cmd/horizon-webhook/kodata/HEAD
@@ -0,0 +1 @@
+../../../.git/HEAD
\ No newline at end of file
diff --git a/cmd/horizon-webhook/kodata/LICENSE b/cmd/horizon-webhook/kodata/LICENSE
new file mode 120000
index 000000000..5853aaea5
--- /dev/null
+++ b/cmd/horizon-webhook/kodata/LICENSE
@@ -0,0 +1 @@
+../../../LICENSE
\ No newline at end of file
diff --git a/cmd/horizon-webhook/kodata/refs b/cmd/horizon-webhook/kodata/refs
new file mode 120000
index 000000000..739d35bf9
--- /dev/null
+++ b/cmd/horizon-webhook/kodata/refs
@@ -0,0 +1 @@
+../../../.git/refs
\ No newline at end of file
diff --git a/cmd/horizon-webhook/main.go b/cmd/horizon-webhook/main.go
new file mode 100644
index 000000000..fc1db5f48
--- /dev/null
+++ b/cmd/horizon-webhook/main.go
@@ -0,0 +1,122 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package main
+
+import (
+ "context"
+
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "knative.dev/pkg/configmap"
+ "knative.dev/pkg/controller"
+ "knative.dev/pkg/injection/sharedmain"
+ "knative.dev/pkg/logging"
+ "knative.dev/pkg/metrics"
+ "knative.dev/pkg/signals"
+ "knative.dev/pkg/webhook"
+ "knative.dev/pkg/webhook/certificates"
+ "knative.dev/pkg/webhook/configmaps"
+ "knative.dev/pkg/webhook/resourcesemantics"
+ "knative.dev/pkg/webhook/resourcesemantics/defaulting"
+ "knative.dev/pkg/webhook/resourcesemantics/validation"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+)
+
+var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{
+ // List the types to validate
+ v1alpha1.SchemeGroupVersion.WithKind("HorizonSource"): &v1alpha1.HorizonSource{},
+}
+
+var callbacks = map[schema.GroupVersionKind]validation.Callback{}
+
+const admissionWebhookName = "horizon-source-webhook"
+
+// NewDefaultingAdmissionController sets up mutating webhook.
+func NewDefaultingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+ return defaulting.NewAdmissionController(ctx,
+
+ // Name of the resource webhook.
+ "defaulting.webhook.horizon.sources.tanzu.vmware.com",
+
+ // The path on which to serve the webhook.
+ "/defaulting",
+
+ // The resource to default.
+ types,
+
+ // A function that infuses the context passed to Validate/SetDefaults with custom metadata.
+ func(ctx context.Context) context.Context {
+ // Here is where you would infuse the context with state
+ // (e.g. attach a store with configmap data)
+ return ctx
+ },
+
+ // Whether to disallow unknown fields.
+ true,
+ )
+}
+
+// NewValidationAdmissionController sets up validation webhook.
+func NewValidationAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+ return validation.NewAdmissionController(ctx,
+
+ // Name of the resource webhook.
+ "validation.webhook.horizon.sources.tanzu.vmware.com",
+
+ // The path on which to serve the webhook.
+ "/resource-validation",
+
+ // The resources to validate.
+ types,
+
+ // A function that infuses the context passed to Validate/SetDefaults with custom metadata.
+ func(ctx context.Context) context.Context {
+ // Here is where you would infuse the context with state
+ // (e.g. attach a store with configmap data)
+ return ctx
+ },
+
+ // Whether to disallow unknown fields.
+ true,
+
+ // Extra validating callbacks to be applied to resources.
+ callbacks,
+ )
+}
+
+// NewConfigValidationController sets up ConfigMap validation webhook.
+func NewConfigValidationController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+ return configmaps.NewAdmissionController(ctx,
+
+ // Name of the configmap webhook.
+ "config.webhook.horizon.sources.tanzu.vmware.com",
+
+ // The path on which to serve the webhook.
+ "/config-validation",
+
+ // The configmaps to validate.
+ configmap.Constructors{
+ logging.ConfigMapName(): logging.NewConfigFromConfigMap,
+ metrics.ConfigMapName(): metrics.NewObservabilityConfigFromConfigMap,
+ },
+ )
+}
+
+func main() {
+ // Set up a signal context with our webhook options
+ ctx := webhook.WithOptions(signals.NewContext(), webhook.Options{
+ ServiceName: admissionWebhookName,
+ Port: 8443,
+ SecretName: "webhook-certs",
+ })
+
+ sharedmain.WebhookMainWithContext(ctx, admissionWebhookName,
+ certificates.NewController,
+ NewDefaultingAdmissionController,
+ NewValidationAdmissionController,
+ NewConfigValidationController,
+ )
+}
diff --git a/cmd/vsphere-adapter/main.go b/cmd/vsphere-adapter/main.go
index dcef7522f..53ef3f677 100644
--- a/cmd/vsphere-adapter/main.go
+++ b/cmd/vsphere-adapter/main.go
@@ -20,9 +20,13 @@ import (
"github.com/vmware-tanzu/sources-for-knative/pkg/vsphere"
)
+const (
+ adapterName = "vsphere-source-adapter"
+)
+
func main() {
ctx := signals.NewContext()
kc := kubernetes.NewForConfigOrDie(injection.ParseAndGetRESTConfigOrDie())
ctx = context.WithValue(ctx, kubeclient.Key{}, kc)
- adapter.MainWithContext(ctx, "vspheresource", vsphere.NewEnvConfig, vsphere.NewAdapter)
+ adapter.MainWithContext(ctx, adapterName, vsphere.NewEnvConfig, vsphere.NewAdapter)
}
diff --git a/cmd/vsphere-controller/main.go b/cmd/vsphere-controller/main.go
index 5939928f0..a63270e66 100644
--- a/cmd/vsphere-controller/main.go
+++ b/cmd/vsphere-controller/main.go
@@ -36,7 +36,9 @@ var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{
v1alpha1.SchemeGroupVersion.WithKind("VSphereBinding"): &v1alpha1.VSphereBinding{},
}
-const admissionWebhookName = "vsphere-source-webhook"
+const (
+ admissionWebhookName = "vsphere-source-webhook"
+)
func NewDefaultingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
return defaulting.NewAdmissionController(ctx,
diff --git a/config/200-horizon-serviceaccount.yaml b/config/200-horizon-serviceaccount.yaml
new file mode 100644
index 000000000..9cc8d85a2
--- /dev/null
+++ b/config/200-horizon-serviceaccount.yaml
@@ -0,0 +1,20 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: horizon-source-controller
+ namespace: vmware-sources
+ labels:
+ sources.tanzu.vmware.com/release: devel
+
+---
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: horizon-source-webhook
+ namespace: vmware-sources
+ labels:
+ sources.tanzu.vmware.com/release: devel
diff --git a/config/vsphere/200-clusterrole-cm.yaml b/config/200-vsphere-clusterrole-cm.yaml
similarity index 100%
rename from config/vsphere/200-clusterrole-cm.yaml
rename to config/200-vsphere-clusterrole-cm.yaml
diff --git a/config/vsphere/200-clusterrole.yaml b/config/200-vsphere-clusterrole.yaml
similarity index 98%
rename from config/vsphere/200-clusterrole.yaml
rename to config/200-vsphere-clusterrole.yaml
index a2efdf688..6fac6b9d4 100644
--- a/config/vsphere/200-clusterrole.yaml
+++ b/config/200-vsphere-clusterrole.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
kind: ClusterRole
diff --git a/config/vsphere/200-podspecable-binding-clusterrole.yaml b/config/200-vsphere-podspecable-binding-clusterrole.yaml
similarity index 97%
rename from config/vsphere/200-podspecable-binding-clusterrole.yaml
rename to config/200-vsphere-podspecable-binding-clusterrole.yaml
index 5b034d365..490e03c71 100644
--- a/config/vsphere/200-podspecable-binding-clusterrole.yaml
+++ b/config/200-vsphere-podspecable-binding-clusterrole.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
# Use this aggregated ClusterRole when you need readonly access to "podspecables
diff --git a/config/vsphere/200-serviceaccount.yaml b/config/200-vsphere-serviceaccount.yaml
similarity index 86%
rename from config/vsphere/200-serviceaccount.yaml
rename to config/200-vsphere-serviceaccount.yaml
index e567a0005..c6eeb2965 100644
--- a/config/vsphere/200-serviceaccount.yaml
+++ b/config/200-vsphere-serviceaccount.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: v1
diff --git a/config/201-horizon-clusterrole.yaml b/config/201-horizon-clusterrole.yaml
new file mode 100644
index 000000000..864446bd4
--- /dev/null
+++ b/config/201-horizon-clusterrole.yaml
@@ -0,0 +1,96 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: horizon-source-controller
+ labels:
+ sources.tanzu.vmware.com/release: devel
+rules:
+- apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs: &everything
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+
+- apiGroups:
+ - rbac.authorization.k8s.io
+ resources:
+ - clusterroles
+ verbs:
+ - list
+
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs: *everything
+
+- apiGroups:
+ - sources.tanzu.vmware.com
+ resources:
+ - horizonsources
+ verbs: *everything
+
+- apiGroups:
+ - sources.tanzu.vmware.com
+ resources:
+ - horizonsources/status
+ - horizonsources/finalizers
+ verbs:
+ - get
+ - update
+ - patch
+
+- apiGroups:
+ - ""
+ resources:
+ - configmaps
+ - secrets
+ verbs:
+ - get
+ - list
+ - watch
+
+# manage adapter SAs
+- apiGroups:
+ - ""
+ resources:
+ - serviceaccounts
+ verbs: *everything
+
+
+# For Leader Election
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs: *everything
+
+---
+# The role is needed for the aggregated role source-observer in knative-eventing to provide readonly access to "Sources".
+# See https://github.com/knative/eventing/blob/master/config/200-source-observer-clusterrole.yaml.
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: horizon-source-observer
+ labels:
+ sources.tanzu.vmware.com/release: devel
+ duck.knative.dev/source: "true"
+rules:
+ - apiGroups:
+ - "sources.eventing.knative.dev"
+ resources:
+ - "horizonsources"
+ verbs:
+ - get
+ - list
+ - watch
diff --git a/config/vsphere/201-clusterrolebinding.yaml b/config/201-vsphere-clusterrolebinding.yaml
similarity index 97%
rename from config/vsphere/201-clusterrolebinding.yaml
rename to config/201-vsphere-clusterrolebinding.yaml
index 0a8a7215c..206636d4d 100644
--- a/config/vsphere/201-clusterrolebinding.yaml
+++ b/config/201-vsphere-clusterrolebinding.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: rbac.authorization.k8s.io/v1
diff --git a/config/202-horizon-clusterrolebinding.yaml b/config/202-horizon-clusterrolebinding.yaml
new file mode 100644
index 000000000..7697c169c
--- /dev/null
+++ b/config/202-horizon-clusterrolebinding.yaml
@@ -0,0 +1,53 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: horizon-source-controller-rolebinding
+ labels:
+ sources.tanzu.vmware.com/release: devel
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: horizon-source-controller
+subjects:
+- kind: ServiceAccount
+ name: horizon-source-controller
+ namespace: vmware-sources
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: horizon-source-webhook-rolebinding
+ labels:
+ sources.tanzu.vmware.com/release: devel
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: horizon-source-webhook
+subjects:
+ - kind: ServiceAccount
+ name: horizon-source-webhook
+ namespace: vmware-sources
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: horizon-source-controller-addressable-resolver
+ labels:
+ sources.tanzu.vmware.com/release: devel
+subjects:
+- kind: ServiceAccount
+ name: horizon-source-controller
+ namespace: vmware-sources
+# An aggregated ClusterRole for all Addressable CRDs.
+# Ref: https://knative.dev/eventing/blob/master/config/200-addressable-resolvers-clusterrole.yaml
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: addressable-resolver
diff --git a/config/203-horizon-webhook-clusterrole.yaml b/config/203-horizon-webhook-clusterrole.yaml
new file mode 100644
index 000000000..be46a545a
--- /dev/null
+++ b/config/203-horizon-webhook-clusterrole.yaml
@@ -0,0 +1,111 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: horizon-source-webhook
+ labels:
+ sources.tanzu.vmware.com/release: devel
+rules:
+ # Sources admin
+ - apiGroups:
+ - sources.knative.dev
+ resources:
+ - horizonsources
+ verbs: &everything
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+
+ # Sources finalizer
+ - apiGroups:
+ - sources.knative.dev
+ resources:
+ - horizonsources/finalizers
+ verbs: *everything
+
+ # Source statuses update
+ - apiGroups:
+ - sources.knative.dev
+ resources:
+ - horizonsources/status
+ verbs:
+ - get
+ - update
+ - patch
+
+ # Deployments admin
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs: *everything
+
+ # Secrets read
+ - apiGroups:
+ - ""
+ resources:
+ - secrets
+ - services
+ verbs:
+ - get
+ - list
+ - watch
+
+ # Namespace labelling for webhook
+ - apiGroups:
+ - ""
+ resources:
+ - namespaces
+ verbs:
+ - get
+ - list
+ - watch
+ - patch
+
+ # Events admin
+ - apiGroups:
+ - ""
+ resources:
+ - events
+ - configmaps
+ verbs: *everything
+
+ # EventTypes admin
+ - apiGroups:
+ - eventing.knative.dev
+ resources:
+ - eventtypes
+ verbs: *everything
+
+ # For manipulating certs into secrets.
+ - apiGroups:
+ - ""
+ resources:
+ - "secrets"
+ verbs:
+ - "get"
+ - "create"
+ - "update"
+ - "list"
+ - "watch"
+
+ # For actually registering our webhook.
+ - apiGroups:
+ - "admissionregistration.k8s.io"
+ resources:
+ - "mutatingwebhookconfigurations"
+ - "validatingwebhookconfigurations"
+ verbs: *everything
+
+ # For Leader Election
+ - apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs: *everything
diff --git a/config/300-horizonsource.yaml b/config/300-horizonsource.yaml
new file mode 100644
index 000000000..1e1f3fd3c
--- /dev/null
+++ b/config/300-horizonsource.yaml
@@ -0,0 +1,56 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: horizonsources.sources.tanzu.vmware.com
+ labels:
+ sources.tanzu.vmware.com/release: devel
+ knative.dev/crd-install: "true"
+ duck.knative.dev/source: "true"
+ eventing.knative.dev/source: "true"
+ annotations:
+ registry.knative.dev/eventTypes: |
+ [
+ { "type": "com.vmware.tanzu.sources" }
+ ]
+spec:
+ group: sources.tanzu.vmware.com
+ names:
+ kind: HorizonSource
+ plural: horizonsources
+ singular: horizonsource
+ categories:
+ - all
+ - knative
+ - eventing
+ - horizon
+ - sources
+ shortNames:
+ - hs
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ served: true
+ storage: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ # TODO: use controller-gen from controller-tools to fill this in?
+ subresources:
+ status: {}
+ additionalPrinterColumns:
+ - name: Source
+ type: string
+ jsonPath: .spec.address
+ - name: Sink
+ type: string
+ jsonPath: .status.sinkUri
+ - name: Ready
+ type: string
+ jsonPath: ".status.conditions[?(@.type=='Ready')].status"
+ - name: Reason
+ type: string
+ jsonPath: ".status.conditions[?(@.type=='Ready')].reason"
diff --git a/config/vsphere/300-vspherebinding.yaml b/config/300-vsphere-vspherebinding.yaml
similarity index 97%
rename from config/vsphere/300-vspherebinding.yaml
rename to config/300-vsphere-vspherebinding.yaml
index 927cddbf7..1615a2adb 100644
--- a/config/vsphere/300-vspherebinding.yaml
+++ b/config/300-vsphere-vspherebinding.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: apiextensions.k8s.io/v1
diff --git a/config/vsphere/300-vspheresource.yaml b/config/300-vspheresource.yaml
similarity index 89%
rename from config/vsphere/300-vspheresource.yaml
rename to config/300-vspheresource.yaml
index c7e3a1a43..b7e7aeff1 100644
--- a/config/vsphere/300-vspheresource.yaml
+++ b/config/300-vspheresource.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: apiextensions.k8s.io/v1
@@ -10,6 +10,11 @@ metadata:
knative.dev/crd-install: "true"
duck.knative.dev/source: "true"
eventing.knative.dev/source: "true"
+ annotations:
+ registry.knative.dev/eventTypes: |
+ [
+ { "type": "com.vmware.tanzu.sources" }
+ ]
spec:
group: sources.tanzu.vmware.com
names:
diff --git a/config/400-horizon-controller-service.yaml b/config/400-horizon-controller-service.yaml
new file mode 100644
index 000000000..e7fb7f27b
--- /dev/null
+++ b/config/400-horizon-controller-service.yaml
@@ -0,0 +1,16 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ sources.tanzu.vmware.com/release: devel
+ control-plane: horizon-source-controller-manager
+ name: horizon-source-controller-manager
+ namespace: vmware-sources
+spec:
+ selector:
+ control-plane: horizon-source-controller-manager
+ ports:
+ - port: 443
diff --git a/config/400-horizon-webhook-service.yaml b/config/400-horizon-webhook-service.yaml
new file mode 100644
index 000000000..7b94f9d09
--- /dev/null
+++ b/config/400-horizon-webhook-service.yaml
@@ -0,0 +1,18 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ sources.tanzu.vmware.com/release: devel
+ role: horizon-source-webhook
+ name: horizon-source-webhook
+ namespace: vmware-sources
+spec:
+ ports:
+ - name: https-webhook
+ port: 443
+ targetPort: 8443
+ selector:
+ role: horizon-source-webhook
diff --git a/config/vsphere/400-webhook-service.yaml b/config/400-vsphere-webhook-service.yaml
similarity index 91%
rename from config/vsphere/400-webhook-service.yaml
rename to config/400-vsphere-webhook-service.yaml
index df8f068c4..65486dbdf 100644
--- a/config/vsphere/400-webhook-service.yaml
+++ b/config/400-vsphere-webhook-service.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: v1
diff --git a/config/500-horizon-controller.yaml b/config/500-horizon-controller.yaml
new file mode 100644
index 000000000..04d193f8a
--- /dev/null
+++ b/config/500-horizon-controller.yaml
@@ -0,0 +1,63 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: horizon-source-controller
+ namespace: vmware-sources
+ labels:
+ sources.tanzu.vmware.com/release: devel
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: horizon-source-controller
+ template:
+ metadata:
+ labels:
+ app: horizon-source-controller
+ sources.tanzu.vmware.com/release: devel
+ control-plane: horizon-source-controller-manager
+ spec:
+ # To avoid node becoming SPOF, spread our replicas to different nodes.
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - podAffinityTerm:
+ labelSelector:
+ matchLabels:
+ app: horizon-source-controller
+ topologyKey: kubernetes.io/hostname
+ weight: 100
+ serviceAccountName: horizon-source-controller
+ containers:
+ - name: horizon-source-controller
+ terminationMessagePolicy: FallbackToLogsOnError
+ image: ko://github.com/vmware-tanzu/sources-for-knative/cmd/horizon-controller
+ resources:
+ limits:
+ cpu: 200m
+ memory: 200Mi
+ env:
+ - name: SYSTEM_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: CONFIG_LOGGING_NAME
+ value: config-logging
+ - name: CONFIG_OBSERVABILITY_NAME
+ value: config-observability
+ - name: METRICS_DOMAIN
+ value: knative.dev/sources
+ - name: HORIZON_SOURCE_RA_IMAGE
+ value: ko://github.com/vmware-tanzu/sources-for-knative/cmd/horizon-adapter
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ securityContext:
+ allowPrivilegeEscalation: false
+ ports:
+ - name: metrics
+ containerPort: 9090
diff --git a/config/500-horizon-webhook-configuration.yaml b/config/500-horizon-webhook-configuration.yaml
new file mode 100644
index 000000000..0f036e50c
--- /dev/null
+++ b/config/500-horizon-webhook-configuration.yaml
@@ -0,0 +1,63 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ name: defaulting.webhook.horizon.sources.tanzu.vmware.com
+ labels:
+ sources.tanzu.vmware.com/release: devel
+webhooks:
+ - admissionReviewVersions: ["v1", "v1beta1"]
+ clientConfig:
+ service:
+ name: horizon-source-webhook
+ namespace: vmware-sources
+ sideEffects: None
+ failurePolicy: Fail
+ name: defaulting.webhook.horizon.sources.tanzu.vmware.com
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: validation.webhook.horizon.sources.tanzu.vmware.com
+ labels:
+ sources.tanzu.vmware.com/release: devel
+webhooks:
+ - admissionReviewVersions: ["v1", "v1beta1"]
+ clientConfig:
+ service:
+ name: horizon-source-webhook
+ namespace: vmware-sources
+ sideEffects: None
+ failurePolicy: Fail
+ name: validation.webhook.horizon.sources.tanzu.vmware.com
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: config.webhook.horizon.sources.tanzu.vmware.com
+ labels:
+ sources.tanzu.vmware.com/release: devel
+webhooks:
+ - admissionReviewVersions: ["v1", "v1beta1"]
+ clientConfig:
+ service:
+ name: horizon-source-webhook
+ namespace: vmware-sources
+ sideEffects: None
+ failurePolicy: Fail
+ name: config.webhook.horizon.sources.tanzu.vmware.com
+ namespaceSelector:
+ matchExpressions:
+ - key: sources.knative.dev/release
+ operator: Exists
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: webhook-certs
+ namespace: vmware-sources
+ labels:
+ sources.tanzu.vmware.com/release: devel
+# The data is populated at install time.
diff --git a/config/500-horizon-webhook.yaml b/config/500-horizon-webhook.yaml
new file mode 100644
index 000000000..71208b412
--- /dev/null
+++ b/config/500-horizon-webhook.yaml
@@ -0,0 +1,68 @@
+# Copyright 2022 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: horizon-source-webhook
+ namespace: vmware-sources
+ labels:
+ sources.tanzu.vmware.com/release: devel
+spec:
+ replicas: 1
+ selector:
+ matchLabels: &labels
+ app: horizon-source-webhook
+ role: horizon-source-webhook
+ template:
+ metadata:
+ labels: *labels
+ spec:
+ # To avoid node becoming SPOF, spread our replicas to different nodes.
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - podAffinityTerm:
+ labelSelector:
+ matchLabels:
+ app: horizon-source-webhook
+ topologyKey: kubernetes.io/hostname
+ weight: 100
+
+ serviceAccountName: horizon-source-webhook
+ containers:
+ - name: horizon-source-webhook
+ terminationMessagePolicy: FallbackToLogsOnError
+ image: ko://github.com/vmware-tanzu/sources-for-knative/cmd/horizon-webhook
+ resources:
+ limits:
+ cpu: 200m
+ memory: 200Mi
+ env:
+ - name: SYSTEM_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: CONFIG_LOGGING_NAME
+ value: config-logging
+ - name: METRICS_DOMAIN
+ value: knative.dev/eventing
+ - name: WEBHOOK_NAME
+ value: horizon-source-webhook
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ ports:
+ - containerPort: 9090
+ name: metrics
+ readinessProbe: &probe
+ periodSeconds: 1
+ httpGet:
+ scheme: HTTPS
+ port: 8443
+ httpHeaders:
+ - name: k-kubelet-probe
+ value: "webhook"
+ livenessProbe: *probe
+
diff --git a/config/vsphere/500-webhook-configuration.yaml b/config/500-vsphere-webhook-configuration.yaml
similarity index 98%
rename from config/vsphere/500-webhook-configuration.yaml
rename to config/500-vsphere-webhook-configuration.yaml
index 9b47f1dc0..8b0cfb117 100644
--- a/config/vsphere/500-webhook-configuration.yaml
+++ b/config/500-vsphere-webhook-configuration.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: admissionregistration.k8s.io/v1
diff --git a/config/vsphere/webhook.yaml b/config/500-vsphere-webhook.yaml
similarity index 91%
rename from config/vsphere/webhook.yaml
rename to config/500-vsphere-webhook.yaml
index 1e6759fa6..c8fa0ec71 100644
--- a/config/vsphere/webhook.yaml
+++ b/config/500-vsphere-webhook.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 VMware, Inc.
+# Copyright 2022 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
apiVersion: apps/v1
@@ -31,7 +31,6 @@ spec:
app: vsphere-source-webhook
topologyKey: kubernetes.io/hostname
weight: 100
-
serviceAccountName: vsphere-controller
containers:
- name: vsphere-source-webhook
@@ -39,11 +38,6 @@ spec:
# and substituted here.
image: ko://github.com/vmware-tanzu/sources-for-knative/cmd/vsphere-controller
resources:
- # Request 2x what we saw running e2e
- requests:
- cpu: 20m
- memory: 20Mi
- # Limit to 10x the request (20x the observed peak during e2e)
limits:
cpu: 200m
memory: 200Mi
@@ -60,7 +54,6 @@ spec:
value: tanzu.vmware.com/sources
- name: WEBHOOK_NAME
value: vsphere-source-webhook
-
readinessProbe: &probe
# Increasing the failure threshold and adding an initial delay
# avoids the situation where failing probes cause the vsphere-source-webhook to restart before it can
diff --git a/config/config-logging.yaml b/config/config-logging.yaml
index 5c839eef1..6be24d98f 100644
--- a/config/config-logging.yaml
+++ b/config/config-logging.yaml
@@ -35,6 +35,6 @@ data:
# Log level overrides
# For all components changes are be picked up immediately.
- loglevel.controller: "info"
- loglevel.webhook: "info"
loglevel.vsphere-source-webhook: "info"
+ loglevel.horizon-source-webhook: "info"
+ loglevel.horizon-source-controller: "info"
diff --git a/go.mod b/go.mod
index ec0efe314..a656a55ae 100644
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,11 @@ require (
)
require (
+ github.com/benbjohnson/clock v1.1.0
+ github.com/go-resty/resty/v2 v2.7.0
github.com/hashicorp/hcl v1.0.0
+ github.com/pkg/errors v0.9.1
+ github.com/stretchr/testify v1.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gotest.tools/v3 v3.1.0
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
@@ -48,7 +52,6 @@ require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20211221011931-643d94fcab96 // indirect
- github.com/benbjohnson/clock v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
@@ -102,7 +105,6 @@ require (
github.com/openzipkin/zipkin-go v0.3.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
- github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
@@ -117,7 +119,6 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
- github.com/stretchr/testify v1.7.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
go.opencensus.io v0.23.0 // indirect
diff --git a/go.sum b/go.sum
index a28cb02aa..f923a40be 100644
--- a/go.sum
+++ b/go.sum
@@ -245,6 +245,7 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc=
github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
+github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -265,8 +266,11 @@ github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d8
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21/go.mod h1:Zlre/PVxuSI9y6/UV4NwGixQ48RHQDSPiUkofr6rbMU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
@@ -574,6 +578,8 @@ github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
+github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
@@ -699,6 +705,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-containerregistry v0.8.1-0.20220414133640-f1b729141d33/go.mod h1:eTLvLZaEe2FoQsb25t7BLxQQryyrwHTzFfwxN87mhAw=
github.com/google/go-containerregistry v0.8.1-0.20220414143355-892d7a808387 h1:GWICy4b02s8EA1M9H5krRQ48BKpIHO5LtBBm2BQLhx0=
+github.com/google/go-containerregistry v0.8.1-0.20220414143355-892d7a808387 h1:GWICy4b02s8EA1M9H5krRQ48BKpIHO5LtBBm2BQLhx0=
+github.com/google/go-containerregistry v0.8.1-0.20220414143355-892d7a808387/go.mod h1:eTLvLZaEe2FoQsb25t7BLxQQryyrwHTzFfwxN87mhAw=
github.com/google/go-containerregistry v0.8.1-0.20220414143355-892d7a808387/go.mod h1:eTLvLZaEe2FoQsb25t7BLxQQryyrwHTzFfwxN87mhAw=
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20220414154538-570ba6c88a50/go.mod h1:m7mMYMlUraMy65yWp4AXkMgousS5LFPYcvI19yjz6W0=
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20220414143355-892d7a808387/go.mod h1:QOryQrrP9Uq/1w9F7WOWWhK2/gHXg7F0i3J/hPG6yQA=
@@ -1256,12 +1264,14 @@ github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiB
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
+github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc=
@@ -1506,6 +1516,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
@@ -1650,6 +1661,7 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -2265,6 +2277,7 @@ k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGw
k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI=
k8s.io/component-base v0.23.4/go.mod h1:8o3Gg8i2vnUXGPOwciiYlkSaZT+p+7gA9Scoz8y4W4E=
+k8s.io/component-base v0.23.4/go.mod h1:8o3Gg8i2vnUXGPOwciiYlkSaZT+p+7gA9Scoz8y4W4E=
k8s.io/component-base v0.23.8/go.mod h1:rCj6EeaYLsNneVoFuSPL/AlEWmomc39j9M9i4NpR8r0=
k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
@@ -2340,6 +2353,7 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyz
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y=
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_defaults.go b/pkg/apis/sources/v1alpha1/horizonsource_defaults.go
new file mode 100644
index 000000000..0a31d6bce
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_defaults.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ "context"
+
+ "knative.dev/pkg/apis"
+)
+
+// SetDefaults mutates HorizonSource.
+func (hs *HorizonSource) SetDefaults(ctx context.Context) {
+ if hs != nil && hs.Spec.ServiceAccountName == "" {
+ hs.Spec.ServiceAccountName = "default"
+ }
+
+ // call SetDefaults against duckv1.Destination with a context of ObjectMeta of HorizonSource.
+ withNS := apis.WithinParent(ctx, hs.ObjectMeta)
+ hs.Spec.Sink.SetDefaults(withNS)
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_defaults_test.go b/pkg/apis/sources/v1alpha1/horizonsource_defaults_test.go
new file mode 100644
index 000000000..c10ef4e80
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_defaults_test.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ "context"
+ "testing"
+
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ duckv1 "knative.dev/pkg/apis/duck/v1"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestHorizonSourceDefaults(t *testing.T) {
+ testCases := map[string]struct {
+ initial HorizonSource
+ expected HorizonSource
+ }{
+ "nil spec": {
+ initial: HorizonSource{},
+ expected: HorizonSource{
+ Spec: HorizonSourceSpec{
+ ServiceAccountName: "default",
+ },
+ },
+ },
+ "no namespace in sink reference": {
+ initial: HorizonSource{
+ ObjectMeta: v1.ObjectMeta{
+ Namespace: "parent",
+ },
+ Spec: HorizonSourceSpec{
+ ServiceAccountName: "default",
+ SourceSpec: duckv1.SourceSpec{
+ Sink: duckv1.Destination{
+ Ref: &duckv1.KReference{},
+ },
+ },
+ },
+ },
+ expected: HorizonSource{
+ ObjectMeta: v1.ObjectMeta{
+ Namespace: "parent",
+ },
+ Spec: HorizonSourceSpec{
+ ServiceAccountName: "default",
+ SourceSpec: duckv1.SourceSpec{
+ Sink: duckv1.Destination{
+ Ref: &duckv1.KReference{
+ Namespace: "parent",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for n, tc := range testCases {
+ t.Run(n, func(t *testing.T) {
+ tc.initial.SetDefaults(context.TODO())
+ if diff := cmp.Diff(tc.expected, tc.initial); diff != "" {
+ t.Fatalf("Unexpected defaults (-want, +got): %s", diff)
+ }
+ })
+ }
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_lifecycle.go b/pkg/apis/sources/v1alpha1/horizonsource_lifecycle.go
new file mode 100644
index 000000000..cb0cdd60f
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_lifecycle.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+//nolint:stylecheck
+package v1alpha1
+
+import (
+ appsv1 "k8s.io/api/apps/v1"
+ "knative.dev/eventing/pkg/apis/duck"
+ "knative.dev/pkg/apis"
+)
+
+const (
+ // HorizonSourceConditionReady has status True when the HorizonSource is ready to send events.
+ HorizonSourceConditionReady = apis.ConditionReady
+
+ // HorizonSourceConditionSinkProvided has status True when the HorizonSource has been configured with a sink target.
+ HorizonSourceConditionSinkProvided apis.ConditionType = "SinkProvided"
+
+ // HorizonSourceConditionDeployed has status True when the HorizonSource has had it's adapter deployment created.
+ HorizonSourceConditionDeployed apis.ConditionType = "Deployed"
+)
+
+var HorizonSourceCondSet = apis.NewLivingConditionSet(
+ HorizonSourceConditionSinkProvided,
+ HorizonSourceConditionDeployed,
+)
+
+// GetCondition returns the condition currently associated with the given type, or nil.
+func (hss *HorizonSourceStatus) GetCondition(t apis.ConditionType) *apis.Condition {
+ return HorizonSourceCondSet.Manage(hss).GetCondition(t)
+}
+
+// InitializeConditions sets relevant unset conditions to Unknown state.
+func (hss *HorizonSourceStatus) InitializeConditions() {
+ HorizonSourceCondSet.Manage(hss).InitializeConditions()
+}
+
+// GetConditionSet returns HorizonSource ConditionSet.
+func (hs *HorizonSource) GetConditionSet() apis.ConditionSet {
+ return HorizonSourceCondSet
+}
+
+// MarkSink sets the condition that the source has a sink configured.
+func (hss *HorizonSourceStatus) MarkSink(uri *apis.URL) {
+ hss.SinkURI = uri
+ if len(uri.String()) > 0 {
+ HorizonSourceCondSet.Manage(hss).MarkTrue(HorizonSourceConditionSinkProvided)
+ } else {
+ HorizonSourceCondSet.Manage(hss).MarkUnknown(HorizonSourceConditionSinkProvided, "SinkEmpty", "Sink has resolved to empty.")
+ }
+}
+
+// MarkNoSink sets the condition that the source does not have a sink configured.
+func (hss *HorizonSourceStatus) MarkNoSink(reason, messageFormat string, messageA ...interface{}) {
+ HorizonSourceCondSet.Manage(hss).MarkFalse(HorizonSourceConditionSinkProvided, reason, messageFormat, messageA...)
+}
+
+// PropagateDeploymentAvailability uses the availability of the provided Deployment to determine if
+// HorizonSourceConditionDeployed should be marked as true or false.
+func (hss *HorizonSourceStatus) PropagateDeploymentAvailability(d *appsv1.Deployment) {
+ if duck.DeploymentIsAvailable(&d.Status, false) {
+ HorizonSourceCondSet.Manage(hss).MarkTrue(HorizonSourceConditionDeployed)
+ } else {
+ // I don't know how to propagate the status well, so just give the name of the Deployment
+ // for now.
+ HorizonSourceCondSet.Manage(hss).MarkFalse(HorizonSourceConditionDeployed, "DeploymentUnavailable", "The Deployment '%s' is unavailable.", d.Name)
+ }
+}
+
+// IsReady returns true if the resource is ready overall.
+func (hss *HorizonSourceStatus) IsReady() bool {
+ return HorizonSourceCondSet.Manage(hss).IsHappy()
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_lifecycle_test.go b/pkg/apis/sources/v1alpha1/horizonsource_lifecycle_test.go
new file mode 100644
index 000000000..06d7fd8de
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_lifecycle_test.go
@@ -0,0 +1,132 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ appsv1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ "knative.dev/pkg/apis"
+ "knative.dev/pkg/apis/duck"
+ duckv1 "knative.dev/pkg/apis/duck/v1"
+)
+
+var availableDeployment = &appsv1.Deployment{
+ Status: appsv1.DeploymentStatus{
+ Conditions: []appsv1.DeploymentCondition{
+ {
+ Type: appsv1.DeploymentAvailable,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ },
+}
+
+var _ = duck.VerifyType(&HorizonSource{}, &duckv1.Conditions{})
+
+func TestRabbitmqSourceStatusGetCondition(t *testing.T) {
+ tests := []struct {
+ name string
+ s *HorizonSourceStatus
+ condQuery apis.ConditionType
+ want *apis.Condition
+ }{
+ {
+ name: "uninitialized",
+ s: &HorizonSourceStatus{},
+ condQuery: HorizonSourceConditionReady,
+ want: nil,
+ },
+ {
+ name: "initialized",
+ s: func() *HorizonSourceStatus {
+ s := &HorizonSourceStatus{}
+ s.InitializeConditions()
+ return s
+ }(),
+ condQuery: HorizonSourceConditionReady,
+ want: &apis.Condition{
+ Type: HorizonSourceConditionReady,
+ Status: corev1.ConditionUnknown,
+ },
+ },
+ {
+ name: "mark deployed",
+ s: func() *HorizonSourceStatus {
+ s := &HorizonSourceStatus{}
+ s.InitializeConditions()
+ s.PropagateDeploymentAvailability(availableDeployment)
+ return s
+ }(),
+ condQuery: HorizonSourceConditionReady,
+ want: &apis.Condition{
+ Type: HorizonSourceConditionReady,
+ Status: corev1.ConditionUnknown,
+ },
+ },
+ {
+ name: "mark sink",
+ s: func() *HorizonSourceStatus {
+ s := &HorizonSourceStatus{}
+ s.InitializeConditions()
+ s.MarkSink(apis.HTTP("uri://example"))
+ return s
+ }(),
+ condQuery: HorizonSourceConditionReady,
+ want: &apis.Condition{
+ Type: HorizonSourceConditionReady,
+ Status: corev1.ConditionUnknown,
+ },
+ },
+ {
+ name: "mark sink and adapter deployed",
+ s: func() *HorizonSourceStatus {
+ s := &HorizonSourceStatus{}
+ s.InitializeConditions()
+ s.MarkSink(apis.HTTP("uri://example"))
+ s.PropagateDeploymentAvailability(availableDeployment)
+ return s
+ }(),
+ condQuery: HorizonSourceConditionReady,
+ want: &apis.Condition{
+ Type: HorizonSourceConditionReady,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ {
+ name: "mark sink and adapter deployed then no sink",
+ s: func() *HorizonSourceStatus {
+ s := &HorizonSourceStatus{}
+ s.InitializeConditions()
+ s.MarkSink(apis.HTTP("uri://example"))
+ s.PropagateDeploymentAvailability(availableDeployment)
+ s.MarkNoSink("Testing", "hi%s", "")
+ return s
+ }(),
+ condQuery: HorizonSourceConditionReady,
+ want: &apis.Condition{
+ Type: HorizonSourceConditionReady,
+ Status: corev1.ConditionFalse,
+ Reason: "Testing",
+ Message: "hi",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got := test.s.GetCondition(test.condQuery)
+ ignoreTime := cmpopts.IgnoreFields(apis.Condition{},
+ "LastTransitionTime", "Severity")
+ if diff := cmp.Diff(test.want, got, ignoreTime); diff != "" {
+ t.Errorf("unexpected condition (-want, +got) = %v", diff)
+ }
+ })
+ }
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_types.go b/pkg/apis/sources/v1alpha1/horizonsource_types.go
new file mode 100644
index 000000000..14054b94f
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_types.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "knative.dev/pkg/apis"
+ "knative.dev/pkg/apis/duck"
+ duckv1 "knative.dev/pkg/apis/duck/v1"
+ "knative.dev/pkg/kmeta"
+ "knative.dev/pkg/webhook/resourcesemantics"
+)
+
+// +genclient
+// +genreconciler
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type HorizonSource struct {
+ metav1.TypeMeta `json:",inline"`
+ // +optional
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // Spec holds the desired state of the HorizonSource (from the client).
+ Spec HorizonSourceSpec `json:"spec"`
+
+ // Status communicates the observed state of the HorizonSource (from the controller).
+ // +optional
+ Status HorizonSourceStatus `json:"status,omitempty"`
+}
+
+// GetGroupVersionKind returns the GroupVersionKind.
+func (*HorizonSource) GetGroupVersionKind() schema.GroupVersionKind {
+ return SchemeGroupVersion.WithKind("HorizonSource")
+}
+
+var (
+ // Check that HorizonSource can be validated and defaulted.
+ _ apis.Validatable = (*HorizonSource)(nil)
+ _ apis.Defaultable = (*HorizonSource)(nil)
+ // Check that we can create OwnerReferences to a HorizonSource.
+ _ kmeta.OwnerRefable = (*HorizonSource)(nil)
+ // Check that HorizonSource is a runtime.Object.
+ _ runtime.Object = (*HorizonSource)(nil)
+ // Check that HorizonSource satisfies resourcesemantics.GenericCRD.
+ _ resourcesemantics.GenericCRD = (*HorizonSource)(nil)
+ // Check that HorizonSource implements the Conditions duck type.
+ _ = duck.VerifyType(&HorizonSource{}, &duckv1.Conditions{})
+ // Check that the type conforms to the duck Knative Resource shape.
+ _ duckv1.KRShaped = (*HorizonSource)(nil)
+)
+
+// HorizonAuthSpec is the information used to authenticate with a Horizon API
+type HorizonAuthSpec struct {
+ // Address contains the URL of the vSphere API.
+ Address apis.URL `json:"address"`
+
+ // SkipTLSVerify specifies whether the client should skip TLS verification when
+ // talking to the vsphere address.
+ SkipTLSVerify bool `json:"skipTLSVerify,omitempty"`
+
+ // SecretRef is a reference to a Kubernetes secret which contains keys for
+ // "domain", "username" and "password", which will be used to authenticate with
+ // the Horizon API at "address".
+ SecretRef corev1.LocalObjectReference `json:"secretRef"`
+}
+
+// HorizonSourceSpec holds the desired state of the HorizonSource (from the client).
+type HorizonSourceSpec struct {
+ // inherits duck/v1 SourceSpec, which currently provides:
+ // * Sink - a reference to an object that will resolve to a domain name or
+ // a URI directly to use as the sink.
+ // * CloudEventOverrides - defines overrides to control the output format
+ // and modifications of the event sent to the sink.
+ duckv1.SourceSpec `json:",inline"`
+
+ // ServiceAccountName holds the name of the Kubernetes service account
+ // as which the underlying K8s resources should be run. If unspecified
+ // this will default to the "default" service account for the namespace
+ // in which the HorizonSource exists.
+ // +optional
+ ServiceAccountName string `json:"serviceAccountName,omitempty"`
+
+ HorizonAuthSpec `json:",inline"`
+}
+
+// HorizonSourceStatus communicates the observed state of the HorizonSource (from the controller).
+type HorizonSourceStatus struct {
+ // inherits duck/v1 SourceStatus, which currently provides:
+ // * ObservedGeneration - the 'Generation' of the Service that was last
+ // processed by the controller.
+ // * Conditions - the latest available observations of a resource's current
+ // state.
+ // * SinkURI - the current active sink URI that has been configured for the
+ // Source.
+ duckv1.SourceStatus `json:",inline"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// HorizonSourceList is a list of HorizonSource resources
+type HorizonSourceList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata"`
+
+ Items []HorizonSource `json:"items"`
+}
+
+// GetStatus retrieves the status of the resource. Implements the KRShaped interface.
+func (hs *HorizonSource) GetStatus() *duckv1.Status {
+ return &hs.Status.Status
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_types_test.go b/pkg/apis/sources/v1alpha1/horizonsource_types_test.go
new file mode 100644
index 000000000..b73fac593
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_types_test.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ "testing"
+)
+
+func TestHorizonSource_GetGroupVersionKind(t *testing.T) {
+ src := HorizonSource{}
+ gvk := src.GetGroupVersionKind()
+
+ if gvk.Kind != "HorizonSource" {
+ t.Errorf("Should be 'HorizonSource'.")
+ }
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_validation.go b/pkg/apis/sources/v1alpha1/horizonsource_validation.go
new file mode 100644
index 000000000..66191965c
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_validation.go
@@ -0,0 +1,65 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ "context"
+
+ "knative.dev/pkg/apis"
+ "knative.dev/pkg/kmp"
+)
+
+// Validate validates HorizonSource.
+func (src *HorizonSource) Validate(ctx context.Context) *apis.FieldError {
+ var errs *apis.FieldError
+
+ if apis.IsInUpdate(ctx) {
+ original := apis.GetBaseline(ctx).(*HorizonSource)
+
+ // all fields immutable
+ if diff, err := kmp.ShortDiff(original.Spec, src.Spec); err != nil {
+ return &apis.FieldError{
+ Message: "Failed to diff HorizonSource",
+ Paths: []string{"spec"},
+ Details: err.Error(),
+ }
+ } else if diff != "" {
+ return &apis.FieldError{
+ Message: "Immutable fields changed (-old +new)",
+ Paths: []string{"spec"},
+ Details: diff,
+ }
+ }
+ }
+
+ errs = errs.Also(src.Spec.Validate(ctx).ViaField("spec"))
+ return errs
+}
+
+// Validate validates HorizonSourceSpec.
+func (spec *HorizonSourceSpec) Validate(ctx context.Context) *apis.FieldError {
+ var errs *apis.FieldError
+
+ errs = spec.Sink.Validate(ctx).ViaField("sink").
+ Also(spec.HorizonAuthSpec.Validate(ctx))
+
+ if spec.ServiceAccountName == "" {
+ errs = errs.Also(apis.ErrMissingField("serviceAccountName"))
+ }
+
+ return errs
+}
+
+// Validate implements apis.Validatable
+func (auth *HorizonAuthSpec) Validate(ctx context.Context) (err *apis.FieldError) {
+ if auth.Address.Host == "" {
+ err = err.Also(apis.ErrMissingField("address.host"))
+ }
+ if auth.SecretRef.Name == "" {
+ err = err.Also(apis.ErrMissingField("secretRef.name"))
+ }
+ return err
+}
diff --git a/pkg/apis/sources/v1alpha1/horizonsource_validation_test.go b/pkg/apis/sources/v1alpha1/horizonsource_validation_test.go
new file mode 100644
index 000000000..ecaca23d7
--- /dev/null
+++ b/pkg/apis/sources/v1alpha1/horizonsource_validation_test.go
@@ -0,0 +1,308 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package v1alpha1
+
+import (
+ "context"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ corev1 "k8s.io/api/core/v1"
+ duckv1 "knative.dev/pkg/apis/duck/v1"
+ "knative.dev/pkg/webhook/resourcesemantics"
+
+ "knative.dev/pkg/apis"
+)
+
+var (
+ source, _ = apis.ParseURL("https://horizon.api.dev")
+
+ fullSpec HorizonSourceSpec = HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: duckv1.Destination{
+ Ref: &duckv1.KReference{
+ APIVersion: "v1",
+ Kind: "Broker",
+ Namespace: "default",
+ Name: "default",
+ },
+ },
+ },
+ ServiceAccountName: "default",
+ HorizonAuthSpec: HorizonAuthSpec{
+ Address: *source,
+ SkipTLSVerify: false,
+ SecretRef: corev1.LocalObjectReference{
+ Name: "horizon-secret",
+ },
+ },
+ }
+)
+
+func TestHorizonSourceImmutableFields(t *testing.T) {
+ testCases := map[string]struct {
+ orig *HorizonSourceSpec
+ updated HorizonSourceSpec
+ allowed bool
+ }{
+ "nil orig": {
+ updated: fullSpec,
+ allowed: true,
+ },
+ "Sink.Namespace changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: duckv1.Destination{
+ Ref: &duckv1.KReference{
+ APIVersion: fullSpec.Sink.Ref.APIVersion,
+ Kind: fullSpec.Sink.Ref.Kind,
+ Namespace: "changed",
+ Name: fullSpec.Sink.Ref.Name,
+ },
+ },
+ },
+ ServiceAccountName: fullSpec.ServiceAccountName,
+ HorizonAuthSpec: fullSpec.HorizonAuthSpec,
+ },
+ allowed: false,
+ },
+ "Sink.Name changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: duckv1.Destination{
+ Ref: &duckv1.KReference{
+ APIVersion: fullSpec.Sink.Ref.APIVersion,
+ Kind: fullSpec.Sink.Ref.Kind,
+ Namespace: fullSpec.Sink.Ref.Namespace,
+ Name: "changed",
+ },
+ },
+ },
+ ServiceAccountName: fullSpec.ServiceAccountName,
+ HorizonAuthSpec: fullSpec.HorizonAuthSpec,
+ },
+ allowed: false,
+ },
+ "Sink.ApiVersion changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: duckv1.Destination{
+ Ref: &duckv1.KReference{
+ APIVersion: "v1alpha1",
+ Kind: fullSpec.Sink.Ref.Kind,
+ Namespace: fullSpec.Sink.Ref.Namespace,
+ Name: fullSpec.Sink.Ref.Name,
+ },
+ },
+ },
+ ServiceAccountName: fullSpec.ServiceAccountName,
+ HorizonAuthSpec: fullSpec.HorizonAuthSpec,
+ },
+ allowed: false,
+ },
+ "ServiceAccount changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: fullSpec.SourceSpec,
+ ServiceAccountName: "changed",
+ HorizonAuthSpec: fullSpec.HorizonAuthSpec,
+ },
+ allowed: false,
+ },
+ "Auth.Address changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: fullSpec.SourceSpec,
+ ServiceAccountName: fullSpec.ServiceAccountName,
+ HorizonAuthSpec: HorizonAuthSpec{
+ Address: apis.URL{
+ Scheme: "http",
+ Host: "changed.example.com",
+ },
+ SkipTLSVerify: fullSpec.SkipTLSVerify,
+ SecretRef: fullSpec.SecretRef,
+ },
+ },
+ allowed: false,
+ },
+ "Auth.SkipTLSVerify changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: fullSpec.SourceSpec,
+ ServiceAccountName: fullSpec.ServiceAccountName,
+ HorizonAuthSpec: HorizonAuthSpec{
+ Address: fullSpec.Address,
+ SkipTLSVerify: true,
+ SecretRef: fullSpec.SecretRef,
+ },
+ },
+ allowed: false,
+ },
+ "Auth.SecretRef changed": {
+ orig: &fullSpec,
+ updated: HorizonSourceSpec{
+ SourceSpec: fullSpec.SourceSpec,
+ ServiceAccountName: fullSpec.ServiceAccountName,
+ HorizonAuthSpec: HorizonAuthSpec{
+ Address: fullSpec.Address,
+ SkipTLSVerify: fullSpec.SkipTLSVerify,
+ SecretRef: corev1.LocalObjectReference{Name: "changed"},
+ },
+ },
+ allowed: false,
+ },
+ }
+
+ for n, tc := range testCases {
+ t.Run(n, func(t *testing.T) {
+ ctx := context.TODO()
+ if tc.orig != nil {
+ orig := &HorizonSource{
+ Spec: *tc.orig,
+ }
+
+ ctx = apis.WithinUpdate(ctx, orig)
+ }
+ updated := &HorizonSource{
+ Spec: tc.updated,
+ }
+ err := updated.Validate(ctx)
+ if tc.allowed != (err == nil) {
+ t.Fatalf("Unexpected immutable field check. Expected %v. Actual %v", tc.allowed, err)
+ }
+ })
+ }
+}
+
+func TestHorizonSourceValidation(t *testing.T) {
+ testCases := map[string]struct {
+ cr resourcesemantics.GenericCRD
+ want *apis.FieldError
+ }{
+ "invalid nil spec": {
+ cr: &HorizonSource{
+ Spec: HorizonSourceSpec{},
+ },
+ want: func() *apis.FieldError {
+ var errs *apis.FieldError
+
+ feSink := apis.ErrGeneric("expected at least one, got none", "ref", "uri")
+ feSink = feSink.ViaField("sink").ViaField("spec")
+ errs = errs.Also(feSink)
+
+ feAddress := apis.ErrMissingField("address.host")
+ feAddress = feAddress.ViaField("spec")
+ errs = errs.Also(feAddress)
+
+ secretRef := apis.ErrMissingField("secretRef.name")
+ secretRef = secretRef.ViaField("spec")
+ errs = errs.Also(secretRef)
+
+ feServiceAccountName := apis.ErrMissingField("serviceAccountName")
+ feServiceAccountName = feServiceAccountName.ViaField("spec")
+ errs = errs.Also(feServiceAccountName)
+
+ return errs
+ }(),
+ },
+ "secret missing": {
+ cr: &HorizonSource{
+ Spec: HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: newDestination(),
+ },
+ ServiceAccountName: "default",
+ HorizonAuthSpec: HorizonAuthSpec{
+ Address: newHorizonAddress(),
+ },
+ },
+ },
+ want: func() *apis.FieldError {
+ var errs *apis.FieldError
+
+ secretRef := apis.ErrMissingField("secretRef.name")
+ secretRef = secretRef.ViaField("spec")
+ errs = errs.Also(secretRef)
+
+ return errs
+ }(),
+ },
+ "horizon source address missing": {
+ cr: &HorizonSource{
+ Spec: HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: newDestination(),
+ },
+ ServiceAccountName: "default",
+ HorizonAuthSpec: HorizonAuthSpec{
+ SecretRef: newSecretRef(),
+ },
+ },
+ },
+ want: func() *apis.FieldError {
+ var errs *apis.FieldError
+
+ secretRef := apis.ErrMissingField("address.host")
+ secretRef = secretRef.ViaField("spec")
+ errs = errs.Also(secretRef)
+
+ return errs
+ }(),
+ },
+ "valid spec": {
+ cr: &HorizonSource{
+ Spec: HorizonSourceSpec{
+ SourceSpec: duckv1.SourceSpec{
+ Sink: newDestination(),
+ },
+ ServiceAccountName: "default",
+ HorizonAuthSpec: HorizonAuthSpec{
+ Address: newHorizonAddress(),
+ SecretRef: newSecretRef(),
+ },
+ },
+ },
+ want: func() *apis.FieldError {
+ return nil
+ }(),
+ },
+ }
+
+ for n, test := range testCases {
+ t.Run(n, func(t *testing.T) {
+ got := test.cr.Validate(context.Background())
+ if diff := cmp.Diff(test.want.Error(), got.Error()); diff != "" {
+ t.Errorf("%s: validate (-want, +got) = %v", n, diff)
+ }
+ })
+ }
+}
+
+func newSecretRef() corev1.LocalObjectReference {
+ return corev1.LocalObjectReference{
+ Name: "horizon-creds",
+ }
+}
+
+func newDestination() duckv1.Destination {
+ return duckv1.Destination{
+ Ref: &duckv1.KReference{
+ Kind: "Deployment",
+ Namespace: "default",
+ Name: "receiver",
+ APIVersion: "v1",
+ },
+ }
+}
+
+func newHorizonAddress() apis.URL {
+ u, _ := url.Parse("http://api.horizon.corp.local")
+ return apis.URL(*u)
+}
diff --git a/pkg/apis/sources/v1alpha1/register.go b/pkg/apis/sources/v1alpha1/register.go
index 311f43092..b86dd11bd 100644
--- a/pkg/apis/sources/v1alpha1/register.go
+++ b/pkg/apis/sources/v1alpha1/register.go
@@ -38,6 +38,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&VSphereSourceList{},
&VSphereBinding{},
&VSphereBindingList{},
+ &HorizonSource{},
+ &HorizonSourceList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
diff --git a/pkg/apis/sources/v1alpha1/register_test.go b/pkg/apis/sources/v1alpha1/register_test.go
index f5ce89970..aa673b49d 100644
--- a/pkg/apis/sources/v1alpha1/register_test.go
+++ b/pkg/apis/sources/v1alpha1/register_test.go
@@ -12,18 +12,26 @@ import (
)
func TestRegisterHelpers(t *testing.T) {
- if got, want := Kind("Foo"), "Foo.sources.tanzu.vmware.com"; got.String() != want {
- t.Errorf("Kind(Foo) = %v, want %v", got.String(), want)
+ if got, want := Kind("VsphereSource"), "VsphereSource.sources.tanzu.vmware.com"; got.String() != want {
+ t.Errorf("Kind(VsphereSource) = %v, want %v", got.String(), want)
}
- if got, want := Resource("Foo"), "Foo.sources.tanzu.vmware.com"; got.String() != want {
- t.Errorf("Resource(Foo) = %v, want %v", got.String(), want)
+ if got, want := Resource("VsphereSource"), "VsphereSource.sources.tanzu.vmware.com"; got.String() != want {
+ t.Errorf("Resource(VsphereSource) = %v, want %v", got.String(), want)
}
if got, want := SchemeGroupVersion.String(), "sources.tanzu.vmware.com/v1alpha1"; got != want {
t.Errorf("SchemeGroupVersion() = %v, want %v", got, want)
}
+ if got, want := Kind("HorizonSource"), "HorizonSource.sources.tanzu.vmware.com"; got.String() != want {
+ t.Errorf("Kind(HorizonSource) = %v, want %v", got.String(), want)
+ }
+
+ if got, want := Resource("HorizonSource"), "HorizonSource.sources.tanzu.vmware.com"; got.String() != want {
+ t.Errorf("Resource(HorizonSource) = %v, want %v", got.String(), want)
+ }
+
scheme := runtime.NewScheme()
if err := addKnownTypes(scheme); err != nil {
t.Errorf("addKnownTypes() = %v", err)
diff --git a/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go
index de63d9e10..bc39b40e8 100644
--- a/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go
@@ -14,6 +14,120 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HorizonAuthSpec) DeepCopyInto(out *HorizonAuthSpec) {
+ *out = *in
+ in.Address.DeepCopyInto(&out.Address)
+ out.SecretRef = in.SecretRef
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizonAuthSpec.
+func (in *HorizonAuthSpec) DeepCopy() *HorizonAuthSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HorizonAuthSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HorizonSource) DeepCopyInto(out *HorizonSource) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizonSource.
+func (in *HorizonSource) DeepCopy() *HorizonSource {
+ if in == nil {
+ return nil
+ }
+ out := new(HorizonSource)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HorizonSource) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HorizonSourceList) DeepCopyInto(out *HorizonSourceList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]HorizonSource, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizonSourceList.
+func (in *HorizonSourceList) DeepCopy() *HorizonSourceList {
+ if in == nil {
+ return nil
+ }
+ out := new(HorizonSourceList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HorizonSourceList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HorizonSourceSpec) DeepCopyInto(out *HorizonSourceSpec) {
+ *out = *in
+ in.SourceSpec.DeepCopyInto(&out.SourceSpec)
+ in.HorizonAuthSpec.DeepCopyInto(&out.HorizonAuthSpec)
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizonSourceSpec.
+func (in *HorizonSourceSpec) DeepCopy() *HorizonSourceSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HorizonSourceSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HorizonSourceStatus) DeepCopyInto(out *HorizonSourceStatus) {
+ *out = *in
+ in.SourceStatus.DeepCopyInto(&out.SourceStatus)
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizonSourceStatus.
+func (in *HorizonSourceStatus) DeepCopy() *HorizonSourceStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(HorizonSourceStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VAuthSpec) DeepCopyInto(out *VAuthSpec) {
*out = *in
diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_horizonsource.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_horizonsource.go
new file mode 100644
index 000000000..5f89d3942
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_horizonsource.go
@@ -0,0 +1,131 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ "context"
+
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ schema "k8s.io/apimachinery/pkg/runtime/schema"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ testing "k8s.io/client-go/testing"
+)
+
+// FakeHorizonSources implements HorizonSourceInterface
+type FakeHorizonSources struct {
+ Fake *FakeSourcesV1alpha1
+ ns string
+}
+
+var horizonsourcesResource = schema.GroupVersionResource{Group: "sources.tanzu.vmware.com", Version: "v1alpha1", Resource: "horizonsources"}
+
+var horizonsourcesKind = schema.GroupVersionKind{Group: "sources.tanzu.vmware.com", Version: "v1alpha1", Kind: "HorizonSource"}
+
+// Get takes name of the horizonSource, and returns the corresponding horizonSource object, and an error if there is any.
+func (c *FakeHorizonSources) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.HorizonSource, err error) {
+ obj, err := c.Fake.
+ Invokes(testing.NewGetAction(horizonsourcesResource, c.ns, name), &v1alpha1.HorizonSource{})
+
+ if obj == nil {
+ return nil, err
+ }
+ return obj.(*v1alpha1.HorizonSource), err
+}
+
+// List takes label and field selectors, and returns the list of HorizonSources that match those selectors.
+func (c *FakeHorizonSources) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.HorizonSourceList, err error) {
+ obj, err := c.Fake.
+ Invokes(testing.NewListAction(horizonsourcesResource, horizonsourcesKind, c.ns, opts), &v1alpha1.HorizonSourceList{})
+
+ if obj == nil {
+ return nil, err
+ }
+
+ label, _, _ := testing.ExtractFromListOptions(opts)
+ if label == nil {
+ label = labels.Everything()
+ }
+ list := &v1alpha1.HorizonSourceList{ListMeta: obj.(*v1alpha1.HorizonSourceList).ListMeta}
+ for _, item := range obj.(*v1alpha1.HorizonSourceList).Items {
+ if label.Matches(labels.Set(item.Labels)) {
+ list.Items = append(list.Items, item)
+ }
+ }
+ return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested horizonSources.
+func (c *FakeHorizonSources) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+ return c.Fake.
+ InvokesWatch(testing.NewWatchAction(horizonsourcesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a horizonSource and creates it. Returns the server's representation of the horizonSource, and an error, if there is any.
+func (c *FakeHorizonSources) Create(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.CreateOptions) (result *v1alpha1.HorizonSource, err error) {
+ obj, err := c.Fake.
+ Invokes(testing.NewCreateAction(horizonsourcesResource, c.ns, horizonSource), &v1alpha1.HorizonSource{})
+
+ if obj == nil {
+ return nil, err
+ }
+ return obj.(*v1alpha1.HorizonSource), err
+}
+
+// Update takes the representation of a horizonSource and updates it. Returns the server's representation of the horizonSource, and an error, if there is any.
+func (c *FakeHorizonSources) Update(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.UpdateOptions) (result *v1alpha1.HorizonSource, err error) {
+ obj, err := c.Fake.
+ Invokes(testing.NewUpdateAction(horizonsourcesResource, c.ns, horizonSource), &v1alpha1.HorizonSource{})
+
+ if obj == nil {
+ return nil, err
+ }
+ return obj.(*v1alpha1.HorizonSource), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeHorizonSources) UpdateStatus(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.UpdateOptions) (*v1alpha1.HorizonSource, error) {
+ obj, err := c.Fake.
+ Invokes(testing.NewUpdateSubresourceAction(horizonsourcesResource, "status", c.ns, horizonSource), &v1alpha1.HorizonSource{})
+
+ if obj == nil {
+ return nil, err
+ }
+ return obj.(*v1alpha1.HorizonSource), err
+}
+
+// Delete takes name of the horizonSource and deletes it. Returns an error if one occurs.
+func (c *FakeHorizonSources) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+ _, err := c.Fake.
+ Invokes(testing.NewDeleteActionWithOptions(horizonsourcesResource, c.ns, name, opts), &v1alpha1.HorizonSource{})
+
+ return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeHorizonSources) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+ action := testing.NewDeleteCollectionAction(horizonsourcesResource, c.ns, listOpts)
+
+ _, err := c.Fake.Invokes(action, &v1alpha1.HorizonSourceList{})
+ return err
+}
+
+// Patch applies the patch and returns the patched horizonSource.
+func (c *FakeHorizonSources) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HorizonSource, err error) {
+ obj, err := c.Fake.
+ Invokes(testing.NewPatchSubresourceAction(horizonsourcesResource, c.ns, name, pt, data, subresources...), &v1alpha1.HorizonSource{})
+
+ if obj == nil {
+ return nil, err
+ }
+ return obj.(*v1alpha1.HorizonSource), err
+}
diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go
index e8762b205..290b05e0a 100644
--- a/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go
+++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go
@@ -17,6 +17,10 @@ type FakeSourcesV1alpha1 struct {
*testing.Fake
}
+func (c *FakeSourcesV1alpha1) HorizonSources(namespace string) v1alpha1.HorizonSourceInterface {
+ return &FakeHorizonSources{c, namespace}
+}
+
func (c *FakeSourcesV1alpha1) VSphereBindings(namespace string) v1alpha1.VSphereBindingInterface {
return &FakeVSphereBindings{c, namespace}
}
diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go
index 67c212ad2..fcf9f8540 100644
--- a/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go
+++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go
@@ -7,6 +7,8 @@ SPDX-License-Identifier: Apache-2.0
package v1alpha1
+type HorizonSourceExpansion interface{}
+
type VSphereBindingExpansion interface{}
type VSphereSourceExpansion interface{}
diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/horizonsource.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/horizonsource.go
new file mode 100644
index 000000000..093aaa0cb
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/horizonsource.go
@@ -0,0 +1,184 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ "context"
+ "time"
+
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ scheme "github.com/vmware-tanzu/sources-for-knative/pkg/client/clientset/versioned/scheme"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ rest "k8s.io/client-go/rest"
+)
+
+// HorizonSourcesGetter has a method to return a HorizonSourceInterface.
+// A group's client should implement this interface.
+type HorizonSourcesGetter interface {
+ HorizonSources(namespace string) HorizonSourceInterface
+}
+
+// HorizonSourceInterface has methods to work with HorizonSource resources.
+type HorizonSourceInterface interface {
+ Create(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.CreateOptions) (*v1alpha1.HorizonSource, error)
+ Update(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.UpdateOptions) (*v1alpha1.HorizonSource, error)
+ UpdateStatus(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.UpdateOptions) (*v1alpha1.HorizonSource, error)
+ Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+ DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+ Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.HorizonSource, error)
+ List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.HorizonSourceList, error)
+ Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+ Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HorizonSource, err error)
+ HorizonSourceExpansion
+}
+
+// horizonSources implements HorizonSourceInterface
+type horizonSources struct {
+ client rest.Interface
+ ns string
+}
+
+// newHorizonSources returns a HorizonSources
+func newHorizonSources(c *SourcesV1alpha1Client, namespace string) *horizonSources {
+ return &horizonSources{
+ client: c.RESTClient(),
+ ns: namespace,
+ }
+}
+
+// Get takes name of the horizonSource, and returns the corresponding horizonSource object, and an error if there is any.
+func (c *horizonSources) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.HorizonSource, err error) {
+ result = &v1alpha1.HorizonSource{}
+ err = c.client.Get().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ Name(name).
+ VersionedParams(&options, scheme.ParameterCodec).
+ Do(ctx).
+ Into(result)
+ return
+}
+
+// List takes label and field selectors, and returns the list of HorizonSources that match those selectors.
+func (c *horizonSources) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.HorizonSourceList, err error) {
+ var timeout time.Duration
+ if opts.TimeoutSeconds != nil {
+ timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+ }
+ result = &v1alpha1.HorizonSourceList{}
+ err = c.client.Get().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ VersionedParams(&opts, scheme.ParameterCodec).
+ Timeout(timeout).
+ Do(ctx).
+ Into(result)
+ return
+}
+
+// Watch returns a watch.Interface that watches the requested horizonSources.
+func (c *horizonSources) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+ var timeout time.Duration
+ if opts.TimeoutSeconds != nil {
+ timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+ }
+ opts.Watch = true
+ return c.client.Get().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ VersionedParams(&opts, scheme.ParameterCodec).
+ Timeout(timeout).
+ Watch(ctx)
+}
+
+// Create takes the representation of a horizonSource and creates it. Returns the server's representation of the horizonSource, and an error, if there is any.
+func (c *horizonSources) Create(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.CreateOptions) (result *v1alpha1.HorizonSource, err error) {
+ result = &v1alpha1.HorizonSource{}
+ err = c.client.Post().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ VersionedParams(&opts, scheme.ParameterCodec).
+ Body(horizonSource).
+ Do(ctx).
+ Into(result)
+ return
+}
+
+// Update takes the representation of a horizonSource and updates it. Returns the server's representation of the horizonSource, and an error, if there is any.
+func (c *horizonSources) Update(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.UpdateOptions) (result *v1alpha1.HorizonSource, err error) {
+ result = &v1alpha1.HorizonSource{}
+ err = c.client.Put().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ Name(horizonSource.Name).
+ VersionedParams(&opts, scheme.ParameterCodec).
+ Body(horizonSource).
+ Do(ctx).
+ Into(result)
+ return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *horizonSources) UpdateStatus(ctx context.Context, horizonSource *v1alpha1.HorizonSource, opts v1.UpdateOptions) (result *v1alpha1.HorizonSource, err error) {
+ result = &v1alpha1.HorizonSource{}
+ err = c.client.Put().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ Name(horizonSource.Name).
+ SubResource("status").
+ VersionedParams(&opts, scheme.ParameterCodec).
+ Body(horizonSource).
+ Do(ctx).
+ Into(result)
+ return
+}
+
+// Delete takes name of the horizonSource and deletes it. Returns an error if one occurs.
+func (c *horizonSources) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+ return c.client.Delete().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ Name(name).
+ Body(&opts).
+ Do(ctx).
+ Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *horizonSources) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+ var timeout time.Duration
+ if listOpts.TimeoutSeconds != nil {
+ timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+ }
+ return c.client.Delete().
+ Namespace(c.ns).
+ Resource("horizonsources").
+ VersionedParams(&listOpts, scheme.ParameterCodec).
+ Timeout(timeout).
+ Body(&opts).
+ Do(ctx).
+ Error()
+}
+
+// Patch applies the patch and returns the patched horizonSource.
+func (c *horizonSources) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HorizonSource, err error) {
+ result = &v1alpha1.HorizonSource{}
+ err = c.client.Patch(pt).
+ Namespace(c.ns).
+ Resource("horizonsources").
+ Name(name).
+ SubResource(subresources...).
+ VersionedParams(&opts, scheme.ParameterCodec).
+ Body(data).
+ Do(ctx).
+ Into(result)
+ return
+}
diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go
index cdac9cc57..1b32a2c76 100644
--- a/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go
+++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go
@@ -17,6 +17,7 @@ import (
type SourcesV1alpha1Interface interface {
RESTClient() rest.Interface
+ HorizonSourcesGetter
VSphereBindingsGetter
VSphereSourcesGetter
}
@@ -26,6 +27,10 @@ type SourcesV1alpha1Client struct {
restClient rest.Interface
}
+func (c *SourcesV1alpha1Client) HorizonSources(namespace string) HorizonSourceInterface {
+ return newHorizonSources(c, namespace)
+}
+
func (c *SourcesV1alpha1Client) VSphereBindings(namespace string) VSphereBindingInterface {
return newVSphereBindings(c, namespace)
}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index 6c272ea61..c90a4dbaa 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -42,6 +42,8 @@ func (f *genericInformer) Lister() cache.GenericLister {
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=sources.tanzu.vmware.com, Version=v1alpha1
+ case v1alpha1.SchemeGroupVersion.WithResource("horizonsources"):
+ return &genericInformer{resource: resource.GroupResource(), informer: f.Sources().V1alpha1().HorizonSources().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("vspherebindings"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Sources().V1alpha1().VSphereBindings().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("vspheresources"):
diff --git a/pkg/client/informers/externalversions/sources/v1alpha1/horizonsource.go b/pkg/client/informers/externalversions/sources/v1alpha1/horizonsource.go
new file mode 100644
index 000000000..58aa41b03
--- /dev/null
+++ b/pkg/client/informers/externalversions/sources/v1alpha1/horizonsource.go
@@ -0,0 +1,79 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ "context"
+ time "time"
+
+ sourcesv1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ versioned "github.com/vmware-tanzu/sources-for-knative/pkg/client/clientset/versioned"
+ internalinterfaces "github.com/vmware-tanzu/sources-for-knative/pkg/client/informers/externalversions/internalinterfaces"
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/client/listers/sources/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ watch "k8s.io/apimachinery/pkg/watch"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// HorizonSourceInformer provides access to a shared informer and lister for
+// HorizonSources.
+type HorizonSourceInformer interface {
+ Informer() cache.SharedIndexInformer
+ Lister() v1alpha1.HorizonSourceLister
+}
+
+type horizonSourceInformer struct {
+ factory internalinterfaces.SharedInformerFactory
+ tweakListOptions internalinterfaces.TweakListOptionsFunc
+ namespace string
+}
+
+// NewHorizonSourceInformer constructs a new informer for HorizonSource type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewHorizonSourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+ return NewFilteredHorizonSourceInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredHorizonSourceInformer constructs a new informer for HorizonSource type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredHorizonSourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+ return cache.NewSharedIndexInformer(
+ &cache.ListWatch{
+ ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.SourcesV1alpha1().HorizonSources(namespace).List(context.TODO(), options)
+ },
+ WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.SourcesV1alpha1().HorizonSources(namespace).Watch(context.TODO(), options)
+ },
+ },
+ &sourcesv1alpha1.HorizonSource{},
+ resyncPeriod,
+ indexers,
+ )
+}
+
+func (f *horizonSourceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+ return NewFilteredHorizonSourceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *horizonSourceInformer) Informer() cache.SharedIndexInformer {
+ return f.factory.InformerFor(&sourcesv1alpha1.HorizonSource{}, f.defaultInformer)
+}
+
+func (f *horizonSourceInformer) Lister() v1alpha1.HorizonSourceLister {
+ return v1alpha1.NewHorizonSourceLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/sources/v1alpha1/interface.go b/pkg/client/informers/externalversions/sources/v1alpha1/interface.go
index 42e90ff9a..e8e918854 100644
--- a/pkg/client/informers/externalversions/sources/v1alpha1/interface.go
+++ b/pkg/client/informers/externalversions/sources/v1alpha1/interface.go
@@ -13,6 +13,8 @@ import (
// Interface provides access to all the informers in this group version.
type Interface interface {
+ // HorizonSources returns a HorizonSourceInformer.
+ HorizonSources() HorizonSourceInformer
// VSphereBindings returns a VSphereBindingInformer.
VSphereBindings() VSphereBindingInformer
// VSphereSources returns a VSphereSourceInformer.
@@ -30,6 +32,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
+// HorizonSources returns a HorizonSourceInformer.
+func (v *version) HorizonSources() HorizonSourceInformer {
+ return &horizonSourceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
// VSphereBindings returns a VSphereBindingInformer.
func (v *version) VSphereBindings() VSphereBindingInformer {
return &vSphereBindingInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/client/injection/client/client.go b/pkg/client/injection/client/client.go
index 2a69e2d25..f522b7f24 100644
--- a/pkg/client/injection/client/client.go
+++ b/pkg/client/injection/client/client.go
@@ -100,6 +100,137 @@ func (w *wrapSourcesV1alpha1) RESTClient() rest.Interface {
panic("RESTClient called on dynamic client!")
}
+func (w *wrapSourcesV1alpha1) HorizonSources(namespace string) typedsourcesv1alpha1.HorizonSourceInterface {
+ return &wrapSourcesV1alpha1HorizonSourceImpl{
+ dyn: w.dyn.Resource(schema.GroupVersionResource{
+ Group: "sources.tanzu.vmware.com",
+ Version: "v1alpha1",
+ Resource: "horizonsources",
+ }),
+
+ namespace: namespace,
+ }
+}
+
+type wrapSourcesV1alpha1HorizonSourceImpl struct {
+ dyn dynamic.NamespaceableResourceInterface
+
+ namespace string
+}
+
+var _ typedsourcesv1alpha1.HorizonSourceInterface = (*wrapSourcesV1alpha1HorizonSourceImpl)(nil)
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) Create(ctx context.Context, in *v1alpha1.HorizonSource, opts v1.CreateOptions) (*v1alpha1.HorizonSource, error) {
+ in.SetGroupVersionKind(schema.GroupVersionKind{
+ Group: "sources.tanzu.vmware.com",
+ Version: "v1alpha1",
+ Kind: "HorizonSource",
+ })
+ uo := &unstructured.Unstructured{}
+ if err := convert(in, uo); err != nil {
+ return nil, err
+ }
+ uo, err := w.dyn.Namespace(w.namespace).Create(ctx, uo, opts)
+ if err != nil {
+ return nil, err
+ }
+ out := &v1alpha1.HorizonSource{}
+ if err := convert(uo, out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+ return w.dyn.Namespace(w.namespace).Delete(ctx, name, opts)
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+ return w.dyn.Namespace(w.namespace).DeleteCollection(ctx, opts, listOpts)
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.HorizonSource, error) {
+ uo, err := w.dyn.Namespace(w.namespace).Get(ctx, name, opts)
+ if err != nil {
+ return nil, err
+ }
+ out := &v1alpha1.HorizonSource{}
+ if err := convert(uo, out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.HorizonSourceList, error) {
+ uo, err := w.dyn.Namespace(w.namespace).List(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ out := &v1alpha1.HorizonSourceList{}
+ if err := convert(uo, out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HorizonSource, err error) {
+ uo, err := w.dyn.Namespace(w.namespace).Patch(ctx, name, pt, data, opts)
+ if err != nil {
+ return nil, err
+ }
+ out := &v1alpha1.HorizonSource{}
+ if err := convert(uo, out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) Update(ctx context.Context, in *v1alpha1.HorizonSource, opts v1.UpdateOptions) (*v1alpha1.HorizonSource, error) {
+ in.SetGroupVersionKind(schema.GroupVersionKind{
+ Group: "sources.tanzu.vmware.com",
+ Version: "v1alpha1",
+ Kind: "HorizonSource",
+ })
+ uo := &unstructured.Unstructured{}
+ if err := convert(in, uo); err != nil {
+ return nil, err
+ }
+ uo, err := w.dyn.Namespace(w.namespace).Update(ctx, uo, opts)
+ if err != nil {
+ return nil, err
+ }
+ out := &v1alpha1.HorizonSource{}
+ if err := convert(uo, out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) UpdateStatus(ctx context.Context, in *v1alpha1.HorizonSource, opts v1.UpdateOptions) (*v1alpha1.HorizonSource, error) {
+ in.SetGroupVersionKind(schema.GroupVersionKind{
+ Group: "sources.tanzu.vmware.com",
+ Version: "v1alpha1",
+ Kind: "HorizonSource",
+ })
+ uo := &unstructured.Unstructured{}
+ if err := convert(in, uo); err != nil {
+ return nil, err
+ }
+ uo, err := w.dyn.Namespace(w.namespace).UpdateStatus(ctx, uo, opts)
+ if err != nil {
+ return nil, err
+ }
+ out := &v1alpha1.HorizonSource{}
+ if err := convert(uo, out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (w *wrapSourcesV1alpha1HorizonSourceImpl) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+ return nil, errors.New("NYI: Watch")
+}
+
func (w *wrapSourcesV1alpha1) VSphereBindings(namespace string) typedsourcesv1alpha1.VSphereBindingInterface {
return &wrapSourcesV1alpha1VSphereBindingImpl{
dyn: w.dyn.Resource(schema.GroupVersionResource{
diff --git a/pkg/client/injection/informers/sources/v1alpha1/horizonsource/fake/fake.go b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/fake/fake.go
new file mode 100644
index 000000000..179ef4fa5
--- /dev/null
+++ b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/fake/fake.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ context "context"
+
+ fake "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/factory/fake"
+ horizonsource "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/sources/v1alpha1/horizonsource"
+ controller "knative.dev/pkg/controller"
+ injection "knative.dev/pkg/injection"
+)
+
+var Get = horizonsource.Get
+
+func init() {
+ injection.Fake.RegisterInformer(withInformer)
+}
+
+func withInformer(ctx context.Context) (context.Context, controller.Informer) {
+ f := fake.Get(ctx)
+ inf := f.Sources().V1alpha1().HorizonSources()
+ return context.WithValue(ctx, horizonsource.Key{}, inf), inf.Informer()
+}
diff --git a/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/fake/fake.go b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/fake/fake.go
new file mode 100644
index 000000000..c7af7b755
--- /dev/null
+++ b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/fake/fake.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ context "context"
+
+ factoryfiltered "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/factory/filtered"
+ filtered "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered"
+ controller "knative.dev/pkg/controller"
+ injection "knative.dev/pkg/injection"
+ logging "knative.dev/pkg/logging"
+)
+
+var Get = filtered.Get
+
+func init() {
+ injection.Fake.RegisterFilteredInformers(withInformer)
+}
+
+func withInformer(ctx context.Context) (context.Context, []controller.Informer) {
+ untyped := ctx.Value(factoryfiltered.LabelKey{})
+ if untyped == nil {
+ logging.FromContext(ctx).Panic(
+ "Unable to fetch labelkey from context.")
+ }
+ labelSelectors := untyped.([]string)
+ infs := []controller.Informer{}
+ for _, selector := range labelSelectors {
+ f := factoryfiltered.Get(ctx, selector)
+ inf := f.Sources().V1alpha1().HorizonSources()
+ ctx = context.WithValue(ctx, filtered.Key{Selector: selector}, inf)
+ infs = append(infs, inf.Informer())
+ }
+ return ctx, infs
+}
diff --git a/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/horizonsource.go b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/horizonsource.go
new file mode 100644
index 000000000..8759374dc
--- /dev/null
+++ b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/horizonsource.go
@@ -0,0 +1,125 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package filtered
+
+import (
+ context "context"
+
+ apissourcesv1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ versioned "github.com/vmware-tanzu/sources-for-knative/pkg/client/clientset/versioned"
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/client/informers/externalversions/sources/v1alpha1"
+ client "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/client"
+ filtered "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/factory/filtered"
+ sourcesv1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/client/listers/sources/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ cache "k8s.io/client-go/tools/cache"
+ controller "knative.dev/pkg/controller"
+ injection "knative.dev/pkg/injection"
+ logging "knative.dev/pkg/logging"
+)
+
+func init() {
+ injection.Default.RegisterFilteredInformers(withInformer)
+ injection.Dynamic.RegisterDynamicInformer(withDynamicInformer)
+}
+
+// Key is used for associating the Informer inside the context.Context.
+type Key struct {
+ Selector string
+}
+
+func withInformer(ctx context.Context) (context.Context, []controller.Informer) {
+ untyped := ctx.Value(filtered.LabelKey{})
+ if untyped == nil {
+ logging.FromContext(ctx).Panic(
+ "Unable to fetch labelkey from context.")
+ }
+ labelSelectors := untyped.([]string)
+ infs := []controller.Informer{}
+ for _, selector := range labelSelectors {
+ f := filtered.Get(ctx, selector)
+ inf := f.Sources().V1alpha1().HorizonSources()
+ ctx = context.WithValue(ctx, Key{Selector: selector}, inf)
+ infs = append(infs, inf.Informer())
+ }
+ return ctx, infs
+}
+
+func withDynamicInformer(ctx context.Context) context.Context {
+ untyped := ctx.Value(filtered.LabelKey{})
+ if untyped == nil {
+ logging.FromContext(ctx).Panic(
+ "Unable to fetch labelkey from context.")
+ }
+ labelSelectors := untyped.([]string)
+ for _, selector := range labelSelectors {
+ inf := &wrapper{client: client.Get(ctx), selector: selector}
+ ctx = context.WithValue(ctx, Key{Selector: selector}, inf)
+ }
+ return ctx
+}
+
+// Get extracts the typed informer from the context.
+func Get(ctx context.Context, selector string) v1alpha1.HorizonSourceInformer {
+ untyped := ctx.Value(Key{Selector: selector})
+ if untyped == nil {
+ logging.FromContext(ctx).Panicf(
+ "Unable to fetch github.com/vmware-tanzu/sources-for-knative/pkg/client/informers/externalversions/sources/v1alpha1.HorizonSourceInformer with selector %s from context.", selector)
+ }
+ return untyped.(v1alpha1.HorizonSourceInformer)
+}
+
+type wrapper struct {
+ client versioned.Interface
+
+ namespace string
+
+ selector string
+}
+
+var _ v1alpha1.HorizonSourceInformer = (*wrapper)(nil)
+var _ sourcesv1alpha1.HorizonSourceLister = (*wrapper)(nil)
+
+func (w *wrapper) Informer() cache.SharedIndexInformer {
+ return cache.NewSharedIndexInformer(nil, &apissourcesv1alpha1.HorizonSource{}, 0, nil)
+}
+
+func (w *wrapper) Lister() sourcesv1alpha1.HorizonSourceLister {
+ return w
+}
+
+func (w *wrapper) HorizonSources(namespace string) sourcesv1alpha1.HorizonSourceNamespaceLister {
+ return &wrapper{client: w.client, namespace: namespace, selector: w.selector}
+}
+
+func (w *wrapper) List(selector labels.Selector) (ret []*apissourcesv1alpha1.HorizonSource, err error) {
+ reqs, err := labels.ParseToRequirements(w.selector)
+ if err != nil {
+ return nil, err
+ }
+ selector = selector.Add(reqs...)
+ lo, err := w.client.SourcesV1alpha1().HorizonSources(w.namespace).List(context.TODO(), v1.ListOptions{
+ LabelSelector: selector.String(),
+ // TODO(mattmoor): Incorporate resourceVersion bounds based on staleness criteria.
+ })
+ if err != nil {
+ return nil, err
+ }
+ for idx := range lo.Items {
+ ret = append(ret, &lo.Items[idx])
+ }
+ return ret, nil
+}
+
+func (w *wrapper) Get(name string) (*apissourcesv1alpha1.HorizonSource, error) {
+ // TODO(mattmoor): Check that the fetched object matches the selector.
+ return w.client.SourcesV1alpha1().HorizonSources(w.namespace).Get(context.TODO(), name, v1.GetOptions{
+ // TODO(mattmoor): Incorporate resourceVersion bounds based on staleness criteria.
+ })
+}
diff --git a/pkg/client/injection/informers/sources/v1alpha1/horizonsource/horizonsource.go b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/horizonsource.go
new file mode 100644
index 000000000..ccb753a3e
--- /dev/null
+++ b/pkg/client/injection/informers/sources/v1alpha1/horizonsource/horizonsource.go
@@ -0,0 +1,105 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package horizonsource
+
+import (
+ context "context"
+
+ apissourcesv1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ versioned "github.com/vmware-tanzu/sources-for-knative/pkg/client/clientset/versioned"
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/client/informers/externalversions/sources/v1alpha1"
+ client "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/client"
+ factory "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/factory"
+ sourcesv1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/client/listers/sources/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ cache "k8s.io/client-go/tools/cache"
+ controller "knative.dev/pkg/controller"
+ injection "knative.dev/pkg/injection"
+ logging "knative.dev/pkg/logging"
+)
+
+func init() {
+ injection.Default.RegisterInformer(withInformer)
+ injection.Dynamic.RegisterDynamicInformer(withDynamicInformer)
+}
+
+// Key is used for associating the Informer inside the context.Context.
+type Key struct{}
+
+func withInformer(ctx context.Context) (context.Context, controller.Informer) {
+ f := factory.Get(ctx)
+ inf := f.Sources().V1alpha1().HorizonSources()
+ return context.WithValue(ctx, Key{}, inf), inf.Informer()
+}
+
+func withDynamicInformer(ctx context.Context) context.Context {
+ inf := &wrapper{client: client.Get(ctx), resourceVersion: injection.GetResourceVersion(ctx)}
+ return context.WithValue(ctx, Key{}, inf)
+}
+
+// Get extracts the typed informer from the context.
+func Get(ctx context.Context) v1alpha1.HorizonSourceInformer {
+ untyped := ctx.Value(Key{})
+ if untyped == nil {
+ logging.FromContext(ctx).Panic(
+ "Unable to fetch github.com/vmware-tanzu/sources-for-knative/pkg/client/informers/externalversions/sources/v1alpha1.HorizonSourceInformer from context.")
+ }
+ return untyped.(v1alpha1.HorizonSourceInformer)
+}
+
+type wrapper struct {
+ client versioned.Interface
+
+ namespace string
+
+ resourceVersion string
+}
+
+var _ v1alpha1.HorizonSourceInformer = (*wrapper)(nil)
+var _ sourcesv1alpha1.HorizonSourceLister = (*wrapper)(nil)
+
+func (w *wrapper) Informer() cache.SharedIndexInformer {
+ return cache.NewSharedIndexInformer(nil, &apissourcesv1alpha1.HorizonSource{}, 0, nil)
+}
+
+func (w *wrapper) Lister() sourcesv1alpha1.HorizonSourceLister {
+ return w
+}
+
+func (w *wrapper) HorizonSources(namespace string) sourcesv1alpha1.HorizonSourceNamespaceLister {
+ return &wrapper{client: w.client, namespace: namespace, resourceVersion: w.resourceVersion}
+}
+
+// SetResourceVersion allows consumers to adjust the minimum resourceVersion
+// used by the underlying client. It is not accessible via the standard
+// lister interface, but can be accessed through a user-defined interface and
+// an implementation check e.g. rvs, ok := foo.(ResourceVersionSetter)
+func (w *wrapper) SetResourceVersion(resourceVersion string) {
+ w.resourceVersion = resourceVersion
+}
+
+func (w *wrapper) List(selector labels.Selector) (ret []*apissourcesv1alpha1.HorizonSource, err error) {
+ lo, err := w.client.SourcesV1alpha1().HorizonSources(w.namespace).List(context.TODO(), v1.ListOptions{
+ LabelSelector: selector.String(),
+ ResourceVersion: w.resourceVersion,
+ })
+ if err != nil {
+ return nil, err
+ }
+ for idx := range lo.Items {
+ ret = append(ret, &lo.Items[idx])
+ }
+ return ret, nil
+}
+
+func (w *wrapper) Get(name string) (*apissourcesv1alpha1.HorizonSource, error) {
+ return w.client.SourcesV1alpha1().HorizonSources(w.namespace).Get(context.TODO(), name, v1.GetOptions{
+ ResourceVersion: w.resourceVersion,
+ })
+}
diff --git a/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/controller.go b/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/controller.go
new file mode 100644
index 000000000..33590e567
--- /dev/null
+++ b/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/controller.go
@@ -0,0 +1,151 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package horizonsource
+
+import (
+ context "context"
+ fmt "fmt"
+ reflect "reflect"
+ strings "strings"
+
+ versionedscheme "github.com/vmware-tanzu/sources-for-knative/pkg/client/clientset/versioned/scheme"
+ client "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/client"
+ horizonsource "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/sources/v1alpha1/horizonsource"
+ zap "go.uber.org/zap"
+ corev1 "k8s.io/api/core/v1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ scheme "k8s.io/client-go/kubernetes/scheme"
+ v1 "k8s.io/client-go/kubernetes/typed/core/v1"
+ record "k8s.io/client-go/tools/record"
+ kubeclient "knative.dev/pkg/client/injection/kube/client"
+ controller "knative.dev/pkg/controller"
+ logging "knative.dev/pkg/logging"
+ logkey "knative.dev/pkg/logging/logkey"
+ reconciler "knative.dev/pkg/reconciler"
+)
+
+const (
+ defaultControllerAgentName = "horizonsource-controller"
+ defaultFinalizerName = "horizonsources.sources.tanzu.vmware.com"
+)
+
+// NewImpl returns a controller.Impl that handles queuing and feeding work from
+// the queue through an implementation of controller.Reconciler, delegating to
+// the provided Interface and optional Finalizer methods. OptionsFn is used to return
+// controller.ControllerOptions to be used by the internal reconciler.
+func NewImpl(ctx context.Context, r Interface, optionsFns ...controller.OptionsFn) *controller.Impl {
+ logger := logging.FromContext(ctx)
+
+ // Check the options function input. It should be 0 or 1.
+ if len(optionsFns) > 1 {
+ logger.Fatal("Up to one options function is supported, found: ", len(optionsFns))
+ }
+
+ horizonsourceInformer := horizonsource.Get(ctx)
+
+ lister := horizonsourceInformer.Lister()
+
+ var promoteFilterFunc func(obj interface{}) bool
+
+ rec := &reconcilerImpl{
+ LeaderAwareFuncs: reconciler.LeaderAwareFuncs{
+ PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
+ all, err := lister.List(labels.Everything())
+ if err != nil {
+ return err
+ }
+ for _, elt := range all {
+ if promoteFilterFunc != nil {
+ if ok := promoteFilterFunc(elt); !ok {
+ continue
+ }
+ }
+ enq(bkt, types.NamespacedName{
+ Namespace: elt.GetNamespace(),
+ Name: elt.GetName(),
+ })
+ }
+ return nil
+ },
+ },
+ Client: client.Get(ctx),
+ Lister: lister,
+ reconciler: r,
+ finalizerName: defaultFinalizerName,
+ }
+
+ ctrType := reflect.TypeOf(r).Elem()
+ ctrTypeName := fmt.Sprintf("%s.%s", ctrType.PkgPath(), ctrType.Name())
+ ctrTypeName = strings.ReplaceAll(ctrTypeName, "/", ".")
+
+ logger = logger.With(
+ zap.String(logkey.ControllerType, ctrTypeName),
+ zap.String(logkey.Kind, "sources.tanzu.vmware.com.HorizonSource"),
+ )
+
+ impl := controller.NewContext(ctx, rec, controller.ControllerOptions{WorkQueueName: ctrTypeName, Logger: logger})
+ agentName := defaultControllerAgentName
+
+ // Pass impl to the options. Save any optional results.
+ for _, fn := range optionsFns {
+ opts := fn(impl)
+ if opts.ConfigStore != nil {
+ rec.configStore = opts.ConfigStore
+ }
+ if opts.FinalizerName != "" {
+ rec.finalizerName = opts.FinalizerName
+ }
+ if opts.AgentName != "" {
+ agentName = opts.AgentName
+ }
+ if opts.SkipStatusUpdates {
+ rec.skipStatusUpdates = true
+ }
+ if opts.DemoteFunc != nil {
+ rec.DemoteFunc = opts.DemoteFunc
+ }
+ if opts.PromoteFilterFunc != nil {
+ promoteFilterFunc = opts.PromoteFilterFunc
+ }
+ }
+
+ rec.Recorder = createRecorder(ctx, agentName)
+
+ return impl
+}
+
+func createRecorder(ctx context.Context, agentName string) record.EventRecorder {
+ logger := logging.FromContext(ctx)
+
+ recorder := controller.GetEventRecorder(ctx)
+ if recorder == nil {
+ // Create event broadcaster
+ logger.Debug("Creating event broadcaster")
+ eventBroadcaster := record.NewBroadcaster()
+ watches := []watch.Interface{
+ eventBroadcaster.StartLogging(logger.Named("event-broadcaster").Infof),
+ eventBroadcaster.StartRecordingToSink(
+ &v1.EventSinkImpl{Interface: kubeclient.Get(ctx).CoreV1().Events("")}),
+ }
+ recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: agentName})
+ go func() {
+ <-ctx.Done()
+ for _, w := range watches {
+ w.Stop()
+ }
+ }()
+ }
+
+ return recorder
+}
+
+func init() {
+ versionedscheme.AddToScheme(scheme.Scheme)
+}
diff --git a/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/reconciler.go b/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/reconciler.go
new file mode 100644
index 000000000..3182727a6
--- /dev/null
+++ b/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/reconciler.go
@@ -0,0 +1,439 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package horizonsource
+
+import (
+ context "context"
+ json "encoding/json"
+ fmt "fmt"
+
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ versioned "github.com/vmware-tanzu/sources-for-knative/pkg/client/clientset/versioned"
+ sourcesv1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/client/listers/sources/v1alpha1"
+ zap "go.uber.org/zap"
+ v1 "k8s.io/api/core/v1"
+ equality "k8s.io/apimachinery/pkg/api/equality"
+ errors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ types "k8s.io/apimachinery/pkg/types"
+ sets "k8s.io/apimachinery/pkg/util/sets"
+ record "k8s.io/client-go/tools/record"
+ controller "knative.dev/pkg/controller"
+ kmp "knative.dev/pkg/kmp"
+ logging "knative.dev/pkg/logging"
+ reconciler "knative.dev/pkg/reconciler"
+)
+
+// Interface defines the strongly typed interfaces to be implemented by a
+// controller reconciling v1alpha1.HorizonSource.
+type Interface interface {
+ // ReconcileKind implements custom logic to reconcile v1alpha1.HorizonSource. Any changes
+ // to the objects .Status or .Finalizers will be propagated to the stored
+ // object. It is recommended that implementors do not call any update calls
+ // for the Kind inside of ReconcileKind, it is the responsibility of the calling
+ // controller to propagate those properties. The resource passed to ReconcileKind
+ // will always have an empty deletion timestamp.
+ ReconcileKind(ctx context.Context, o *v1alpha1.HorizonSource) reconciler.Event
+}
+
+// Finalizer defines the strongly typed interfaces to be implemented by a
+// controller finalizing v1alpha1.HorizonSource.
+type Finalizer interface {
+ // FinalizeKind implements custom logic to finalize v1alpha1.HorizonSource. Any changes
+ // to the objects .Status or .Finalizers will be ignored. Returning a nil or
+ // Normal type reconciler.Event will allow the finalizer to be deleted on
+ // the resource. The resource passed to FinalizeKind will always have a set
+ // deletion timestamp.
+ FinalizeKind(ctx context.Context, o *v1alpha1.HorizonSource) reconciler.Event
+}
+
+// ReadOnlyInterface defines the strongly typed interfaces to be implemented by a
+// controller reconciling v1alpha1.HorizonSource if they want to process resources for which
+// they are not the leader.
+type ReadOnlyInterface interface {
+ // ObserveKind implements logic to observe v1alpha1.HorizonSource.
+ // This method should not write to the API.
+ ObserveKind(ctx context.Context, o *v1alpha1.HorizonSource) reconciler.Event
+}
+
+type doReconcile func(ctx context.Context, o *v1alpha1.HorizonSource) reconciler.Event
+
+// reconcilerImpl implements controller.Reconciler for v1alpha1.HorizonSource resources.
+type reconcilerImpl struct {
+ // LeaderAwareFuncs is inlined to help us implement reconciler.LeaderAware.
+ reconciler.LeaderAwareFuncs
+
+ // Client is used to write back status updates.
+ Client versioned.Interface
+
+ // Listers index properties about resources.
+ Lister sourcesv1alpha1.HorizonSourceLister
+
+ // Recorder is an event recorder for recording Event resources to the
+ // Kubernetes API.
+ Recorder record.EventRecorder
+
+ // configStore allows for decorating a context with config maps.
+ // +optional
+ configStore reconciler.ConfigStore
+
+ // reconciler is the implementation of the business logic of the resource.
+ reconciler Interface
+
+ // finalizerName is the name of the finalizer to reconcile.
+ finalizerName string
+
+ // skipStatusUpdates configures whether or not this reconciler automatically updates
+ // the status of the reconciled resource.
+ skipStatusUpdates bool
+}
+
+// Check that our Reconciler implements controller.Reconciler.
+var _ controller.Reconciler = (*reconcilerImpl)(nil)
+
+// Check that our generated Reconciler is always LeaderAware.
+var _ reconciler.LeaderAware = (*reconcilerImpl)(nil)
+
+func NewReconciler(ctx context.Context, logger *zap.SugaredLogger, client versioned.Interface, lister sourcesv1alpha1.HorizonSourceLister, recorder record.EventRecorder, r Interface, options ...controller.Options) controller.Reconciler {
+ // Check the options function input. It should be 0 or 1.
+ if len(options) > 1 {
+ logger.Fatal("Up to one options struct is supported, found: ", len(options))
+ }
+
+ // Fail fast when users inadvertently implement the other LeaderAware interface.
+ // For the typed reconcilers, Promote shouldn't take any arguments.
+ if _, ok := r.(reconciler.LeaderAware); ok {
+ logger.Fatalf("%T implements the incorrect LeaderAware interface. Promote() should not take an argument as genreconciler handles the enqueuing automatically.", r)
+ }
+
+ rec := &reconcilerImpl{
+ LeaderAwareFuncs: reconciler.LeaderAwareFuncs{
+ PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
+ all, err := lister.List(labels.Everything())
+ if err != nil {
+ return err
+ }
+ for _, elt := range all {
+ // TODO: Consider letting users specify a filter in options.
+ enq(bkt, types.NamespacedName{
+ Namespace: elt.GetNamespace(),
+ Name: elt.GetName(),
+ })
+ }
+ return nil
+ },
+ },
+ Client: client,
+ Lister: lister,
+ Recorder: recorder,
+ reconciler: r,
+ finalizerName: defaultFinalizerName,
+ }
+
+ for _, opts := range options {
+ if opts.ConfigStore != nil {
+ rec.configStore = opts.ConfigStore
+ }
+ if opts.FinalizerName != "" {
+ rec.finalizerName = opts.FinalizerName
+ }
+ if opts.SkipStatusUpdates {
+ rec.skipStatusUpdates = true
+ }
+ if opts.DemoteFunc != nil {
+ rec.DemoteFunc = opts.DemoteFunc
+ }
+ }
+
+ return rec
+}
+
+// Reconcile implements controller.Reconciler
+func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error {
+ logger := logging.FromContext(ctx)
+
+ // Initialize the reconciler state. This will convert the namespace/name
+ // string into a distinct namespace and name, determine if this instance of
+ // the reconciler is the leader, and any additional interfaces implemented
+ // by the reconciler. Returns an error is the resource key is invalid.
+ s, err := newState(key, r)
+ if err != nil {
+ logger.Error("Invalid resource key: ", key)
+ return nil
+ }
+
+ // If we are not the leader, and we don't implement either ReadOnly
+ // observer interfaces, then take a fast-path out.
+ if s.isNotLeaderNorObserver() {
+ return controller.NewSkipKey(key)
+ }
+
+ // If configStore is set, attach the frozen configuration to the context.
+ if r.configStore != nil {
+ ctx = r.configStore.ToContext(ctx)
+ }
+
+ // Add the recorder to context.
+ ctx = controller.WithEventRecorder(ctx, r.Recorder)
+
+ // Get the resource with this namespace/name.
+
+ getter := r.Lister.HorizonSources(s.namespace)
+
+ original, err := getter.Get(s.name)
+
+ if errors.IsNotFound(err) {
+ // The resource may no longer exist, in which case we stop processing and call
+ // the ObserveDeletion handler if appropriate.
+ logger.Debugf("Resource %q no longer exists", key)
+ if del, ok := r.reconciler.(reconciler.OnDeletionInterface); ok {
+ return del.ObserveDeletion(ctx, types.NamespacedName{
+ Namespace: s.namespace,
+ Name: s.name,
+ })
+ }
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ // Don't modify the informers copy.
+ resource := original.DeepCopy()
+
+ var reconcileEvent reconciler.Event
+
+ name, do := s.reconcileMethodFor(resource)
+ // Append the target method to the logger.
+ logger = logger.With(zap.String("targetMethod", name))
+ switch name {
+ case reconciler.DoReconcileKind:
+ // Set and update the finalizer on resource if r.reconciler
+ // implements Finalizer.
+ if resource, err = r.setFinalizerIfFinalizer(ctx, resource); err != nil {
+ return fmt.Errorf("failed to set finalizers: %w", err)
+ }
+
+ if !r.skipStatusUpdates {
+ reconciler.PreProcessReconcile(ctx, resource)
+ }
+
+ // Reconcile this copy of the resource and then write back any status
+ // updates regardless of whether the reconciliation errored out.
+ reconcileEvent = do(ctx, resource)
+
+ if !r.skipStatusUpdates {
+ reconciler.PostProcessReconcile(ctx, resource, original)
+ }
+
+ case reconciler.DoFinalizeKind:
+ // For finalizing reconcilers, if this resource being marked for deletion
+ // and reconciled cleanly (nil or normal event), remove the finalizer.
+ reconcileEvent = do(ctx, resource)
+
+ if resource, err = r.clearFinalizer(ctx, resource, reconcileEvent); err != nil {
+ return fmt.Errorf("failed to clear finalizers: %w", err)
+ }
+
+ case reconciler.DoObserveKind:
+ // Observe any changes to this resource, since we are not the leader.
+ reconcileEvent = do(ctx, resource)
+
+ }
+
+ // Synchronize the status.
+ switch {
+ case r.skipStatusUpdates:
+ // This reconciler implementation is configured to skip resource updates.
+ // This may mean this reconciler does not observe spec, but reconciles external changes.
+ case equality.Semantic.DeepEqual(original.Status, resource.Status):
+ // If we didn't change anything then don't call updateStatus.
+ // This is important because the copy we loaded from the injectionInformer's
+ // cache may be stale and we don't want to overwrite a prior update
+ // to status with this stale state.
+ case !s.isLeader:
+ // High-availability reconcilers may have many replicas watching the resource, but only
+ // the elected leader is expected to write modifications.
+ logger.Warn("Saw status changes when we aren't the leader!")
+ default:
+ if err = r.updateStatus(ctx, original, resource); err != nil {
+ logger.Warnw("Failed to update resource status", zap.Error(err))
+ r.Recorder.Eventf(resource, v1.EventTypeWarning, "UpdateFailed",
+ "Failed to update status for %q: %v", resource.Name, err)
+ return err
+ }
+ }
+
+ // Report the reconciler event, if any.
+ if reconcileEvent != nil {
+ var event *reconciler.ReconcilerEvent
+ if reconciler.EventAs(reconcileEvent, &event) {
+ logger.Infow("Returned an event", zap.Any("event", reconcileEvent))
+ r.Recorder.Event(resource, event.EventType, event.Reason, event.Error())
+
+ // the event was wrapped inside an error, consider the reconciliation as failed
+ if _, isEvent := reconcileEvent.(*reconciler.ReconcilerEvent); !isEvent {
+ return reconcileEvent
+ }
+ return nil
+ }
+
+ if controller.IsSkipKey(reconcileEvent) {
+ // This is a wrapped error, don't emit an event.
+ } else if ok, _ := controller.IsRequeueKey(reconcileEvent); ok {
+ // This is a wrapped error, don't emit an event.
+ } else {
+ logger.Errorw("Returned an error", zap.Error(reconcileEvent))
+ r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error())
+ }
+ return reconcileEvent
+ }
+
+ return nil
+}
+
+func (r *reconcilerImpl) updateStatus(ctx context.Context, existing *v1alpha1.HorizonSource, desired *v1alpha1.HorizonSource) error {
+ existing = existing.DeepCopy()
+ return reconciler.RetryUpdateConflicts(func(attempts int) (err error) {
+ // The first iteration tries to use the injectionInformer's state, subsequent attempts fetch the latest state via API.
+ if attempts > 0 {
+
+ getter := r.Client.SourcesV1alpha1().HorizonSources(desired.Namespace)
+
+ existing, err = getter.Get(ctx, desired.Name, metav1.GetOptions{})
+ if err != nil {
+ return err
+ }
+ }
+
+ // If there's nothing to update, just return.
+ if equality.Semantic.DeepEqual(existing.Status, desired.Status) {
+ return nil
+ }
+
+ if diff, err := kmp.SafeDiff(existing.Status, desired.Status); err == nil && diff != "" {
+ logging.FromContext(ctx).Debug("Updating status with: ", diff)
+ }
+
+ existing.Status = desired.Status
+
+ updater := r.Client.SourcesV1alpha1().HorizonSources(existing.Namespace)
+
+ _, err = updater.UpdateStatus(ctx, existing, metav1.UpdateOptions{})
+ return err
+ })
+}
+
+// updateFinalizersFiltered will update the Finalizers of the resource.
+// TODO: this method could be generic and sync all finalizers. For now it only
+// updates defaultFinalizerName or its override.
+func (r *reconcilerImpl) updateFinalizersFiltered(ctx context.Context, resource *v1alpha1.HorizonSource) (*v1alpha1.HorizonSource, error) {
+
+ getter := r.Lister.HorizonSources(resource.Namespace)
+
+ actual, err := getter.Get(resource.Name)
+ if err != nil {
+ return resource, err
+ }
+
+ // Don't modify the informers copy.
+ existing := actual.DeepCopy()
+
+ var finalizers []string
+
+ // If there's nothing to update, just return.
+ existingFinalizers := sets.NewString(existing.Finalizers...)
+ desiredFinalizers := sets.NewString(resource.Finalizers...)
+
+ if desiredFinalizers.Has(r.finalizerName) {
+ if existingFinalizers.Has(r.finalizerName) {
+ // Nothing to do.
+ return resource, nil
+ }
+ // Add the finalizer.
+ finalizers = append(existing.Finalizers, r.finalizerName)
+ } else {
+ if !existingFinalizers.Has(r.finalizerName) {
+ // Nothing to do.
+ return resource, nil
+ }
+ // Remove the finalizer.
+ existingFinalizers.Delete(r.finalizerName)
+ finalizers = existingFinalizers.List()
+ }
+
+ mergePatch := map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "finalizers": finalizers,
+ "resourceVersion": existing.ResourceVersion,
+ },
+ }
+
+ patch, err := json.Marshal(mergePatch)
+ if err != nil {
+ return resource, err
+ }
+
+ patcher := r.Client.SourcesV1alpha1().HorizonSources(resource.Namespace)
+
+ resourceName := resource.Name
+ updated, err := patcher.Patch(ctx, resourceName, types.MergePatchType, patch, metav1.PatchOptions{})
+ if err != nil {
+ r.Recorder.Eventf(existing, v1.EventTypeWarning, "FinalizerUpdateFailed",
+ "Failed to update finalizers for %q: %v", resourceName, err)
+ } else {
+ r.Recorder.Eventf(updated, v1.EventTypeNormal, "FinalizerUpdate",
+ "Updated %q finalizers", resource.GetName())
+ }
+ return updated, err
+}
+
+func (r *reconcilerImpl) setFinalizerIfFinalizer(ctx context.Context, resource *v1alpha1.HorizonSource) (*v1alpha1.HorizonSource, error) {
+ if _, ok := r.reconciler.(Finalizer); !ok {
+ return resource, nil
+ }
+
+ finalizers := sets.NewString(resource.Finalizers...)
+
+ // If this resource is not being deleted, mark the finalizer.
+ if resource.GetDeletionTimestamp().IsZero() {
+ finalizers.Insert(r.finalizerName)
+ }
+
+ resource.Finalizers = finalizers.List()
+
+ // Synchronize the finalizers filtered by r.finalizerName.
+ return r.updateFinalizersFiltered(ctx, resource)
+}
+
+func (r *reconcilerImpl) clearFinalizer(ctx context.Context, resource *v1alpha1.HorizonSource, reconcileEvent reconciler.Event) (*v1alpha1.HorizonSource, error) {
+ if _, ok := r.reconciler.(Finalizer); !ok {
+ return resource, nil
+ }
+ if resource.GetDeletionTimestamp().IsZero() {
+ return resource, nil
+ }
+
+ finalizers := sets.NewString(resource.Finalizers...)
+
+ if reconcileEvent != nil {
+ var event *reconciler.ReconcilerEvent
+ if reconciler.EventAs(reconcileEvent, &event) {
+ if event.EventType == v1.EventTypeNormal {
+ finalizers.Delete(r.finalizerName)
+ }
+ }
+ } else {
+ finalizers.Delete(r.finalizerName)
+ }
+
+ resource.Finalizers = finalizers.List()
+
+ // Synchronize the finalizers filtered by r.finalizerName.
+ return r.updateFinalizersFiltered(ctx, resource)
+}
diff --git a/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/state.go b/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/state.go
new file mode 100644
index 000000000..6baf1438c
--- /dev/null
+++ b/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/state.go
@@ -0,0 +1,86 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package horizonsource
+
+import (
+ fmt "fmt"
+
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ types "k8s.io/apimachinery/pkg/types"
+ cache "k8s.io/client-go/tools/cache"
+ reconciler "knative.dev/pkg/reconciler"
+)
+
+// state is used to track the state of a reconciler in a single run.
+type state struct {
+ // key is the original reconciliation key from the queue.
+ key string
+ // namespace is the namespace split from the reconciliation key.
+ namespace string
+ // name is the name split from the reconciliation key.
+ name string
+ // reconciler is the reconciler.
+ reconciler Interface
+ // roi is the read only interface cast of the reconciler.
+ roi ReadOnlyInterface
+ // isROI (Read Only Interface) the reconciler only observes reconciliation.
+ isROI bool
+ // isLeader the instance of the reconciler is the elected leader.
+ isLeader bool
+}
+
+func newState(key string, r *reconcilerImpl) (*state, error) {
+ // Convert the namespace/name string into a distinct namespace and name.
+ namespace, name, err := cache.SplitMetaNamespaceKey(key)
+ if err != nil {
+ return nil, fmt.Errorf("invalid resource key: %s", key)
+ }
+
+ roi, isROI := r.reconciler.(ReadOnlyInterface)
+
+ isLeader := r.IsLeaderFor(types.NamespacedName{
+ Namespace: namespace,
+ Name: name,
+ })
+
+ return &state{
+ key: key,
+ namespace: namespace,
+ name: name,
+ reconciler: r.reconciler,
+ roi: roi,
+ isROI: isROI,
+ isLeader: isLeader,
+ }, nil
+}
+
+// isNotLeaderNorObserver checks to see if this reconciler with the current
+// state is enabled to do any work or not.
+// isNotLeaderNorObserver returns true when there is no work possible for the
+// reconciler.
+func (s *state) isNotLeaderNorObserver() bool {
+ if !s.isLeader && !s.isROI {
+ // If we are not the leader, and we don't implement the ReadOnly
+ // interface, then take a fast-path out.
+ return true
+ }
+ return false
+}
+
+func (s *state) reconcileMethodFor(o *v1alpha1.HorizonSource) (string, doReconcile) {
+ if o.GetDeletionTimestamp().IsZero() {
+ if s.isLeader {
+ return reconciler.DoReconcileKind, s.reconciler.ReconcileKind
+ } else if s.isROI {
+ return reconciler.DoObserveKind, s.roi.ObserveKind
+ }
+ } else if fin, ok := s.reconciler.(Finalizer); s.isLeader && ok {
+ return reconciler.DoFinalizeKind, fin.FinalizeKind
+ }
+ return "unknown", nil
+}
diff --git a/pkg/client/listers/sources/v1alpha1/expansion_generated.go b/pkg/client/listers/sources/v1alpha1/expansion_generated.go
index 4a9b37643..587641d53 100644
--- a/pkg/client/listers/sources/v1alpha1/expansion_generated.go
+++ b/pkg/client/listers/sources/v1alpha1/expansion_generated.go
@@ -7,6 +7,14 @@ SPDX-License-Identifier: Apache-2.0
package v1alpha1
+// HorizonSourceListerExpansion allows custom methods to be added to
+// HorizonSourceLister.
+type HorizonSourceListerExpansion interface{}
+
+// HorizonSourceNamespaceListerExpansion allows custom methods to be added to
+// HorizonSourceNamespaceLister.
+type HorizonSourceNamespaceListerExpansion interface{}
+
// VSphereBindingListerExpansion allows custom methods to be added to
// VSphereBindingLister.
type VSphereBindingListerExpansion interface{}
diff --git a/pkg/client/listers/sources/v1alpha1/horizonsource.go b/pkg/client/listers/sources/v1alpha1/horizonsource.go
new file mode 100644
index 000000000..0ebec6a39
--- /dev/null
+++ b/pkg/client/listers/sources/v1alpha1/horizonsource.go
@@ -0,0 +1,88 @@
+/*
+Copyright 2020 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ v1alpha1 "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/client-go/tools/cache"
+)
+
+// HorizonSourceLister helps list HorizonSources.
+// All objects returned here must be treated as read-only.
+type HorizonSourceLister interface {
+ // List lists all HorizonSources in the indexer.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*v1alpha1.HorizonSource, err error)
+ // HorizonSources returns an object that can list and get HorizonSources.
+ HorizonSources(namespace string) HorizonSourceNamespaceLister
+ HorizonSourceListerExpansion
+}
+
+// horizonSourceLister implements the HorizonSourceLister interface.
+type horizonSourceLister struct {
+ indexer cache.Indexer
+}
+
+// NewHorizonSourceLister returns a new HorizonSourceLister.
+func NewHorizonSourceLister(indexer cache.Indexer) HorizonSourceLister {
+ return &horizonSourceLister{indexer: indexer}
+}
+
+// List lists all HorizonSources in the indexer.
+func (s *horizonSourceLister) List(selector labels.Selector) (ret []*v1alpha1.HorizonSource, err error) {
+ err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+ ret = append(ret, m.(*v1alpha1.HorizonSource))
+ })
+ return ret, err
+}
+
+// HorizonSources returns an object that can list and get HorizonSources.
+func (s *horizonSourceLister) HorizonSources(namespace string) HorizonSourceNamespaceLister {
+ return horizonSourceNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// HorizonSourceNamespaceLister helps list and get HorizonSources.
+// All objects returned here must be treated as read-only.
+type HorizonSourceNamespaceLister interface {
+ // List lists all HorizonSources in the indexer for a given namespace.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*v1alpha1.HorizonSource, err error)
+ // Get retrieves the HorizonSource from the indexer for a given namespace and name.
+ // Objects returned here must be treated as read-only.
+ Get(name string) (*v1alpha1.HorizonSource, error)
+ HorizonSourceNamespaceListerExpansion
+}
+
+// horizonSourceNamespaceLister implements the HorizonSourceNamespaceLister
+// interface.
+type horizonSourceNamespaceLister struct {
+ indexer cache.Indexer
+ namespace string
+}
+
+// List lists all HorizonSources in the indexer for a given namespace.
+func (s horizonSourceNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.HorizonSource, err error) {
+ err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+ ret = append(ret, m.(*v1alpha1.HorizonSource))
+ })
+ return ret, err
+}
+
+// Get retrieves the HorizonSource from the indexer for a given namespace and name.
+func (s horizonSourceNamespaceLister) Get(name string) (*v1alpha1.HorizonSource, error) {
+ obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+ if err != nil {
+ return nil, err
+ }
+ if !exists {
+ return nil, errors.NewNotFound(v1alpha1.Resource("horizonsource"), name)
+ }
+ return obj.(*v1alpha1.HorizonSource), nil
+}
diff --git a/pkg/horizon/adapter.go b/pkg/horizon/adapter.go
new file mode 100644
index 000000000..1146c3f00
--- /dev/null
+++ b/pkg/horizon/adapter.go
@@ -0,0 +1,269 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizon
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ cloudevents "github.com/cloudevents/sdk-go/v2"
+ "github.com/jpillora/backoff"
+ "github.com/kelseyhightower/envconfig"
+ "go.uber.org/zap"
+ "knative.dev/eventing/pkg/adapter/v2"
+ "knative.dev/pkg/logging"
+)
+
+const (
+ defaultPollInterval = time.Second
+ eventTypeFormat = "com.vmware.horizon.%s.v0"
+
+ retryBackoff = time.Second
+ retryMaxTries = 5
+)
+
+type envConfig struct {
+ // Include the standard adapter.EnvConfig used by all adapters.
+ adapter.EnvConfig
+
+ // Horizon settings
+ Address string `envconfig:"HORIZON_URL" required:"true"`
+ Insecure bool `envconfig:"HORIZON_INSECURE" default:"false"`
+ // overwrite useful for local development
+ SecretPath string `envconfig:"HORIZON_SECRET_PATH" default:""`
+}
+
+func NewEnv() adapter.EnvConfigAccessor { return &envConfig{} }
+
+// Adapter reads events from the VMware Horizon API
+type Adapter struct {
+ client cloudevents.Client
+
+ source string
+ sink string
+ hclient Client
+ clock clock.Clock
+ pollInterval time.Duration
+}
+
+func NewAdapter(ctx context.Context, _ adapter.EnvConfigAccessor, ceClient cloudevents.Client) adapter.Adapter {
+ logger := logging.FromContext(ctx)
+
+ var env envConfig
+ if err := envconfig.Process("", &env); err != nil {
+ logger.Fatalw("process environment variables", zap.Error(err))
+ }
+
+ hc, err := newHorizonClient(ctx)
+ if err != nil {
+ logger.Fatalw("create horizon client", zap.Error(err))
+ }
+
+ return &Adapter{
+ client: ceClient,
+ source: env.Address,
+ sink: env.GetSink(),
+ hclient: hc,
+ clock: clock.New(),
+ pollInterval: defaultPollInterval,
+ }
+}
+
+// Start runs the adapter. Returns if ctx is cancelled or on unrecoverable
+// error, e.g. reading or sending events.
+func (a *Adapter) Start(ctx context.Context) error {
+ return a.run(ctx)
+}
+
+// run starts polling the Horizon event API until the specified context is
+// cancelled or when an error is returned while retrieving Horizon events
+func (a *Adapter) run(ctx context.Context) error {
+ var (
+ lastEvent *AuditEventSummary
+ since Timestamp
+ )
+
+ logger := logging.FromContext(ctx).With(
+ zap.String("source", a.source),
+ zap.Duration("pollIntervalSeconds", a.pollInterval),
+ )
+ logger.Infow("starting horizon source adapter")
+
+ ticker := a.clock.Ticker(a.pollInterval)
+ defer func() {
+ ticker.Stop()
+
+ if err := a.hclient.Logout(context.Background()); err != nil {
+ logger.Warn("could not logout from Horizon API", zap.Error(err))
+ }
+ }()
+
+ backoffCfg := backoff.Backoff{
+ Factor: 2,
+ Jitter: false,
+ Min: retryBackoff,
+ Max: retryMaxTries * time.Second,
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ logger.Infof("stopping event stream")
+ return ctx.Err()
+
+ case <-ticker.C:
+ if lastEvent != nil {
+ since = Timestamp(lastEvent.Time)
+ }
+
+ if since == 0 {
+ logger.Debug("retrieving initial set of events")
+ } else {
+ logger.Debugw("retrieving events with time range filter",
+ zap.Any("sinceUnixMilli", since),
+ zap.String("sinceConverted", time.Unix(int64(since/1000), 0).String()),
+ )
+ }
+
+ events, err := a.hclient.GetEvents(ctx, since)
+ if err != nil {
+ return fmt.Errorf("get events: %w", err)
+ }
+
+ skip := false
+ switch len(events) {
+ case 0:
+ skip = true
+ case 1: // check if this is lastEvent we have already seen
+ if lastEvent != nil && events[0].ID == lastEvent.ID {
+ skip = true
+ }
+ }
+
+ if skip {
+ sleep := backoffCfg.Duration()
+ logger.Debugw("backing off retrieving events: no new events received", zap.Duration("backoffSeconds", sleep))
+ time.Sleep(sleep)
+ continue
+ }
+
+ logger.Debugw("retrieved new events", zap.Int("count", len(events)))
+ events = removeEvent(events, lastEvent)
+ logger.Debugw("remaining new events after filtering out duplicate events", zap.Int("count", len(events)))
+ lastEvent = a.sendEvents(ctx, events)
+ backoffCfg.Reset()
+ }
+ }
+}
+
+// sendEvents sends the given events to the configured SINK returning the last
+// successfully sent event
+//
+// TODO (@mgasch): There is a risk of poison pill issue here, leading to a constant loop
+// in the invoking function.
+func (a *Adapter) sendEvents(ctx context.Context, events []AuditEventSummary) *AuditEventSummary {
+ logger := logging.FromContext(ctx).With(
+ zap.String("source", a.source),
+ zap.String("sink", a.sink),
+ )
+
+ // Horizon events are returned in descending time order thus the "id" in a
+ // Horizon event can not be used for ordering (see testdata for example with
+ // concurrent timestamps)
+ reverse(events)
+
+ // last successful processed event to track time offset in stream
+ var lastEvent *AuditEventSummary
+
+ ctx = cloudevents.ContextWithRetriesExponentialBackoff(ctx, retryBackoff, retryMaxTries)
+ for i := range events {
+ event := events[i]
+ // don't waste cycles when ctx canceled
+ if ctx.Err() != nil {
+ return lastEvent
+ }
+
+ log := logger.With(zap.Any("event", event))
+ ce, err := toCloudEvent(event, a.source)
+ if err != nil {
+ log.Errorw("skipping event because it could not be converted to cloudevent", zap.Error(err))
+ continue
+ }
+
+ // TODO: better partial batch failure handling here?
+ result := a.client.Send(ctx, ce)
+ if !cloudevents.IsACK(result) {
+ log.Errorw("could not send cloudevent", zap.Error(result))
+ continue
+ }
+ log.Debugw("successfully sent event")
+ lastEvent = &event
+ }
+
+ return lastEvent
+}
+
+// reverse mutates the given slice and reverses its order
+func reverse(ev []AuditEventSummary) {
+ for i := len(ev)/2 - 1; i >= 0; i-- {
+ opp := len(ev) - 1 - i
+ ev[i], ev[opp] = ev[opp], ev[i]
+ }
+}
+
+// removeEvent returns a copy of list with the given event removed
+func removeEvent(list []AuditEventSummary, event *AuditEventSummary) []AuditEventSummary {
+ deduped := make([]AuditEventSummary, len(list))
+ copy(deduped, list)
+
+ if event == nil {
+ return deduped
+ }
+
+ for i := range list {
+ if list[i].ID == event.ID {
+ // Remove the element at index i from a.
+ copy(deduped[i:], deduped[i+1:]) // shift deduped[i+1:] left one index.
+ deduped[len(deduped)-1] = AuditEventSummary{} // erase last element (write zero value).
+ deduped = deduped[:len(deduped)-1] // truncate slice.
+ }
+ }
+ return deduped
+}
+
+func toCloudEvent(horizonEvent AuditEventSummary, source string) (cloudevents.Event, error) {
+ ce := cloudevents.NewEvent()
+
+ // TODO: revisit CE properties used here
+ id := strconv.Itoa(int(horizonEvent.ID))
+ ce.SetID(id)
+ ce.SetSource(source)
+ ce.SetType(convertEventType(horizonEvent.Type))
+ t := time.Unix(horizonEvent.Time/1000, 0) // time is converted from ms
+ ce.SetTime(t)
+
+ if err := ce.SetData(cloudevents.ApplicationJSON, horizonEvent); err != nil {
+ return cloudevents.Event{}, fmt.Errorf("set cloudevent data: %w", err)
+ }
+
+ if err := ce.Validate(); err != nil {
+ return cloudevents.Event{}, fmt.Errorf("validation for cloudevent failed: %w", err)
+ }
+
+ return ce, nil
+}
+
+// convertEventType converts a Horizon event type to a normalized cloud event
+// type. For example, VLSI_USERLOGGEDIN is converted to
+// com.vmware.horizon.vlsi_userloggedin.v0
+func convertEventType(t string) string {
+ t = strings.ToLower(t)
+ return fmt.Sprintf(eventTypeFormat, t)
+}
diff --git a/pkg/horizon/adapter_test.go b/pkg/horizon/adapter_test.go
new file mode 100644
index 000000000..fce1d8525
--- /dev/null
+++ b/pkg/horizon/adapter_test.go
@@ -0,0 +1,225 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizon
+
+import (
+ "context"
+ "encoding/json"
+ "net"
+ "os"
+ "os/exec"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ ce "github.com/cloudevents/sdk-go/v2"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap/zaptest"
+ "knative.dev/eventing/pkg/adapter/v2"
+ "knative.dev/pkg/logging"
+)
+
+const (
+ testEvents = "./testdata/audit_events.golden"
+)
+
+func TestAdapter(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+
+ ctx = logging.WithLogger(ctx, zaptest.NewLogger(t).Sugar())
+
+ receiver := newSink(t, ctx)
+ tr, err := ce.NewHTTP(ce.WithTarget(receiver.URL()))
+ require.NoError(t, err)
+
+ ceClient, err := ce.NewClient(tr)
+ require.NoError(t, err)
+
+ f, err := os.Open(testEvents)
+ require.NoErrorf(t, err, "open golden file: %s", testEvents)
+
+ var events []AuditEventSummary
+ dec := json.NewDecoder(f)
+ err = dec.Decode(&events)
+ require.NoError(t, err, "JSON decode test events")
+
+ a := &Adapter{
+ client: ceClient,
+ source: "http://api.horizon.corp.local",
+ sink: receiver.URL(),
+ hclient: &horizonMockClient{events: events},
+ clock: clock.New(),
+ pollInterval: time.Millisecond * 100,
+ }
+
+ var wg sync.WaitGroup
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ err = a.Start(ctx)
+ require.ErrorIs(t, err, context.DeadlineExceeded)
+ }()
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ var (
+ expect = len(events)
+
+ counter int
+ lastTimestamp time.Time
+ )
+
+ for counter != expect {
+ e := <-receiver.receiveChan
+ // verify timestamps are ascending
+ if lastTimestamp.IsZero() {
+ lastTimestamp = e.Time()
+ }
+
+ require.GreaterOrEqual(t, e.Time().UnixMilli(), lastTimestamp.UnixMilli())
+ counter++
+ }
+ }()
+
+ wg.Wait()
+}
+
+func TestAdapterMain(t *testing.T) {
+ // Use the test executable to simulate the cmd/adapter process if
+ // environment var t.Name() is set to "main"
+ // (see https://talks.golang.org/2014/testing.slide#23)
+ if os.Getenv(t.Name()) == "main" {
+ adapter.Main("horizon-source", NewEnv, NewAdapter)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ receiver := newSink(t, ctx)
+
+ // Run a simulated adapter main using the test executable.
+ cmd := exec.CommandContext(ctx, os.Args[0], "-test.run="+t.Name())
+ cmd.Env = append(os.Environ(),
+ t.Name()+"=main",
+ "K_SINK="+receiver.URL(),
+ "INTERVAL="+"1ms",
+ "NAMESPACE=namespace",
+ "NAME=name",
+ `K_METRICS_CONFIG={"domain":"x", "component":"x", "prometheusport":0, "configmap":{}}`,
+ `K_LOGGING_CONFIG={}`,
+ )
+ err := cmd.Start()
+ if err != nil {
+ t.Error(err)
+ }
+ defer func() {
+ if err = cmd.Wait(); err != nil {
+ t.Logf("wait returned with error: %v", err)
+ }
+ }()
+}
+
+func Test_removeItem(t *testing.T) {
+ t.Run("event is nil", func(t *testing.T) {
+ ev := createFakeEvents(10)
+ got := removeEvent(ev, nil)
+ require.Equal(t, ev, got)
+ })
+
+ t.Run("empty events", func(t *testing.T) {
+ got := removeEvent([]AuditEventSummary{}, &AuditEventSummary{})
+ require.Equal(t, []AuditEventSummary{}, got)
+ })
+
+ t.Run("one duplicate event", func(t *testing.T) {
+ ev := createFakeEvents(3)
+ got := removeEvent(ev, &AuditEventSummary{ID: 10})
+ require.Equal(t, ev[1:], got)
+ })
+}
+
+// createFakeEvents creates returns a []AuditEventSummary where the ID of each
+// element is set to 10 + current counter
+func createFakeEvents(count int) []AuditEventSummary {
+ events := make([]AuditEventSummary, count)
+ for i := 0; i < count; i++ {
+ events[i] = AuditEventSummary{ID: int64(10 + i)}
+ }
+
+ return events
+}
+
+type sink struct {
+ listener net.Listener
+ client ce.Client
+ proto *ce.HTTPProtocol
+ receiveChan chan ce.Event
+}
+
+func newSink(t *testing.T, ctx context.Context) *sink {
+ s := &sink{receiveChan: make(chan ce.Event)}
+
+ var err error
+ s.listener, err = net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+
+ s.proto, err = ce.NewHTTP(ce.WithListener(s.listener))
+ require.NoError(t, err)
+
+ s.client, err = ce.NewClient(s.proto)
+ require.NoError(t, err)
+
+ go func() {
+ err = s.client.StartReceiver(ctx, func(ctx context.Context, e ce.Event) ce.Result {
+ select {
+ case s.receiveChan <- e:
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ })
+ require.NoError(t, err)
+ close(s.receiveChan)
+ }()
+
+ return s
+}
+
+func (s *sink) URL() string { return "http://" + s.listener.Addr().String() }
+
+type horizonMockClient struct {
+ invocations int
+ events []AuditEventSummary // 10 items in golden file
+}
+
+func (h *horizonMockClient) GetEvents(ctx context.Context, since Timestamp) ([]AuditEventSummary, error) {
+ h.invocations++
+
+ // Horizon API returns events ordered from newest to oldest
+ // note: concurrent events (by time) are not ordered by id (see golden file for example)
+ switch h.invocations {
+ case 1:
+ // first half
+ return h.events[5:], nil
+ case 2:
+ return h.events[2:5], nil
+ case 3:
+ // two most recent events
+ return h.events[0:2], nil
+ default:
+ // always return newest event, triggers backoff
+ return h.events[0:1], nil
+ }
+}
+
+func (h *horizonMockClient) Logout(ctx context.Context) error {
+ return nil
+}
diff --git a/pkg/horizon/horizon.go b/pkg/horizon/horizon.go
new file mode 100644
index 000000000..f76f88532
--- /dev/null
+++ b/pkg/horizon/horizon.go
@@ -0,0 +1,355 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizon
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "path/filepath"
+ "time"
+
+ cloudevents "github.com/cloudevents/sdk-go/v2"
+ "github.com/go-resty/resty/v2"
+ "github.com/kelseyhightower/envconfig"
+ "github.com/pkg/errors"
+ "go.uber.org/zap"
+ corev1 "k8s.io/api/core/v1"
+ "knative.dev/pkg/logging"
+)
+
+const (
+ // domainSecretKey is the key (filename) of the projected secret containing the
+ // Horizon Active Directory Domain to use
+ domainSecretKey = "domain"
+
+ // DefaultSecretMountPath is the default mount path of the Kubernetes Secret
+ // containing Horizon credentials
+ //nolint:gosec
+ DefaultSecretMountPath = "/var/bindings/horizon" // filepath.Join isn't const.
+
+ // HTTP client
+ defaultTimeout = time.Second * 5
+ defaultRetries = 3
+
+ // Horizon API
+ loginPath = "/rest/login"
+ logoutPath = "/rest/logout"
+ refreshPath = "/rest/refresh"
+ eventsPath = "/rest/external/v1/audit-events"
+)
+
+var errTokenExpired = errors.New("refresh token expired")
+
+// Client gets events from the configured Horizon API REST server
+type Client interface {
+ GetEvents(ctx context.Context, since Timestamp) ([]AuditEventSummary, error)
+ Logout(ctx context.Context) error
+}
+
+type horizonClient struct {
+ client *resty.Client
+ credentials AuthLoginRequest
+ tokens AuthTokens
+ logger *zap.SugaredLogger
+}
+
+var _ Client = (*horizonClient)(nil)
+
+// readSecretKey reads the key from a Kubernetes secret
+func readSecretKey(key string) (string, error) {
+ var env envConfig
+ if err := envconfig.Process("", &env); err != nil {
+ return "", fmt.Errorf("process environment variables: %w", err)
+ }
+
+ mountPath := DefaultSecretMountPath
+ if env.SecretPath != "" {
+ mountPath = env.SecretPath
+ }
+
+ data, err := ioutil.ReadFile(filepath.Join(mountPath, key))
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+func newHorizonClient(ctx context.Context) (*horizonClient, error) {
+ user, err := readSecretKey(corev1.BasicAuthUsernameKey)
+ if err != nil {
+ return nil, fmt.Errorf("read secret key %q: %w", corev1.BasicAuthUsernameKey, err)
+ }
+
+ pass, err := readSecretKey(corev1.BasicAuthPasswordKey)
+ if err != nil {
+ return nil, fmt.Errorf("read secret key %q: %w", corev1.BasicAuthPasswordKey, err)
+ }
+
+ domain, err := readSecretKey(domainSecretKey)
+ if err != nil {
+ return nil, fmt.Errorf("read secret key %q: %w", domainSecretKey, err)
+ }
+
+ creds := AuthLoginRequest{
+ Domain: domain,
+ Username: user,
+ Password: pass,
+ }
+
+ emptyCredentials := func() bool {
+ if creds.Domain == "" || creds.Username == "" || creds.Password == "" {
+ return true
+ }
+ return false
+ }
+
+ if emptyCredentials() {
+ return nil, fmt.Errorf("invalid credentials: domain, username and password must be set")
+ }
+
+ var env envConfig
+ if err = envconfig.Process("", &env); err != nil {
+ return nil, fmt.Errorf("process environment variables: %w", err)
+ }
+
+ rc := newRESTClient(ctx, env.Address, env.Insecure)
+ c := horizonClient{
+ client: rc,
+ logger: logging.FromContext(ctx),
+ credentials: creds,
+ }
+
+ if env.Insecure {
+ c.logger.Warnw("using potentially insecure connection to Horizon API server", "address", env.Address, "insecure", env.Insecure)
+ }
+
+ c.logger.Debug("authenticating against Horizon API")
+ if err = c.login(ctx); err != nil {
+ return nil, fmt.Errorf("horizon API login failure: %w", err)
+ }
+
+ return &c, nil
+}
+
+func newRESTClient(ctx context.Context, server string, insecure bool) *resty.Client {
+ // REST global client defaults
+ r := resty.New().SetLogger(logging.FromContext(ctx))
+ r.SetBaseURL(server)
+ r.SetHeader("content-type", cloudevents.ApplicationJSON)
+ r.SetAuthScheme("Bearer")
+ r.SetRetryCount(defaultRetries).SetRetryMaxWaitTime(defaultTimeout)
+ //nolint:gosec
+ r.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: insecure})
+
+ return r
+}
+
+// login performs an authentication request to the Horizon API server, sets and
+// stores the returned auth and refresh tokens
+func (h *horizonClient) login(ctx context.Context) error {
+ /* Access tokens would be valid for 30 minutes while the refresh token would be
+ valid for 8 hours. Once the access token has expired, the user will get a 401
+ response from the APIs and would need to get a new access token from the refresh
+ endpoint using the refresh token. If the Refresh token is also expired (after 8
+ hours and when user gets a 400), it indicates that user needs to fully
+ re-authenticate using login endpoint due to invalid refresh token.
+ */
+
+ // check if we can use an existing refresh token
+ if h.tokens.RefreshToken != "" {
+ err := h.refresh(ctx)
+
+ // success
+ if err == nil {
+ return nil
+ }
+
+ if !errors.Is(err, errTokenExpired) {
+ return fmt.Errorf("refresh token: %w", err)
+ }
+ }
+
+ // perform full login
+ res, err := h.client.R().SetContext(ctx).SetBody(h.credentials).Post(loginPath)
+ if err != nil {
+ return err
+ }
+
+ if !res.IsSuccess() {
+ return fmt.Errorf("horizon API login returned non-success status code: %d", res.StatusCode())
+ }
+
+ var tokens AuthTokens
+ err = json.Unmarshal(res.Body(), &tokens)
+ if err != nil {
+ return fmt.Errorf("unmarshal JSON authentication token response: %w", err)
+ }
+
+ h.tokens = tokens
+ h.client.SetAuthToken(h.tokens.AccessToken)
+ h.logger.Debug("Horizon API login successful")
+
+ return nil
+}
+
+// refresh attempts to refresh an expired auth token. If the refresh token has
+// expired, errTokenExpired will be returned.
+func (h *horizonClient) refresh(ctx context.Context) error {
+ request := RefreshTokenRequest{h.tokens.RefreshToken}
+ res, err := h.client.R().SetContext(ctx).SetBody(request).Post(refreshPath)
+ if err != nil {
+ return err
+ }
+
+ if !res.IsSuccess() {
+ switch res.StatusCode() {
+ case http.StatusBadRequest:
+ return errTokenExpired
+
+ default:
+ return fmt.Errorf("unexpected HTTP response: %d %s", res.StatusCode(), string(res.Body()))
+ }
+ }
+
+ var accessToken AccessToken
+ err = json.Unmarshal(res.Body(), &accessToken)
+ if err != nil {
+ return fmt.Errorf("unmarshal JSON access token response: %w", err)
+ }
+
+ token := accessToken.AccessToken
+ h.tokens.AccessToken = token
+ h.client.SetAuthToken(token)
+ h.logger.Debug("auth token refresh successful")
+ return nil
+}
+
+// GetEvents returns a list of AuditEventSummary from the Horizon API
+func (h *horizonClient) GetEvents(ctx context.Context, since Timestamp) ([]AuditEventSummary, error) {
+ var (
+ res *resty.Response
+ retries int
+ err error
+
+ timeRange string
+ params map[string]string
+ )
+
+ // handle auth expired cases
+ for retries < 2 {
+ if since == 0 {
+ // return last (up to) 10 initial events if no timestamp is specified
+ params = map[string]string{
+ "size": "10",
+ "page": "1",
+ }
+ } else {
+ timeRange, err = timeRangeFilter(since, 0)
+ h.logger.Debugw("using time range filter", "filter", timeRange)
+ if err != nil {
+ return nil, fmt.Errorf("create time range query filter: %w", err)
+ }
+
+ params = map[string]string{
+ "filter": timeRange,
+ }
+ }
+
+ req := h.client.R().SetContext(ctx).SetQueryParams(params)
+ res, err = req.Get(eventsPath)
+ if err != nil {
+ return nil, err
+ }
+
+ h.logger.Debugw("Horizon GetEvents response headers", zap.Any("headers", res.Header()))
+ h.logger.Debugw("Horizon GetEvents response body", zap.String("body", string(res.Body())))
+
+ if !res.IsSuccess() {
+ switch res.StatusCode() {
+ // perform re-auth
+ case http.StatusUnauthorized:
+ if err = h.login(ctx); err != nil {
+ h.logger.Error(string(res.Body()))
+ return nil, fmt.Errorf("not authenticated: %w: %s", err, string(res.Body()))
+ }
+ h.logger.Debugw("retrying get events after re-authentication", zap.Int("retried", retries))
+ retries++
+ continue
+
+ // conflict (note: should never happen on GET and incorrectly used in spec for
+ // DB missing error)
+ case http.StatusConflict:
+ return nil, errors.New("HTTP conflict error: 401 (DB not initialized?)")
+
+ // not defined in spec
+ default:
+ return nil, fmt.Errorf("unexpected status code: %d %s", res.StatusCode(), string(res.Body()))
+ }
+ }
+
+ var events []AuditEventSummary
+ err = json.Unmarshal(res.Body(), &events)
+ if err != nil {
+ return nil, fmt.Errorf("unmarshal JSON audit events response: %w", err)
+ }
+
+ return events, nil
+ }
+
+ return nil, fmt.Errorf("get events status code: %d %s", res.StatusCode(), string(res.Body()))
+}
+
+// timeRangeFilter returns the JSON-encoded query string for the given timestamp
+// range. Both values are interpreted as inclusive range values. If to is 0 an
+// arbitrary time (UTC) in the future is used as the upper range bound.
+func timeRangeFilter(from, to Timestamp) (string, error) {
+ // avoid small clock sync issues between client and server and use 1d as future
+ // timestamp buffer
+ timeBuffer := time.Hour * 24
+
+ if to == 0 {
+ to = Timestamp(time.Now().Add(timeBuffer).Unix() * 1000) // milliseconds
+ }
+
+ f := BetweenFilter{
+ Type: "Between",
+ Name: "time",
+ FromValue: from,
+ ToValue: to,
+ }
+
+ filter, err := json.Marshal(f)
+ if err != nil {
+ return "", fmt.Errorf("JSON marshal filter: %w", err)
+ }
+
+ return string(filter), nil
+}
+
+// Logout performs a logout against the Horizon API
+func (h *horizonClient) Logout(ctx context.Context) error {
+ request := RefreshTokenRequest{h.tokens.RefreshToken}
+ res, err := h.client.R().SetContext(ctx).SetBody(request).Post(logoutPath)
+ if err != nil {
+ return err
+ }
+
+ if !res.IsSuccess() {
+ switch res.StatusCode() {
+ case http.StatusBadRequest:
+ return errors.New("auth token already expired")
+
+ default:
+ return fmt.Errorf("unexpected status code: code: %d error: %s", res.StatusCode(), string(res.Body()))
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/horizon/horizon_test.go b/pkg/horizon/horizon_test.go
new file mode 100644
index 000000000..bc94c19d0
--- /dev/null
+++ b/pkg/horizon/horizon_test.go
@@ -0,0 +1,318 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizon
+
+import (
+ "context"
+ "encoding/json"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap/zaptest"
+)
+
+const (
+ testDomain = "corp"
+ testUsername = "user"
+ testPassword = "password"
+)
+
+func Test_horizonClient_login(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ logger := zaptest.NewLogger(t).Sugar()
+
+ t.Run("successful login", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ credentials: AuthLoginRequest{
+ Domain: testDomain,
+ Username: testUsername,
+ Password: testPassword,
+ },
+ logger: logger,
+ }
+
+ err := h.login(ctx)
+ require.NoError(t, err)
+ })
+
+ t.Run("invalid credentials", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ credentials: AuthLoginRequest{
+ Domain: testDomain,
+ Username: "unknown",
+ Password: "wrong",
+ },
+ logger: logger,
+ }
+
+ err := h.login(ctx)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "401")
+ })
+
+ t.Run("client with valid refresh token", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ tsRefreshToken := ts.getTokens().RefreshToken
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ tokens: AuthTokens{
+ RefreshToken: tsRefreshToken,
+ },
+ logger: logger,
+ }
+
+ err := h.login(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tsRefreshToken, h.tokens.RefreshToken)
+ })
+
+ t.Run("client with invalid refresh token triggers re-auth", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ tsRefreshToken := ts.getTokens().RefreshToken
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ credentials: AuthLoginRequest{
+ Domain: testDomain,
+ Username: testUsername,
+ Password: testPassword,
+ },
+ tokens: AuthTokens{
+ RefreshToken: "invalid",
+ },
+ logger: logger,
+ }
+
+ err := h.login(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tsRefreshToken, h.tokens.RefreshToken)
+ })
+
+ t.Run("client with expired refresh token triggers re-auth", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ currentTokens := ts.getTokens()
+ ts.rotateTokens()
+ newTokens := ts.getTokens()
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ credentials: AuthLoginRequest{
+ Domain: testDomain,
+ Username: testUsername,
+ Password: testPassword,
+ },
+ tokens: AuthTokens{
+ RefreshToken: currentTokens.RefreshToken,
+ },
+ logger: logger,
+ }
+
+ err := h.login(ctx)
+ require.NoError(t, err)
+ require.Equal(t, newTokens.RefreshToken, h.tokens.RefreshToken)
+ })
+}
+
+func Test_horizonClient_Logout(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ logger := zaptest.NewLogger(t).Sugar()
+
+ t.Run("successful logout", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ tsRefreshToken := ts.getTokens().RefreshToken
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ tokens: AuthTokens{
+ RefreshToken: tsRefreshToken,
+ },
+ logger: logger,
+ }
+
+ err := h.Logout(ctx)
+ require.NoError(t, err)
+ })
+
+ t.Run("logout throws error with invalid refresh token", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ tokens: AuthTokens{
+ RefreshToken: "invalid",
+ },
+ logger: logger,
+ }
+
+ err := h.Logout(ctx)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "expired")
+ })
+
+ t.Run("logout with expired context", func(t *testing.T) {
+ ts := newTestServer(ctx)
+ defer ts.httpSrv.Close()
+
+ canceledCtx, cancel := context.WithCancel(ctx)
+ cancel()
+
+ h := &horizonClient{
+ client: newRESTClient(ctx, ts.httpSrv.URL, false),
+ logger: logger,
+ }
+
+ err := h.Logout(canceledCtx)
+ assert.ErrorIs(t, err, context.Canceled)
+ })
+}
+
+type horizonAPIMock struct {
+ httpSrv *httptest.Server
+
+ sync.RWMutex
+ tokens AuthTokens
+}
+
+func newTestServer(_ context.Context) *horizonAPIMock {
+ mux := http.NewServeMux()
+ ts := horizonAPIMock{
+ httpSrv: httptest.NewServer(mux),
+ tokens: AuthTokens{
+ AccessToken: randomToken(10),
+ RefreshToken: randomToken(20),
+ },
+ }
+
+ mux.HandleFunc(loginPath, ts.loginHandler)
+ mux.HandleFunc(logoutPath, ts.logoutHandler)
+ mux.HandleFunc(refreshPath, ts.refreshHandler)
+
+ return &ts
+}
+
+func (h *horizonAPIMock) rotateTokens() {
+ h.Lock()
+ h.tokens.AccessToken = randomToken(10)
+ h.tokens.RefreshToken = randomToken(20)
+ h.Unlock()
+}
+
+func (h *horizonAPIMock) getTokens() AuthTokens {
+ h.RLock()
+ defer h.RUnlock()
+ return h.tokens
+}
+
+func (h *horizonAPIMock) loginHandler(w http.ResponseWriter, r *http.Request) {
+ var creds AuthLoginRequest
+ dec := json.NewDecoder(r.Body)
+ if err := dec.Decode(&creds); err != nil {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
+ if creds.Domain != testDomain ||
+ creds.Username != testUsername ||
+ creds.Password != testPassword {
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ return
+ }
+
+ enc := json.NewEncoder(w)
+ w.Header().Set("content-type", "application/json")
+
+ h.RLock()
+ defer h.RUnlock()
+ if err := enc.Encode(h.tokens); err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+}
+
+func (h *horizonAPIMock) refreshHandler(w http.ResponseWriter, r *http.Request) {
+ var refresh RefreshTokenRequest
+ dec := json.NewDecoder(r.Body)
+ if err := dec.Decode(&refresh); err != nil {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
+ h.Lock()
+ defer h.Unlock()
+
+ if h.tokens.RefreshToken != refresh.RefreshToken {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
+ enc := json.NewEncoder(w)
+ w.Header().Set("content-type", "application/json")
+
+ accessToken := AccessToken{
+ AccessToken: h.tokens.AccessToken,
+ }
+
+ if err := enc.Encode(accessToken); err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+}
+
+func (h *horizonAPIMock) logoutHandler(w http.ResponseWriter, r *http.Request) {
+ var refresh RefreshTokenRequest
+ dec := json.NewDecoder(r.Body)
+ if err := dec.Decode(&refresh); err != nil {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
+ h.Lock()
+ defer h.Unlock()
+
+ if refresh.RefreshToken == h.tokens.RefreshToken {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ w.WriteHeader(http.StatusBadRequest)
+}
+
+func randomToken(n int) string {
+ rand.Seed(time.Now().Unix())
+
+ letter := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letter[rand.Intn(len(letter))]
+ }
+ return string(b)
+}
diff --git a/pkg/horizon/testdata/audit_events.golden b/pkg/horizon/testdata/audit_events.golden
new file mode 100644
index 000000000..c54700d32
--- /dev/null
+++ b/pkg/horizon/testdata/audit_events.golden
@@ -0,0 +1,94 @@
+[
+ {
+ "id": 98563,
+ "type": "REST_AUTH_LOGIN_SUCCESS",
+ "severity": "AUDIT_SUCCESS",
+ "module": "Rest",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627369939733,
+ "message": "User corp\\administrator has logged in to Horizon Server REST API"
+ },
+ {
+ "id": 98562,
+ "user_id": "S-1-5-21-4442515-1634369418-872054540-500",
+ "type": "VLSI_USERLOGGEDIN",
+ "severity": "AUDIT_SUCCESS",
+ "module": "Vlsi",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627369939490,
+ "message": "User corp\\administrator has logged in to View Administrator"
+ },
+ {
+ "id": 98561,
+ "type": "BROKER_SMALL_MEMORY",
+ "severity": "WARNING",
+ "module": "Broker",
+ "machine_dns_name": "Horizon-02.corp.local",
+ "time": 1627368981807,
+ "message": "Broker Horizon-02.corp.local has been detected as being configured with a small amount of physical memory. Please refer to the View Installation Guide."
+ },
+ {
+ "id": 98558,
+ "type": "BROKER_DAILY_MAX_APP_USERS",
+ "severity": "INFO",
+ "module": "Broker",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627368903620,
+ "message": "Over the past 24 hours, the maximum number of users with concurrent application sessions was 0"
+ },
+ {
+ "id": 98559,
+ "type": "BROKER_DAILY_MAX_CCU_USERS",
+ "severity": "INFO",
+ "module": "Broker",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627368903620,
+ "message": "Over the past 24 hours, the maximum number of concurrent connection sessions was 0"
+ },
+ {
+ "id": 98560,
+ "type": "BROKER_DAILY_MAX_NU_USERS",
+ "severity": "INFO",
+ "module": "Broker",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627368903620,
+ "message": "Over the past 24 hours, the maximum number of named user sessions was 23"
+ },
+ {
+ "id": 98557,
+ "type": "BROKER_DAILY_MAX_DESKTOP_SESSIONS",
+ "severity": "INFO",
+ "module": "Broker",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627368903617,
+ "message": "Over the past 24 hours, the maximum number of concurrent desktop sessions was 0"
+ },
+ {
+ "id": 98556,
+ "type": "BROKER_SMALL_MEMORY",
+ "severity": "WARNING",
+ "module": "Broker",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627367926447,
+ "message": "Broker Horizon-01.corp.local has been detected as being configured with a small amount of physical memory. Please refer to the View Installation Guide."
+ },
+ {
+ "id": 98555,
+ "type": "REST_AUTH_LOGOUT_SUCCESS",
+ "severity": "AUDIT_SUCCESS",
+ "module": "Rest",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627332342703,
+ "message": "User corp\\administrator has logged out from Horizon Server REST API"
+ },
+ {
+ "id": 98554,
+ "user_id": "S-1-5-21-4442515-1634369418-872054540-500",
+ "type": "ADMIN_USERLOGGEDOUT",
+ "severity": "AUDIT_SUCCESS",
+ "module": "Vlsi",
+ "machine_dns_name": "Horizon-01.corp.local",
+ "time": 1627332342687,
+ "message": "User corp\\administrator has logged out from View Administrator"
+ }
+]
diff --git a/pkg/horizon/types.go b/pkg/horizon/types.go
new file mode 100644
index 000000000..f54d8a31a
--- /dev/null
+++ b/pkg/horizon/types.go
@@ -0,0 +1,95 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizon
+
+// generated from https://code-stg.vmware.com/apis/1169/view-rest-api
+
+// AuthLoginRequest is used to perform a full authentication against the Horizon
+// API server
+type AuthLoginRequest struct {
+ // Domain
+ Domain string `json:"domain"`
+ // User Name
+ Username string `json:"username"`
+ // User password
+ Password string `json:"password"`
+}
+
+// RefreshTokenRequest is used to get new Access token
+type RefreshTokenRequest struct {
+ RefreshToken string `json:"refresh_token"`
+}
+
+// AccessToken contains the new access token returned from a successful token
+// refresh
+type AccessToken struct {
+ // Access Token to be used in API calls.
+ AccessToken string `json:"access_token"`
+}
+
+// AuthTokens contains authentication details with access and refresh token
+type AuthTokens struct {
+ // Access Token to be used in API calls.
+ AccessToken string `json:"access_token"`
+ // Refresh Token to be used to get a new Access token.
+ RefreshToken string `json:"refresh_token"`
+}
+
+// AuditEventAttributeInfo contains extended event attribute information
+type AuditEventAttributeInfo struct {
+ // Key value pairs representing Extended attributes related to the event.
+ EventData map[string]interface{} `json:"event_data,omitempty"`
+ // Unique id representing an event.
+ ID int64 `json:"id,omitempty"`
+}
+
+// AuditEventSummary contains information about audit events
+type AuditEventSummary struct {
+ // Application Pool associated with this event. Will be unset if there is no
+ // application association for this event. Supported Filters : 'Equals'.
+ ApplicationPoolName string `json:"application_pool_name,omitempty"`
+ // Desktop Pool associated with this event. Will be unset if there is no desktop
+ // association for this event. Supported Filters : 'Equals'.
+ DesktopPoolName string `json:"desktop_pool_name,omitempty"`
+ // Unique id representing an event. Supported Filters : 'Equals'.
+ ID int64 `json:"id,omitempty"`
+ // FQDN of the machine in the Pod that has logged this event. Supported Filters
+ // : 'Equals'.
+ MachineDNSName string `json:"machine_dns_name,omitempty"`
+ // Machine associated with this event. Will be unset if there is no machine
+ // association for this event. Supported Filters : 'Equals'.
+ MachineID string `json:"machine_id,omitempty"`
+ // Audit event message.
+ Message string `json:"message,omitempty"`
+ // Horizon component that has logged this event. Supported Filters : 'Equals'.
+ Module string `json:"module,omitempty"`
+ // Severity type of the event. Supported Filters : 'Equals'. * INFO: Audit event
+ // is of INFO severity. * WARNING: Audit event is of WARNING severity * ERROR:
+ // Audit event is of ERROR severity * AUDIT_SUCCESS: Audit event is of
+ // AUDIT_SUCCESS severity * AUDIT_FAIL: Audit event is of AUDIT_FAIL severity *
+ // UNKNOWN: Not able to identify severity
+ Severity string `json:"severity,omitempty"`
+ // Time at which the event occurred. Supported Filters : 'Equals'.
+ Time int64 `json:"time,omitempty"`
+ // Event name that corresponds to an item in the message catalog. Supported
+ // Filters : 'Equals'.
+ Type string `json:"type,omitempty"`
+ // Sid of the user associated with this event. Supported Filters : 'Equals'.
+ UserID string `json:"user_id,omitempty"`
+}
+
+// BetweenFilter is a range filter. It can be used to filter on int64
+// timestamps.
+type BetweenFilter struct {
+ Type string `json:"type,omitempty"`
+ FromValue interface{} `json:"fromValue,omitempty"`
+ Name string `json:"name,omitempty"`
+ ToValue interface{} `json:"toValue,omitempty"`
+}
+
+// Timestamp is time since unix epoch (UTC) in milliseconds (as defined by
+// Horizon spec)
+type Timestamp int64
diff --git a/pkg/reconciler/horizonsource/controller.go b/pkg/reconciler/horizonsource/controller.go
new file mode 100644
index 000000000..0478bc61e
--- /dev/null
+++ b/pkg/reconciler/horizonsource/controller.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizonsource
+
+import (
+ "context"
+ "time"
+
+ "knative.dev/pkg/metrics"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+
+ "github.com/kelseyhightower/envconfig"
+ "k8s.io/client-go/tools/cache"
+
+ "knative.dev/pkg/configmap"
+ "knative.dev/pkg/controller"
+ "knative.dev/pkg/logging"
+ "knative.dev/pkg/resolver"
+
+ kubeclient "knative.dev/pkg/client/injection/kube/client"
+ deploymentinformer "knative.dev/pkg/client/injection/kube/informers/apps/v1/deployment"
+
+ sainformer "knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount"
+
+ horizonsourceinformer "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/informers/sources/v1alpha1/horizonsource"
+ "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource"
+)
+
+const (
+ resyncPeriod = time.Second * 10
+)
+
+// NewController initializes the controller and is called by the generated code
+// Registers event handlers to enqueue events
+func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+ ctx = controller.WithResyncPeriod(ctx, resyncPeriod)
+
+ r := &Reconciler{
+ loggingContext: ctx,
+ kclient: kubeclient.Get(ctx),
+ depl: &DeploymentReconciler{KubeClientSet: kubeclient.Get(ctx)},
+ sa: &ServiceAccountReconciler{KubeClientSet: kubeclient.Get(ctx)},
+ }
+
+ if err := envconfig.Process("", r); err != nil {
+ logging.FromContext(ctx).Panicf("required environment variable is not defined: %v", err)
+ }
+
+ impl := horizonsource.NewImpl(ctx, r)
+ r.sinkResolver = resolver.NewURIResolverFromTracker(ctx, impl.Tracker)
+
+ horizonSourceInformer := horizonsourceinformer.Get(ctx)
+ saInformer := sainformer.Get(ctx)
+ deploymentInformer := deploymentinformer.Get(ctx)
+
+ horizonSourceInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))
+
+ saInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
+ FilterFunc: controller.FilterController(&v1alpha1.HorizonSource{}),
+ Handler: controller.HandleAll(impl.EnqueueControllerOf),
+ })
+
+ deploymentInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
+ FilterFunc: controller.FilterController(&v1alpha1.HorizonSource{}),
+ Handler: controller.HandleAll(impl.EnqueueControllerOf),
+ })
+
+ cmw.Watch(logging.ConfigMapName(), r.UpdateFromLoggingConfigMap)
+ cmw.Watch(metrics.ConfigMapName(), r.UpdateFromMetricsConfigMap)
+
+ return impl
+}
diff --git a/pkg/reconciler/horizonsource/deployment.go b/pkg/reconciler/horizonsource/deployment.go
new file mode 100644
index 000000000..d387b6226
--- /dev/null
+++ b/pkg/reconciler/horizonsource/deployment.go
@@ -0,0 +1,151 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizonsource
+
+import (
+ "context"
+ "fmt"
+ "sort"
+
+ // k8s.io imports
+ appsv1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/equality"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/kubernetes"
+
+ "knative.dev/pkg/kmeta"
+ "knative.dev/pkg/logging"
+ pkgreconciler "knative.dev/pkg/reconciler"
+
+ "go.uber.org/zap"
+)
+
+// newDeploymentCreated makes a reconciler event with event type Normal, and
+// reason DeploymentCreated.
+func newDeploymentCreated(namespace, name string) pkgreconciler.Event {
+ return pkgreconciler.NewEvent(corev1.EventTypeNormal, "DeploymentCreated", "created deployment: \"%s/%s\"", namespace, name)
+}
+
+// newDeploymentFailed makes a reconciler event with event type Warning, and
+// reason DeploymentFailed.
+func newDeploymentFailed(namespace, name string, err error) pkgreconciler.Event {
+ return pkgreconciler.NewEvent(corev1.EventTypeWarning, "DeploymentFailed", "failed to create deployment: \"%s/%s\", %w", namespace, name, err)
+}
+
+// newDeploymentUpdated makes a reconciler event with event type Normal, and
+// reason DeploymentUpdated.
+func newDeploymentUpdated(namespace, name string) pkgreconciler.Event {
+ return pkgreconciler.NewEvent(corev1.EventTypeNormal, "DeploymentUpdated", "updated deployment: \"%s/%s\"", namespace, name)
+}
+
+type DeploymentReconciler struct {
+ KubeClientSet kubernetes.Interface
+}
+
+// ReconcileDeployment reconciles deployment resource (adapter) for HorizonSource
+func (r *DeploymentReconciler) ReconcileDeployment(ctx context.Context, owner kmeta.OwnerRefable, expected *appsv1.Deployment) (*appsv1.Deployment, pkgreconciler.Event) {
+ namespace := owner.GetObjectMeta().GetNamespace()
+
+ ra, err := r.KubeClientSet.AppsV1().Deployments(namespace).Get(ctx, expected.Name, metav1.GetOptions{})
+ if err != nil {
+ if apierrors.IsNotFound(err) {
+ ra, err = r.KubeClientSet.AppsV1().Deployments(namespace).Create(ctx, expected, metav1.CreateOptions{})
+ if err != nil {
+ return nil, newDeploymentFailed(expected.Namespace, expected.Name, err)
+ }
+ return ra, newDeploymentCreated(ra.Namespace, ra.Name)
+ }
+ return nil, fmt.Errorf("error getting receive adapter %q: %v", expected.Name, err)
+ }
+
+ if !metav1.IsControlledBy(ra, owner.GetObjectMeta()) {
+ return nil, fmt.Errorf("deployment %q is not owned by %s %q",
+ ra.Name, owner.GetGroupVersionKind().Kind, owner.GetObjectMeta().GetName())
+ }
+
+ if podSpecSync(ctx, expected.Spec.Template.Spec, ra.Spec.Template.Spec) {
+ logging.FromContext(ctx).Debugw("updating receive adapter: pod template spec out of sync")
+
+ ra.Spec.Template.Spec = expected.Spec.Template.Spec
+ ra, err = r.KubeClientSet.AppsV1().Deployments(namespace).Update(ctx, ra, metav1.UpdateOptions{})
+ if err != nil {
+ return ra, err
+ }
+ return ra, newDeploymentUpdated(ra.Namespace, ra.Name)
+ }
+
+ logging.FromContext(ctx).Debugw("reusing existing receive adapter", zap.Any("receiveAdapter", ra))
+ return ra, nil
+}
+
+func (r *DeploymentReconciler) FindOwned(ctx context.Context, owner kmeta.OwnerRefable, selector labels.Selector) (*appsv1.Deployment, error) {
+ dl, err := r.KubeClientSet.AppsV1().Deployments(owner.GetObjectMeta().GetNamespace()).List(ctx, metav1.ListOptions{
+ LabelSelector: selector.String(),
+ })
+ if err != nil {
+ logging.FromContext(ctx).Error("Unable to list deployments: %v", zap.Error(err))
+ return nil, err
+ }
+ for i := range dl.Items {
+ if metav1.IsControlledBy(&dl.Items[i], owner.GetObjectMeta()) {
+ return &dl.Items[i], nil
+ }
+ }
+ return nil, apierrors.NewNotFound(schema.GroupResource{}, "")
+}
+
+func getContainer(name string, spec corev1.PodSpec) (int, *corev1.Container) {
+ for i, c := range spec.Containers {
+ if c.Name == name {
+ return i, &c
+ }
+ }
+ return -1, nil
+}
+
+// Returns true if an update is needed.
+func podSpecSync(_ context.Context, expected corev1.PodSpec, now corev1.PodSpec) bool {
+ old := *now.DeepCopy()
+ syncContainers(expected, now)
+ return !equality.Semantic.DeepEqual(old, now)
+}
+
+func syncContainers(expected corev1.PodSpec, now corev1.PodSpec) {
+ // got needs all of the containers that want as, but it is allowed to have more.
+ for _, ec := range expected.Containers {
+ n, nc := getContainer(ec.Name, now)
+ if nc == nil {
+ now.Containers = append(now.Containers, ec)
+ continue
+ }
+ if nc.Image != ec.Image {
+ now.Containers[n].Image = ec.Image
+ }
+
+ // copy and sort envs to avoid reconcile when only env order is different
+ expEnvs := make([]corev1.EnvVar, len(ec.Env))
+ nowEnvs := make([]corev1.EnvVar, len(nc.Env))
+
+ copy(expEnvs, ec.Env)
+ copy(nowEnvs, nc.Env)
+
+ sort.Slice(expEnvs, func(i, j int) bool {
+ return expEnvs[i].Name < expEnvs[j].Name
+ })
+
+ sort.Slice(nowEnvs, func(i, j int) bool {
+ return nowEnvs[i].Name < nowEnvs[j].Name
+ })
+
+ if !equality.Semantic.DeepEqual(expEnvs, nowEnvs) {
+ now.Containers[n].Env = ec.Env
+ }
+ }
+}
diff --git a/pkg/reconciler/horizonsource/horizonsource.go b/pkg/reconciler/horizonsource/horizonsource.go
new file mode 100644
index 000000000..f6e64e384
--- /dev/null
+++ b/pkg/reconciler/horizonsource/horizonsource.go
@@ -0,0 +1,173 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizonsource
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "go.uber.org/zap"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes"
+ "knative.dev/pkg/metrics"
+
+ // k8s.io imports
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ // knative.dev/pkg imports
+ "knative.dev/pkg/logging"
+ pkgreconciler "knative.dev/pkg/reconciler"
+ "knative.dev/pkg/resolver"
+
+ // knative.dev/eventing imports
+ sourcesv1 "knative.dev/eventing/pkg/apis/sources/v1"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ "github.com/vmware-tanzu/sources-for-knative/pkg/client/injection/reconciler/sources/v1alpha1/horizonsource"
+ "github.com/vmware-tanzu/sources-for-knative/pkg/reconciler/horizonsource/resources"
+)
+
+const (
+ component = "horizonsource"
+)
+
+// Reconciler reconciles a HorizonSource object
+type Reconciler struct {
+ ReceiveAdapterImage string `envconfig:"HORIZON_SOURCE_RA_IMAGE" required:"true"`
+
+ kclient kubernetes.Interface
+
+ // reconcilers
+ depl *DeploymentReconciler
+ sa *ServiceAccountReconciler
+
+ loggingContext context.Context
+ loggingConfig *logging.Config
+ metricsConfig *metrics.ExporterOptions
+
+ sinkResolver *resolver.URIResolver
+}
+
+// Check that our Reconciler implements Interface
+var _ horizonsource.Interface = (*Reconciler)(nil)
+
+// ReconcileKind implements Interface.ReconcileKind.
+func (r *Reconciler) ReconcileKind(ctx context.Context, src *v1alpha1.HorizonSource) pkgreconciler.Event {
+ ctx = sourcesv1.WithURIResolver(ctx, r.sinkResolver)
+
+ src.Status.InitializeConditions()
+
+ if err := src.Spec.Sink.Validate(ctx); err != nil {
+ src.Status.MarkNoSink("SinkMissing", "")
+ return fmt.Errorf("spec.sink missing")
+ }
+
+ dest := src.Spec.Sink.DeepCopy()
+ if dest.Ref != nil {
+ if dest.Ref.Namespace == "" {
+ dest.Ref.Namespace = src.GetNamespace()
+ }
+ }
+ sinkURI, err := r.sinkResolver.URIFromDestinationV1(ctx, *dest, src)
+ if err != nil {
+ src.Status.MarkNoSink("NotFound", "")
+ return fmt.Errorf("getting sink URI: %v", err)
+ }
+ src.Status.MarkSink(sinkURI)
+
+ _, err = r.kclient.CoreV1().Secrets(src.Namespace).Get(ctx, src.Spec.SecretRef.Name, metav1.GetOptions{})
+ if err != nil {
+ logging.FromContext(ctx).Errorw("returning because required secret not found", zap.String("secret", src.Spec.SecretRef.Name), zap.Error(err))
+ return err
+ }
+
+ labels := resources.Labels(src.Name)
+
+ // create serviceAccount
+ _, err = r.sa.ReconcileServiceAccount(ctx, src, labels)
+ if err != nil {
+ logging.FromContext(ctx).Errorw("returning because of event from ReconcileServiceAccount", zap.Error(err))
+ return err
+ }
+
+ loggingConfig, err := logging.ConfigToJSON(r.loggingConfig)
+ if err != nil {
+ logging.FromContext(ctx).Error("returning because cannot convert logging config to JSON", zap.Error(err))
+ return err
+ }
+
+ metricsConfig, err := metrics.OptionsToJSON(r.metricsConfig)
+ if err != nil {
+ logging.FromContext(ctx).Error("returning because cannot convert metrics config to JSON", zap.Error(err))
+ return err
+ }
+
+ // create adapter
+ args := resources.ReceiveAdapterArgs{
+ Image: r.ReceiveAdapterImage,
+ Labels: labels,
+ Source: src,
+ SinkURI: sinkURI.String(),
+ LoggingConfig: loggingConfig,
+ MetricsConfig: metricsConfig,
+ }
+ adapter, err := resources.NewReceiveAdapter(ctx, &args)
+ if err != nil {
+ logging.FromContext(ctx).Errorw("returning because adapter could not be configured", zap.Error(err))
+ return err
+ }
+
+ ra, err := r.depl.ReconcileDeployment(ctx, src, adapter)
+ if ra != nil {
+ src.Status.PropagateDeploymentAvailability(ra)
+ }
+
+ if err != nil {
+ // ignore normal reconcile events
+ var reconcileErr *pkgreconciler.ReconcilerEvent
+ if errors.As(err, &reconcileErr) {
+ if reconcileErr.EventType == corev1.EventTypeNormal {
+ return nil
+ }
+ logging.FromContext(ctx).Errorw("returning because of non-normal event from ReconcileDeployment", zap.Error(err))
+ return err
+ }
+
+ logging.FromContext(ctx).Errorw("returning because of reconcile error", zap.Error(err))
+ return err
+ }
+
+ return nil
+}
+
+func (r *Reconciler) UpdateFromLoggingConfigMap(cfg *corev1.ConfigMap) {
+ if cfg != nil {
+ delete(cfg.Data, "_example")
+ }
+
+ logcfg, err := logging.NewConfigFromConfigMap(cfg)
+ if err != nil {
+ logging.FromContext(r.loggingContext).Warn("failed to create logging config from configmap", zap.String("cfg.Name", cfg.Name))
+ return
+ }
+
+ r.loggingConfig = logcfg
+ logging.FromContext(r.loggingContext).Info("Update from logging ConfigMap", zap.Any("configMap", cfg))
+}
+
+func (r *Reconciler) UpdateFromMetricsConfigMap(cfg *corev1.ConfigMap) {
+ if cfg != nil {
+ delete(cfg.Data, "_example")
+ }
+
+ r.metricsConfig = &metrics.ExporterOptions{
+ Domain: metrics.Domain(),
+ Component: component,
+ ConfigMap: cfg.Data,
+ }
+ logging.FromContext(r.loggingContext).Info("Update from metrics ConfigMap", zap.Any("configMap", cfg))
+}
diff --git a/pkg/reconciler/horizonsource/resources/adapter.go b/pkg/reconciler/horizonsource/resources/adapter.go
new file mode 100644
index 000000000..3bd809824
--- /dev/null
+++ b/pkg/reconciler/horizonsource/resources/adapter.go
@@ -0,0 +1,154 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package resources
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ v1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "knative.dev/pkg/kmeta"
+ "knative.dev/pkg/ptr"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ "github.com/vmware-tanzu/sources-for-knative/pkg/horizon"
+ "github.com/vmware-tanzu/sources-for-knative/pkg/reconciler/horizonsource/resources/names"
+)
+
+// ReceiveAdapterArgs are the arguments needed to create a Horizon source Receive Adapter.
+// Every field is required.
+type ReceiveAdapterArgs struct {
+ Image string
+ Labels map[string]string
+ Source *v1alpha1.HorizonSource
+ SinkURI string
+ LoggingConfig string
+ MetricsConfig string
+}
+
+// NewReceiveAdapter generates the Receive Adapter Deployment for Horizon
+// sources
+func NewReceiveAdapter(ctx context.Context, args *ReceiveAdapterArgs) (*v1.Deployment, error) {
+ env, err := makeEnv(ctx, args)
+ if err != nil {
+ return nil, err
+ }
+
+ return &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: args.Source.Namespace,
+ Name: names.NewAdapterName(args.Source.Name),
+ Labels: args.Labels,
+ OwnerReferences: []metav1.OwnerReference{
+ *kmeta.NewControllerRef(args.Source),
+ },
+ },
+ Spec: v1.DeploymentSpec{
+ Selector: &metav1.LabelSelector{
+ MatchLabels: args.Labels,
+ },
+ Replicas: ptr.Int32(1),
+ Template: corev1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: args.Labels,
+ },
+ Spec: corev1.PodSpec{
+ ServiceAccountName: args.Source.Spec.ServiceAccountName,
+ Containers: []corev1.Container{
+ {
+ Name: "adapter",
+ Image: args.Image,
+ Env: env,
+ // TODO (@mgasch): add resources
+ // Resources: corev1.ResourceRequirements{},,
+ VolumeMounts: []corev1.VolumeMount{
+ {
+ Name: args.Source.Spec.SecretRef.Name,
+ ReadOnly: true,
+ MountPath: horizon.DefaultSecretMountPath,
+ },
+ },
+ },
+ },
+ Volumes: []corev1.Volume{
+ {
+ Name: args.Source.Spec.SecretRef.Name,
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: args.Source.Spec.SecretRef.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Strategy: v1.DeploymentStrategy{
+ // terminate existing instance before creating a new one to reduce chance of
+ // multiple source adapters sending events when changing log levels and running
+ // kubectl rollout restart
+ Type: v1.RecreateDeploymentStrategyType,
+ },
+ },
+ }, nil
+}
+
+func makeEnv(ctx context.Context, args *ReceiveAdapterArgs) ([]corev1.EnvVar, error) {
+ var ceOverrides string
+ if args.Source.Spec.CloudEventOverrides != nil {
+ if co, err := json.Marshal(args.Source.Spec.SourceSpec.CloudEventOverrides); err != nil {
+ return nil,
+ fmt.Errorf("failed to marshal CloudEventOverrides into JSON for %+v: %w", args.Source, err)
+ } else if len(co) > 0 {
+ ceOverrides = string(co)
+ }
+ }
+
+ return []corev1.EnvVar{
+ {
+ Name: "HORIZON_URL",
+ Value: args.Source.Spec.Address.String(),
+ },
+ {
+ Name: "HORIZON_INSECURE",
+ Value: fmt.Sprintf("%t", args.Source.Spec.SkipTLSVerify),
+ },
+ {
+ Name: "METRICS_DOMAIN",
+ Value: "knative.dev/eventing",
+ },
+ {
+ Name: "K_CE_OVERRIDES",
+ Value: ceOverrides,
+ },
+ {
+ Name: "SINK_URI",
+ Value: args.SinkURI,
+ },
+ {
+ Name: "K_SINK",
+ Value: args.SinkURI,
+ },
+ {
+ Name: "NAME",
+ Value: args.Source.Name,
+ },
+ {
+ Name: "NAMESPACE",
+ Value: args.Source.Namespace,
+ },
+ {
+ Name: "K_LOGGING_CONFIG",
+ Value: args.LoggingConfig,
+ },
+ {
+ Name: "K_METRICS_CONFIG",
+ Value: args.MetricsConfig,
+ },
+ }, nil
+}
diff --git a/pkg/reconciler/horizonsource/resources/labels.go b/pkg/reconciler/horizonsource/resources/labels.go
new file mode 100644
index 000000000..1b42092af
--- /dev/null
+++ b/pkg/reconciler/horizonsource/resources/labels.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package resources
+
+const (
+ // controllerAgentName is the string used by this controller to identify
+ // itself when creating events.
+ controllerAgentName = "horizon-source-controller"
+)
+
+func Labels(name string) map[string]string {
+ return map[string]string{
+ "knative-eventing-source": controllerAgentName,
+ "knative-eventing-source-name": name,
+ }
+}
diff --git a/pkg/reconciler/horizonsource/resources/names/names.go b/pkg/reconciler/horizonsource/resources/names/names.go
new file mode 100644
index 000000000..14c2e7963
--- /dev/null
+++ b/pkg/reconciler/horizonsource/resources/names/names.go
@@ -0,0 +1,17 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package names
+
+import (
+ "knative.dev/pkg/kmeta"
+)
+
+// (@mgasch) not using source prefixes for now
+// const prefix = "horizon-source"
+
+func NewAdapterName(source string) string {
+ return kmeta.ChildName(source, "-adapter")
+}
diff --git a/pkg/reconciler/horizonsource/resources/names/names_test.go b/pkg/reconciler/horizonsource/resources/names/names_test.go
new file mode 100644
index 000000000..2efd600f6
--- /dev/null
+++ b/pkg/reconciler/horizonsource/resources/names/names_test.go
@@ -0,0 +1,43 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package names
+
+import (
+ "testing"
+)
+
+func TestNewAdapterName(t *testing.T) {
+ type args struct {
+ source string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "source name within 63 char limit",
+ args: args{
+ source: "horizon-01",
+ },
+ want: "horizon-01-adapter",
+ },
+ {
+ name: "source name exceeds 63 char limit",
+ args: args{
+ source: "horizon-01-7c6e3f71-7c98-43dc-b783-72bcf0103970-way-tooooooooooooooo-long",
+ },
+ want: "horizon-01-7c6e3f71-7c9f88c636a5f0781b807c2771ff73668ac-adapter",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := NewAdapterName(tt.args.source); got != tt.want {
+ t.Errorf("NewAdapterName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/reconciler/horizonsource/resources/serviceaccount.go b/pkg/reconciler/horizonsource/resources/serviceaccount.go
new file mode 100644
index 000000000..8b806aff3
--- /dev/null
+++ b/pkg/reconciler/horizonsource/resources/serviceaccount.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package resources
+
+import (
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "knative.dev/pkg/kmeta"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+)
+
+func NewServiceAccount(src *v1alpha1.HorizonSource, labels map[string]string) *corev1.ServiceAccount {
+ return &corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: src.Spec.ServiceAccountName,
+ Namespace: src.Namespace,
+ Labels: labels,
+ OwnerReferences: []metav1.OwnerReference{
+ *kmeta.NewControllerRef(src),
+ },
+ },
+ }
+}
diff --git a/pkg/reconciler/horizonsource/serviceaccount.go b/pkg/reconciler/horizonsource/serviceaccount.go
new file mode 100644
index 000000000..a90c960e2
--- /dev/null
+++ b/pkg/reconciler/horizonsource/serviceaccount.go
@@ -0,0 +1,58 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizonsource
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ pkgreconciler "knative.dev/pkg/reconciler"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ "github.com/vmware-tanzu/sources-for-knative/pkg/reconciler/horizonsource/resources"
+)
+
+// newServiceAccountCreated makes a reconciler event with event type Normal, and
+// reason RoleCreated.
+func newServiceAccountCreated(namespace, name string) pkgreconciler.Event {
+ return pkgreconciler.NewEvent(corev1.EventTypeNormal, "ServiceAccountCreated", "created service account: \"%s/%s\"", namespace, name)
+}
+
+// newServiceAccountFailed makes a reconciler event with event type Warning, and
+// reason RoleFailed.
+func newServiceAccountFailed(namespace, name string, err error) pkgreconciler.Event {
+ return pkgreconciler.NewEvent(corev1.EventTypeWarning, "ServiceAccountFailed", "failed to create service account: \"%s/%s\", %w", namespace, name, err)
+}
+
+type ServiceAccountReconciler struct {
+ KubeClientSet kubernetes.Interface
+}
+
+// ReconcileServiceAccount reconciles service account resource for HorizonSource
+func (s *ServiceAccountReconciler) ReconcileServiceAccount(ctx context.Context, src *v1alpha1.HorizonSource, labels map[string]string) (*corev1.ServiceAccount, pkgreconciler.Event) {
+ namespace := src.Namespace
+ saName := src.Spec.ServiceAccountName
+
+ sa, err := s.KubeClientSet.CoreV1().ServiceAccounts(namespace).Get(ctx, saName, metav1.GetOptions{})
+ if err != nil {
+ if apierrors.IsNotFound(err) {
+ sa, err = s.KubeClientSet.CoreV1().ServiceAccounts(namespace).Create(ctx, resources.NewServiceAccount(src, labels), metav1.CreateOptions{})
+ if err != nil {
+ return nil, newServiceAccountFailed(src.Namespace, saName, err)
+ }
+ return sa, newServiceAccountCreated(sa.Namespace, sa.Name)
+ }
+ return nil, fmt.Errorf("error getting service account %q: %v", saName, err)
+ }
+
+ // TODO (@mgasch): handle updates
+
+ return sa, nil
+}
diff --git a/pkg/reconciler/horizonsource/serviceaccount_test.go b/pkg/reconciler/horizonsource/serviceaccount_test.go
new file mode 100644
index 000000000..46dd53de3
--- /dev/null
+++ b/pkg/reconciler/horizonsource/serviceaccount_test.go
@@ -0,0 +1,94 @@
+/*
+Copyright 2022 VMware, Inc.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package horizonsource
+
+import (
+ "context"
+ "reflect"
+ "testing"
+
+ "github.com/vmware-tanzu/sources-for-knative/pkg/apis/sources/v1alpha1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes"
+ pkgreconciler "knative.dev/pkg/reconciler"
+)
+
+func Test_newServiceAccountCreated(t *testing.T) {
+ type args struct {
+ namespace string
+ name string
+ }
+ tests := []struct {
+ name string
+ args args
+ want pkgreconciler.Event
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := newServiceAccountCreated(tt.args.namespace, tt.args.name); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("newServiceAccountCreated() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_newServiceAccountFailed(t *testing.T) {
+ type args struct {
+ namespace string
+ name string
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ want pkgreconciler.Event
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := newServiceAccountFailed(tt.args.namespace, tt.args.name, tt.args.err); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("newServiceAccountFailed() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestServiceAccountReconciler_ReconcileServiceAccount(t *testing.T) {
+ type fields struct {
+ KubeClientSet kubernetes.Interface
+ }
+ type args struct {
+ ctx context.Context
+ src *v1alpha1.HorizonSource
+ labels map[string]string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want *corev1.ServiceAccount
+ want1 pkgreconciler.Event
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ s := &ServiceAccountReconciler{
+ KubeClientSet: tt.fields.KubeClientSet,
+ }
+ got, got1 := s.ReconcileServiceAccount(tt.args.ctx, tt.args.src, tt.args.labels)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ServiceAccountReconciler.ReconcileServiceAccount() got = %v, want %v", got, tt.want)
+ }
+ if !reflect.DeepEqual(got1, tt.want1) {
+ t.Errorf("ServiceAccountReconciler.ReconcileServiceAccount() got1 = %v, want %v", got1, tt.want1)
+ }
+ })
+ }
+}
diff --git a/pkg/reconciler/vspheresource/resources/deployment.go b/pkg/reconciler/vspheresource/resources/deployment.go
index f65dfbcf6..bf18be0c6 100644
--- a/pkg/reconciler/vspheresource/resources/deployment.go
+++ b/pkg/reconciler/vspheresource/resources/deployment.go
@@ -60,6 +60,7 @@ func MakeDeployment(ctx context.Context, vms *v1alpha1.VSphereSource, args Adapt
Name: names.Deployment(vms),
Namespace: vms.Namespace,
OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(vms)},
+ Labels: labels,
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.Int32(1),
diff --git a/pkg/reconciler/vspheresource/resources/names/names.go b/pkg/reconciler/vspheresource/resources/names/names.go
index 52cd5f6c0..9f98306c9 100644
--- a/pkg/reconciler/vspheresource/resources/names/names.go
+++ b/pkg/reconciler/vspheresource/resources/names/names.go
@@ -12,7 +12,7 @@ import (
)
func Deployment(vms *v1alpha1.VSphereSource) string {
- return kmeta.ChildName(vms.Name, "-deployment")
+ return kmeta.ChildName(vms.Name, "-adapter")
}
func VSphereBinding(vms *v1alpha1.VSphereSource) string {
diff --git a/pkg/reconciler/vspheresource/resources/names/names_test.go b/pkg/reconciler/vspheresource/resources/names/names_test.go
index eb0d3deff..5adf3380d 100644
--- a/pkg/reconciler/vspheresource/resources/names/names_test.go
+++ b/pkg/reconciler/vspheresource/resources/names/names_test.go
@@ -27,7 +27,7 @@ func TestNames(t *testing.T) {
},
},
f: Deployment,
- want: "ffffffffffffffffffff105d7597f637e83cc711605ac3ea4957-deployment",
+ want: "fffffffffffffffffffffff105d7597f637e83cc711605ac3ea4957-adapter",
}, {
name: "Deployment long enough",
vss: &v1alpha1.VSphereSource{
@@ -36,7 +36,7 @@ func TestNames(t *testing.T) {
},
},
f: Deployment,
- want: strings.Repeat("f", 52) + "-deployment",
+ want: strings.Repeat("f", 52) + "-adapter",
}, {
name: "Deployment",
vss: &v1alpha1.VSphereSource{
@@ -45,7 +45,7 @@ func TestNames(t *testing.T) {
},
},
f: Deployment,
- want: "foo-deployment",
+ want: "foo-adapter",
}, {
name: "vspherebinding",
vss: &v1alpha1.VSphereSource{
diff --git a/pkg/vsphere/adapter.go b/pkg/vsphere/adapter.go
index 5e71ccdbd..38c00180a 100644
--- a/pkg/vsphere/adapter.go
+++ b/pkg/vsphere/adapter.go
@@ -200,7 +200,7 @@ func (a *vAdapter) readEvents(ctx context.Context, c *event.HistoryCollector) er
if len(events) == 0 {
delay := bOff.Duration()
- logger.Debugw("no new events, backing off", zap.String("delaySeconds", delay.String()))
+ logger.Debugw("backing off retrieving events: no new events received", zap.Duration("backoffSeconds", delay))
time.Sleep(delay)
continue
}
diff --git a/samples/README.md b/samples/README.md
index a190e0dd0..20803d340 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -1,7 +1,8 @@
-## VSphere Source and Binding samples
+## VMware Sources and Binding samples
- [Using `VSphereSource` with `vcsim`](./vcsim/README.md)
- [Using `VSphereBinding` with `govc`](./govc/README.md)
- [Using `VSphereBinding` with `PowerCLI`](./powercli/README.md)
- [Using `VSphereSource` and `VSphereBinding` to tag new VMs](./tag-new-vms/README.md)
- [Using `VSphereBinding` to build a `PowerCLI` "Cloud Shell"](./cloud-power-shell/README.md)
+- [Using `HorizonSource`](./horizon/README.md)
diff --git a/samples/horizon/README.md b/samples/horizon/README.md
new file mode 100644
index 000000000..a709cf24b
--- /dev/null
+++ b/samples/horizon/README.md
@@ -0,0 +1,97 @@
+## `HorizonSource` Example
+
+To create a `HorizonSource` the Horizon server API version must be at least
+`2106`. There are over 850+ events that are available and for a complete list,
+please take a look [here](https://github.com/lamw/horizon-event-mapping/).
+
+
+### Configure the Source
+
+Modify the `horizon-source.yml` according to your environment.
+
+`address` is the HTTPs endpoint of the Horizon API server. To skip TLS and
+certificate verification, set `skipTLSVerify` to `true`.
+
+Change the values under `sink` to match your Knative Eventing environment.
+
+If the specified `serviceAccountName` does not exist, it will be created
+automatically.
+
+```yaml
+apiVersion: sources.tanzu.vmware.com/v1alpha1
+kind: HorizonSource
+metadata:
+ name: horizon-example
+spec:
+ sink:
+ ref:
+ apiVersion: eventing.knative.dev/v1
+ kind: Broker
+ name: example-broker
+ namespace: default
+ address: https://horizon.server.example.com
+ skipTLSVerify: false
+ secretRef:
+ name: horizon-credentials
+ serviceAccountName: horizon-source-sa
+```
+
+### Configure authentication
+
+Create a Kubernetes `Secret` as per the name under `secretRef` in the
+`HorizonSource` above which holds the required Horizon credentials. `domain`,
+`username` and `password` are required fields. Replace the field values
+accordingly.
+
+```shell
+kubectl create secret generic horizon-credentials --from-literal=domain="example.com" --from-literal=username="horizon-source-account" --from-literal=password='ReplaceMe'
+```
+
+### Deploy the Source
+
+Finally, deploy the `HorizonSource`.
+
+You should see a new deployment with the name `horizon-example-adapter` coming
+up in the specified namespace (here `default`).
+
+
+```shell
+kubectl create -f horizon-source.yml
+
+# wait for the deployment to become ready
+kubectl wait --timeout=3m --for=condition=Available deploy/horizon-example-adapter
+deployment.apps/horizon-example-adapter condition met
+```
+
+### Enable verbose (debug) logging
+
+By default, each `HorizonSource` uses the `info` level for logging.
+
+```shell
+kubectl logs deploy/horizon-example-adapter
+{"level":"warn","ts":"2022-07-05T09:59:02.701Z","logger":"horizon-source-adapter","caller":"v2/config.go:185","msg":"Tracing configuration is invalid, using the no-op default{error 26 0 empty json tracing config}","commit":"01ea50f"}
+{"level":"warn","ts":"2022-07-05T09:59:02.701Z","logger":"horizon-source-adapter","caller":"v2/config.go:178","msg":"Sink timeout configuration is invalid, default to -1 (no timeout)","commit":"01ea50f"}
+{"level":"warn","ts":"2022-07-05T09:59:02.701Z","logger":"horizon-source-adapter","caller":"horizon/horizon.go:130","msg":"using potentially insecure connection to Horizon API server","commit":"01ea50f","address":"https://horizon.server.example.com","insecure":true}
+{"level":"info","ts":"2022-07-05T09:59:04.140Z","logger":"horizon-source-adapter","caller":"horizon/adapter.go:97","msg":"starting horizon source adapter","commit":"01ea50f","source":"https://horizon.server.example.com","pollIntervalSeconds":1}
+```
+
+To increase verbosity, update the logging configuration for the VMware Sources
+and then perform a rolling restart of the `HorizonSource` adapter for the
+logging changes to take effect.
+
+```shell
+# update general logging configuration
+kubectl -n vmware-sources edit cm config-logging
+```
+
+A new window opens with an interactive editor.
+
+Change the JSON line `"level": "info"` to `"level": "debug"`. Save and exit the
+editor.
+
+Perform a rolling restart of the running `HorizonSource`.
+
+```shell
+kubectl rollout restart deployment/horizon-example-adapter
+```
+
diff --git a/samples/horizon/horizon-source.yml b/samples/horizon/horizon-source.yml
new file mode 100644
index 000000000..308aa2105
--- /dev/null
+++ b/samples/horizon/horizon-source.yml
@@ -0,0 +1,16 @@
+apiVersion: sources.tanzu.vmware.com/v1alpha1
+kind: HorizonSource
+metadata:
+ name: horizon-example
+spec:
+ sink:
+ ref:
+ apiVersion: eventing.knative.dev/v1
+ kind: Broker
+ name: example-broker
+ namespace: default
+ address: https://horizon.server.example.com
+ skipTLSVerify: false
+ secretRef:
+ name: horizon-credentials
+ serviceAccountName: horizon-source-sa
diff --git a/samples/tag-new-vms/source.yaml b/samples/tag-new-vms/source.yaml
index 4a6e3178f..509d33f9e 100644
--- a/samples/tag-new-vms/source.yaml
+++ b/samples/tag-new-vms/source.yaml
@@ -1,14 +1,13 @@
apiVersion: sources.tanzu.vmware.com/v1alpha1
kind: VSphereSource
metadata:
- name: vcsim-to-broker
+ name: vcsim-to-broker
spec:
sink:
ref:
- apiVersion: eventing.knative.dev/v1alpha1
+ apiVersion: eventing.knative.dev/v1
kind: Broker
name: default
-
address: https://vcsim.default.svc.cluster.local
skipTLSVerify: true
secretRef:
diff --git a/samples/vcsim/source.yaml b/samples/vcsim/source.yaml
index abcfca032..1efef95f5 100644
--- a/samples/vcsim/source.yaml
+++ b/samples/vcsim/source.yaml
@@ -1,15 +1,14 @@
apiVersion: sources.tanzu.vmware.com/v1alpha1
kind: VSphereSource
metadata:
- name: vcsim
+ name: vcsim
spec:
- sink:
- ref:
- apiVersion: serving.knative.dev/v1
- kind: Service
- name: sockeye
-
- address: https://vcsim.default.svc.cluster.local
- skipTLSVerify: true
- secretRef:
- name: vsphere-credentials
+ sink:
+ ref:
+ apiVersion: serving.knative.dev/v1
+ kind: Service
+ name: sockeye
+ address: https://vcsim.default.svc.cluster.local
+ skipTLSVerify: true
+ secretRef:
+ name: vsphere-credentials
diff --git a/third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE b/third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE
new file mode 100644
index 000000000..ce212cb1c
--- /dev/null
+++ b/third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Ben Johnson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/VENDOR-LICENSE/github.com/go-resty/resty/v2/LICENSE b/third_party/VENDOR-LICENSE/github.com/go-resty/resty/v2/LICENSE
new file mode 100644
index 000000000..27326a653
--- /dev/null
+++ b/third_party/VENDOR-LICENSE/github.com/go-resty/resty/v2/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2021 Jeevanandam M., https://myjeeva.com
+ Simple HTTP and REST client library for Go (inspired by Ruby rest-client) Features section describes in detail about Resty capabilitiesResty
+
+
+ ++