From f02e4128a4cd0a56237670fa44b4650d763cdf98 Mon Sep 17 00:00:00 2001
From: Michael Gasch <15986659+embano1@users.noreply.github.com>
Date: Wed, 6 Jul 2022 02:30:35 +0200
Subject: [PATCH] feat: Add HorizonSource (#416)
Closes: #392
Signed-off-by: Michael Gasch
Signed-off-by: Michael Gasch <15986659+embano1@users.noreply.github.com>
---
README.md | 13 +-
cmd/horizon-adapter/kodata/HEAD | 1 +
cmd/horizon-adapter/kodata/LICENSE | 1 +
cmd/horizon-adapter/kodata/refs | 1 +
cmd/horizon-adapter/main.go | 20 +
cmd/horizon-controller/kodata/HEAD | 1 +
cmd/horizon-controller/kodata/LICENSE | 1 +
cmd/horizon-controller/kodata/refs | 1 +
cmd/horizon-controller/main.go | 22 +
cmd/horizon-webhook/kodata/HEAD | 1 +
cmd/horizon-webhook/kodata/LICENSE | 1 +
cmd/horizon-webhook/kodata/refs | 1 +
cmd/horizon-webhook/main.go | 122 +
cmd/vsphere-adapter/main.go | 6 +-
cmd/vsphere-controller/main.go | 4 +-
config/200-horizon-serviceaccount.yaml | 20 +
...m.yaml => 200-vsphere-clusterrole-cm.yaml} | 0
...role.yaml => 200-vsphere-clusterrole.yaml} | 2 +-
...here-podspecable-binding-clusterrole.yaml} | 2 +-
...t.yaml => 200-vsphere-serviceaccount.yaml} | 2 +-
config/201-horizon-clusterrole.yaml | 96 +
...ml => 201-vsphere-clusterrolebinding.yaml} | 2 +-
config/202-horizon-clusterrolebinding.yaml | 53 +
config/203-horizon-webhook-clusterrole.yaml | 111 +
config/300-horizonsource.yaml | 56 +
...g.yaml => 300-vsphere-vspherebinding.yaml} | 2 +-
config/{vsphere => }/300-vspheresource.yaml | 7 +-
config/400-horizon-controller-service.yaml | 16 +
config/400-horizon-webhook-service.yaml | 18 +
....yaml => 400-vsphere-webhook-service.yaml} | 2 +-
config/500-horizon-controller.yaml | 63 +
config/500-horizon-webhook-configuration.yaml | 63 +
config/500-horizon-webhook.yaml | 68 +
...=> 500-vsphere-webhook-configuration.yaml} | 2 +-
.../webhook.yaml => 500-vsphere-webhook.yaml} | 9 +-
config/config-logging.yaml | 4 +-
go.mod | 7 +-
go.sum | 14 +
.../v1alpha1/horizonsource_defaults.go | 23 +
.../v1alpha1/horizonsource_defaults_test.go | 70 +
.../v1alpha1/horizonsource_lifecycle.go | 76 +
.../v1alpha1/horizonsource_lifecycle_test.go | 132 +
.../sources/v1alpha1/horizonsource_types.go | 116 +
.../v1alpha1/horizonsource_types_test.go | 19 +
.../v1alpha1/horizonsource_validation.go | 65 +
.../v1alpha1/horizonsource_validation_test.go | 308 +
pkg/apis/sources/v1alpha1/register.go | 2 +
pkg/apis/sources/v1alpha1/register_test.go | 16 +-
.../sources/v1alpha1/zz_generated.deepcopy.go | 114 +
.../v1alpha1/fake/fake_horizonsource.go | 131 +
.../v1alpha1/fake/fake_sources_client.go | 4 +
.../sources/v1alpha1/generated_expansion.go | 2 +
.../typed/sources/v1alpha1/horizonsource.go | 184 +
.../typed/sources/v1alpha1/sources_client.go | 5 +
.../informers/externalversions/generic.go | 2 +
.../sources/v1alpha1/horizonsource.go | 79 +
.../sources/v1alpha1/interface.go | 7 +
pkg/client/injection/client/client.go | 131 +
.../v1alpha1/horizonsource/fake/fake.go | 29 +
.../horizonsource/filtered/fake/fake.go | 41 +
.../horizonsource/filtered/horizonsource.go | 125 +
.../v1alpha1/horizonsource/horizonsource.go | 105 +
.../v1alpha1/horizonsource/controller.go | 151 +
.../v1alpha1/horizonsource/reconciler.go | 439 +
.../sources/v1alpha1/horizonsource/state.go | 86 +
.../sources/v1alpha1/expansion_generated.go | 8 +
.../listers/sources/v1alpha1/horizonsource.go | 88 +
pkg/horizon/adapter.go | 269 +
pkg/horizon/adapter_test.go | 225 +
pkg/horizon/horizon.go | 355 +
pkg/horizon/horizon_test.go | 318 +
pkg/horizon/testdata/audit_events.golden | 94 +
pkg/horizon/types.go | 95 +
pkg/reconciler/horizonsource/controller.go | 76 +
pkg/reconciler/horizonsource/deployment.go | 151 +
pkg/reconciler/horizonsource/horizonsource.go | 173 +
.../horizonsource/resources/adapter.go | 154 +
.../horizonsource/resources/labels.go | 19 +
.../horizonsource/resources/names/names.go | 17 +
.../resources/names/names_test.go | 43 +
.../horizonsource/resources/serviceaccount.go | 27 +
.../horizonsource/serviceaccount.go | 58 +
.../horizonsource/serviceaccount_test.go | 94 +
.../vspheresource/resources/deployment.go | 1 +
.../vspheresource/resources/names/names.go | 2 +-
.../resources/names/names_test.go | 6 +-
pkg/vsphere/adapter.go | 2 +-
samples/README.md | 3 +-
samples/horizon/README.md | 97 +
samples/horizon/horizon-source.yml | 16 +
samples/tag-new-vms/source.yaml | 5 +-
samples/vcsim/source.yaml | 21 +-
.../github.com/benbjohnson/clock/LICENSE | 21 +
.../github.com/go-resty/resty/v2/LICENSE | 21 +
.../github.com/go-resty/resty/v2/.gitignore | 30 +
vendor/github.com/go-resty/resty/v2/LICENSE | 21 +
vendor/github.com/go-resty/resty/v2/README.md | 906 ++
vendor/github.com/go-resty/resty/v2/WORKSPACE | 31 +
vendor/github.com/go-resty/resty/v2/client.go | 1115 ++
.../go-resty/resty/v2/middleware.go | 543 +
.../github.com/go-resty/resty/v2/redirect.go | 101 +
.../github.com/go-resty/resty/v2/request.go | 896 ++
.../github.com/go-resty/resty/v2/response.go | 175 +
vendor/github.com/go-resty/resty/v2/resty.go | 40 +
vendor/github.com/go-resty/resty/v2/retry.go | 221 +
vendor/github.com/go-resty/resty/v2/trace.go | 130 +
.../github.com/go-resty/resty/v2/transport.go | 35 +
.../go-resty/resty/v2/transport112.go | 34 +
vendor/github.com/go-resty/resty/v2/util.go | 391 +
vendor/golang.org/x/net/publicsuffix/list.go | 181 +
vendor/golang.org/x/net/publicsuffix/table.go | 10583 ++++++++++++++++
vendor/modules.txt | 4 +
112 files changed, 21120 insertions(+), 53 deletions(-)
create mode 120000 cmd/horizon-adapter/kodata/HEAD
create mode 120000 cmd/horizon-adapter/kodata/LICENSE
create mode 120000 cmd/horizon-adapter/kodata/refs
create mode 100644 cmd/horizon-adapter/main.go
create mode 120000 cmd/horizon-controller/kodata/HEAD
create mode 120000 cmd/horizon-controller/kodata/LICENSE
create mode 120000 cmd/horizon-controller/kodata/refs
create mode 100644 cmd/horizon-controller/main.go
create mode 120000 cmd/horizon-webhook/kodata/HEAD
create mode 120000 cmd/horizon-webhook/kodata/LICENSE
create mode 120000 cmd/horizon-webhook/kodata/refs
create mode 100644 cmd/horizon-webhook/main.go
create mode 100644 config/200-horizon-serviceaccount.yaml
rename config/{vsphere/200-clusterrole-cm.yaml => 200-vsphere-clusterrole-cm.yaml} (100%)
rename config/{vsphere/200-clusterrole.yaml => 200-vsphere-clusterrole.yaml} (98%)
rename config/{vsphere/200-podspecable-binding-clusterrole.yaml => 200-vsphere-podspecable-binding-clusterrole.yaml} (97%)
rename config/{vsphere/200-serviceaccount.yaml => 200-vsphere-serviceaccount.yaml} (86%)
create mode 100644 config/201-horizon-clusterrole.yaml
rename config/{vsphere/201-clusterrolebinding.yaml => 201-vsphere-clusterrolebinding.yaml} (97%)
create mode 100644 config/202-horizon-clusterrolebinding.yaml
create mode 100644 config/203-horizon-webhook-clusterrole.yaml
create mode 100644 config/300-horizonsource.yaml
rename config/{vsphere/300-vspherebinding.yaml => 300-vsphere-vspherebinding.yaml} (97%)
rename config/{vsphere => }/300-vspheresource.yaml (89%)
create mode 100644 config/400-horizon-controller-service.yaml
create mode 100644 config/400-horizon-webhook-service.yaml
rename config/{vsphere/400-webhook-service.yaml => 400-vsphere-webhook-service.yaml} (91%)
create mode 100644 config/500-horizon-controller.yaml
create mode 100644 config/500-horizon-webhook-configuration.yaml
create mode 100644 config/500-horizon-webhook.yaml
rename config/{vsphere/500-webhook-configuration.yaml => 500-vsphere-webhook-configuration.yaml} (98%)
rename config/{vsphere/webhook.yaml => 500-vsphere-webhook.yaml} (91%)
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_defaults.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_defaults_test.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_lifecycle.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_lifecycle_test.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_types.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_types_test.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_validation.go
create mode 100644 pkg/apis/sources/v1alpha1/horizonsource_validation_test.go
create mode 100644 pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_horizonsource.go
create mode 100644 pkg/client/clientset/versioned/typed/sources/v1alpha1/horizonsource.go
create mode 100644 pkg/client/informers/externalversions/sources/v1alpha1/horizonsource.go
create mode 100644 pkg/client/injection/informers/sources/v1alpha1/horizonsource/fake/fake.go
create mode 100644 pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/fake/fake.go
create mode 100644 pkg/client/injection/informers/sources/v1alpha1/horizonsource/filtered/horizonsource.go
create mode 100644 pkg/client/injection/informers/sources/v1alpha1/horizonsource/horizonsource.go
create mode 100644 pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/controller.go
create mode 100644 pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/reconciler.go
create mode 100644 pkg/client/injection/reconciler/sources/v1alpha1/horizonsource/state.go
create mode 100644 pkg/client/listers/sources/v1alpha1/horizonsource.go
create mode 100644 pkg/horizon/adapter.go
create mode 100644 pkg/horizon/adapter_test.go
create mode 100644 pkg/horizon/horizon.go
create mode 100644 pkg/horizon/horizon_test.go
create mode 100644 pkg/horizon/testdata/audit_events.golden
create mode 100644 pkg/horizon/types.go
create mode 100644 pkg/reconciler/horizonsource/controller.go
create mode 100644 pkg/reconciler/horizonsource/deployment.go
create mode 100644 pkg/reconciler/horizonsource/horizonsource.go
create mode 100644 pkg/reconciler/horizonsource/resources/adapter.go
create mode 100644 pkg/reconciler/horizonsource/resources/labels.go
create mode 100644 pkg/reconciler/horizonsource/resources/names/names.go
create mode 100644 pkg/reconciler/horizonsource/resources/names/names_test.go
create mode 100644 pkg/reconciler/horizonsource/resources/serviceaccount.go
create mode 100644 pkg/reconciler/horizonsource/serviceaccount.go
create mode 100644 pkg/reconciler/horizonsource/serviceaccount_test.go
create mode 100644 samples/horizon/README.md
create mode 100644 samples/horizon/horizon-source.yml
create mode 100644 third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE
create mode 100644 third_party/VENDOR-LICENSE/github.com/go-resty/resty/v2/LICENSE
create mode 100644 vendor/github.com/go-resty/resty/v2/.gitignore
create mode 100644 vendor/github.com/go-resty/resty/v2/LICENSE
create mode 100644 vendor/github.com/go-resty/resty/v2/README.md
create mode 100644 vendor/github.com/go-resty/resty/v2/WORKSPACE
create mode 100644 vendor/github.com/go-resty/resty/v2/client.go
create mode 100644 vendor/github.com/go-resty/resty/v2/middleware.go
create mode 100644 vendor/github.com/go-resty/resty/v2/redirect.go
create mode 100644 vendor/github.com/go-resty/resty/v2/request.go
create mode 100644 vendor/github.com/go-resty/resty/v2/response.go
create mode 100644 vendor/github.com/go-resty/resty/v2/resty.go
create mode 100644 vendor/github.com/go-resty/resty/v2/retry.go
create mode 100644 vendor/github.com/go-resty/resty/v2/trace.go
create mode 100644 vendor/github.com/go-resty/resty/v2/transport.go
create mode 100644 vendor/github.com/go-resty/resty/v2/transport112.go
create mode 100644 vendor/github.com/go-resty/resty/v2/util.go
create mode 100644 vendor/golang.org/x/net/publicsuffix/list.go
create mode 100644 vendor/golang.org/x/net/publicsuffix/table.go
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
+
+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/vendor/github.com/go-resty/resty/v2/.gitignore b/vendor/github.com/go-resty/resty/v2/.gitignore
new file mode 100644
index 000000000..9e856bd48
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/.gitignore
@@ -0,0 +1,30 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
+
+coverage.out
+coverage.txt
+
+# Exclude intellij IDE folders
+.idea/*
diff --git a/vendor/github.com/go-resty/resty/v2/LICENSE b/vendor/github.com/go-resty/resty/v2/LICENSE
new file mode 100644
index 000000000..27326a653
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2021 Jeevanandam M., https://myjeeva.com
+
+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/vendor/github.com/go-resty/resty/v2/README.md b/vendor/github.com/go-resty/resty/v2/README.md
new file mode 100644
index 000000000..8ec651828
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/README.md
@@ -0,0 +1,906 @@
+
+
Resty
+Simple HTTP and REST client library for Go (inspired by Ruby rest-client)
+Features section describes in detail about Resty capabilities
+
+
+
![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)
+
+
+
Resty Communication Channels
+
![Twitter @go_resty](https://img.shields.io/badge/twitter-@go__resty-55acee.svg)
+
+
+## News
+
+ * v2.7.0 [released](https://github.com/go-resty/resty/releases/tag/v2.7.0) and tagged on Nov 03, 2021.
+ * v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019.
+ * v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019.
+ * v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors).
+
+## Features
+
+ * GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, etc.
+ * Simple and chainable methods for settings and request
+ * [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Request) Body can be `string`, `[]byte`, `struct`, `map`, `slice` and `io.Reader` too
+ * Auto detects `Content-Type`
+ * Buffer less processing for `io.Reader`
+ * Native `*http.Request` instance may be accessed during middleware and request execution via `Request.RawRequest`
+ * Request Body can be read multiple times via `Request.RawRequest.GetBody()`
+ * [Response](https://pkg.go.dev/github.com/go-resty/resty/v2#Response) object gives you more possibility
+ * Access as `[]byte` array - `response.Body()` OR Access as `string` - `response.String()`
+ * Know your `response.Time()` and when we `response.ReceivedAt()`
+ * Automatic marshal and unmarshal for `JSON` and `XML` content type
+ * Default is `JSON`, if you supply `struct/map` without header `Content-Type`
+ * For auto-unmarshal, refer to -
+ - Success scenario [Request.SetResult()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetResult) and [Response.Result()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Result).
+ - Error scenario [Request.SetError()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetError) and [Response.Error()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Error).
+ - Supports [RFC7807](https://tools.ietf.org/html/rfc7807) - `application/problem+json` & `application/problem+xml`
+ * Resty provides an option to override [JSON Marshal/Unmarshal and XML Marshal/Unmarshal](#override-json--xml-marshalunmarshal)
+ * Easy to upload one or more file(s) via `multipart/form-data`
+ * Auto detects file content type
+ * Request URL [Path Params (aka URI Params)](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetPathParams)
+ * Backoff Retry Mechanism with retry condition function [reference](retry_test.go)
+ * Resty client HTTP & REST [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.OnBeforeRequest) and [Response](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.OnAfterResponse) middlewares
+ * `Request.SetContext` supported
+ * Authorization option of `BasicAuth` and `Bearer` token
+ * Set request `ContentLength` value for all request or particular request
+ * Custom [Root Certificates](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetRootCertificate) and Client [Certificates](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetCertificates)
+ * Download/Save HTTP response directly into File, like `curl -o` flag. See [SetOutputDirectory](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetOutputDirectory) & [SetOutput](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetOutput).
+ * Cookies for your request and CookieJar support
+ * SRV Record based request instead of Host URL
+ * Client settings like `Timeout`, `RedirectPolicy`, `Proxy`, `TLSClientConfig`, `Transport`, etc.
+ * Optionally allows GET request with payload, see [SetAllowGetMethodPayload](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetAllowGetMethodPayload)
+ * Supports registering external JSON library into resty, see [how to use](https://github.com/go-resty/resty/issues/76#issuecomment-314015250)
+ * Exposes Response reader without reading response (no auto-unmarshaling) if need be, see [how to use](https://github.com/go-resty/resty/issues/87#issuecomment-322100604)
+ * Option to specify expected `Content-Type` when response `Content-Type` header missing. Refer to [#92](https://github.com/go-resty/resty/issues/92)
+ * Resty design
+ * Have client level settings & options and also override at Request level if you want to
+ * Request and Response middleware
+ * Create Multiple clients if you want to `resty.New()`
+ * Supports `http.RoundTripper` implementation, see [SetTransport](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetTransport)
+ * goroutine concurrent safe
+ * Resty Client trace, see [Client.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableTrace) and [Request.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableTrace)
+ * Since v2.4.0, trace info contains a `RequestAttempt` value, and the `Request` object contains an `Attempt` attribute
+ * Debug mode - clean and informative logging presentation
+ * Gzip - Go does it automatically also resty has fallback handling too
+ * Works fine with `HTTP/2` and `HTTP/1.1`
+ * [Bazel support](#bazel-support)
+ * Easily mock Resty for testing, [for e.g.](#mocking-http-requests-using-httpmock-library)
+ * Well tested client library
+
+### Included Batteries
+
+ * Redirect Policies - see [how to use](#redirect-policy)
+ * NoRedirectPolicy
+ * FlexibleRedirectPolicy
+ * DomainCheckRedirectPolicy
+ * etc. [more info](redirect.go)
+ * Retry Mechanism [how to use](#retries)
+ * Backoff Retry
+ * Conditional Retry
+ * Since v2.6.0, Retry Hooks - [Client](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.AddRetryHook), [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.AddRetryHook)
+ * SRV Record based request instead of Host URL [how to use](resty_test.go#L1412)
+ * etc (upcoming - throw your idea's [here](https://github.com/go-resty/resty/issues)).
+
+
+#### Supported Go Versions
+
+Initially Resty started supporting `go modules` since `v1.10.0` release.
+
+Starting Resty v2 and higher versions, it fully embraces [go modules](https://github.com/golang/go/wiki/Modules) package release. It requires a Go version capable of understanding `/vN` suffixed imports:
+
+- 1.9.7+
+- 1.10.3+
+- 1.11+
+
+
+## It might be beneficial for your project :smile:
+
+Resty author also published following projects for Go Community.
+
+ * [aah framework](https://aahframework.org) - A secure, flexible, rapid Go web framework.
+ * [THUMBAI](https://thumbai.app) - Go Mod Repository, Go Vanity Service and Simple Proxy Server.
+ * [go-model](https://github.com/jeevatkm/go-model) - Robust & Easy to use model mapper and utility methods for Go `struct`.
+
+
+## Installation
+
+```bash
+# Go Modules
+require github.com/go-resty/resty/v2 v2.7.0
+```
+
+## Usage
+
+The following samples will assist you to become as comfortable as possible with resty library.
+
+```go
+// Import resty into your code and refer it as `resty`.
+import "github.com/go-resty/resty/v2"
+```
+
+#### Simple GET
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+resp, err := client.R().
+ EnableTrace().
+ Get("https://httpbin.org/get")
+
+// Explore response object
+fmt.Println("Response Info:")
+fmt.Println(" Error :", err)
+fmt.Println(" Status Code:", resp.StatusCode())
+fmt.Println(" Status :", resp.Status())
+fmt.Println(" Proto :", resp.Proto())
+fmt.Println(" Time :", resp.Time())
+fmt.Println(" Received At:", resp.ReceivedAt())
+fmt.Println(" Body :\n", resp)
+fmt.Println()
+
+// Explore trace info
+fmt.Println("Request Trace Info:")
+ti := resp.Request.TraceInfo()
+fmt.Println(" DNSLookup :", ti.DNSLookup)
+fmt.Println(" ConnTime :", ti.ConnTime)
+fmt.Println(" TCPConnTime :", ti.TCPConnTime)
+fmt.Println(" TLSHandshake :", ti.TLSHandshake)
+fmt.Println(" ServerTime :", ti.ServerTime)
+fmt.Println(" ResponseTime :", ti.ResponseTime)
+fmt.Println(" TotalTime :", ti.TotalTime)
+fmt.Println(" IsConnReused :", ti.IsConnReused)
+fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle)
+fmt.Println(" ConnIdleTime :", ti.ConnIdleTime)
+fmt.Println(" RequestAttempt:", ti.RequestAttempt)
+fmt.Println(" RemoteAddr :", ti.RemoteAddr.String())
+
+/* Output
+Response Info:
+ Error :
+ Status Code: 200
+ Status : 200 OK
+ Proto : HTTP/2.0
+ Time : 457.034718ms
+ Received At: 2020-09-14 15:35:29.784681 -0700 PDT m=+0.458137045
+ Body :
+ {
+ "args": {},
+ "headers": {
+ "Accept-Encoding": "gzip",
+ "Host": "httpbin.org",
+ "User-Agent": "go-resty/2.4.0 (https://github.com/go-resty/resty)",
+ "X-Amzn-Trace-Id": "Root=1-5f5ff031-000ff6292204aa6898e4de49"
+ },
+ "origin": "0.0.0.0",
+ "url": "https://httpbin.org/get"
+ }
+
+Request Trace Info:
+ DNSLookup : 4.074657ms
+ ConnTime : 381.709936ms
+ TCPConnTime : 77.428048ms
+ TLSHandshake : 299.623597ms
+ ServerTime : 75.414703ms
+ ResponseTime : 79.337µs
+ TotalTime : 457.034718ms
+ IsConnReused : false
+ IsConnWasIdle : false
+ ConnIdleTime : 0s
+ RequestAttempt: 1
+ RemoteAddr : 3.221.81.55:443
+*/
+```
+
+#### Enhanced GET
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+resp, err := client.R().
+ SetQueryParams(map[string]string{
+ "page_no": "1",
+ "limit": "20",
+ "sort":"name",
+ "order": "asc",
+ "random":strconv.FormatInt(time.Now().Unix(), 10),
+ }).
+ SetHeader("Accept", "application/json").
+ SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
+ Get("/search_result")
+
+
+// Sample of using Request.SetQueryString method
+resp, err := client.R().
+ SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more").
+ SetHeader("Accept", "application/json").
+ SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
+ Get("/show_product")
+
+
+// If necessary, you can force response content type to tell Resty to parse a JSON response into your struct
+resp, err := client.R().
+ SetResult(result).
+ ForceContentType("application/json").
+ Get("v2/alpine/manifests/latest")
+```
+
+#### Various POST method combinations
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// POST JSON string
+// No need to set content type, if you have client level setting
+resp, err := client.R().
+ SetHeader("Content-Type", "application/json").
+ SetBody(`{"username":"testuser", "password":"testpass"}`).
+ SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
+ Post("https://myapp.com/login")
+
+// POST []byte array
+// No need to set content type, if you have client level setting
+resp, err := client.R().
+ SetHeader("Content-Type", "application/json").
+ SetBody([]byte(`{"username":"testuser", "password":"testpass"}`)).
+ SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
+ Post("https://myapp.com/login")
+
+// POST Struct, default is JSON content type. No need to set one
+resp, err := client.R().
+ SetBody(User{Username: "testuser", Password: "testpass"}).
+ SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
+ SetError(&AuthError{}). // or SetError(AuthError{}).
+ Post("https://myapp.com/login")
+
+// POST Map, default is JSON content type. No need to set one
+resp, err := client.R().
+ SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
+ SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
+ SetError(&AuthError{}). // or SetError(AuthError{}).
+ Post("https://myapp.com/login")
+
+// POST of raw bytes for file upload. For example: upload file to Dropbox
+fileBytes, _ := ioutil.ReadFile("/Users/jeeva/mydocument.pdf")
+
+// See we are not setting content-type header, since go-resty automatically detects Content-Type for you
+resp, err := client.R().
+ SetBody(fileBytes).
+ SetContentLength(true). // Dropbox expects this value
+ SetAuthToken("").
+ SetError(&DropboxError{}). // or SetError(DropboxError{}).
+ Post("https://content.dropboxapi.com/1/files_put/auto/resty/mydocument.pdf") // for upload Dropbox supports PUT too
+
+// Note: resty detects Content-Type for request body/payload if content type header is not set.
+// * For struct and map data type defaults to 'application/json'
+// * Fallback is plain text content type
+```
+
+#### Sample PUT
+
+You can use various combinations of `PUT` method call like demonstrated for `POST`.
+
+```go
+// Note: This is one sample of PUT method usage, refer POST for more combination
+
+// Create a Resty Client
+client := resty.New()
+
+// Request goes as JSON content type
+// No need to set auth token, error, if you have client level settings
+resp, err := client.R().
+ SetBody(Article{
+ Title: "go-resty",
+ Content: "This is my article content, oh ya!",
+ Author: "Jeevanandam M",
+ Tags: []string{"article", "sample", "resty"},
+ }).
+ SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
+ SetError(&Error{}). // or SetError(Error{}).
+ Put("https://myapp.com/article/1234")
+```
+
+#### Sample PATCH
+
+You can use various combinations of `PATCH` method call like demonstrated for `POST`.
+
+```go
+// Note: This is one sample of PUT method usage, refer POST for more combination
+
+// Create a Resty Client
+client := resty.New()
+
+// Request goes as JSON content type
+// No need to set auth token, error, if you have client level settings
+resp, err := client.R().
+ SetBody(Article{
+ Tags: []string{"new tag1", "new tag2"},
+ }).
+ SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
+ SetError(&Error{}). // or SetError(Error{}).
+ Patch("https://myapp.com/articles/1234")
+```
+
+#### Sample DELETE, HEAD, OPTIONS
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// DELETE a article
+// No need to set auth token, error, if you have client level settings
+resp, err := client.R().
+ SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
+ SetError(&Error{}). // or SetError(Error{}).
+ Delete("https://myapp.com/articles/1234")
+
+// DELETE a articles with payload/body as a JSON string
+// No need to set auth token, error, if you have client level settings
+resp, err := client.R().
+ SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
+ SetError(&Error{}). // or SetError(Error{}).
+ SetHeader("Content-Type", "application/json").
+ SetBody(`{article_ids: [1002, 1006, 1007, 87683, 45432] }`).
+ Delete("https://myapp.com/articles")
+
+// HEAD of resource
+// No need to set auth token, if you have client level settings
+resp, err := client.R().
+ SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
+ Head("https://myapp.com/videos/hi-res-video")
+
+// OPTIONS of resource
+// No need to set auth token, if you have client level settings
+resp, err := client.R().
+ SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
+ Options("https://myapp.com/servers/nyc-dc-01")
+```
+
+#### Override JSON & XML Marshal/Unmarshal
+
+User could register choice of JSON/XML library into resty or write your own. By default resty registers standard `encoding/json` and `encoding/xml` respectively.
+```go
+// Example of registering json-iterator
+import jsoniter "github.com/json-iterator/go"
+
+json := jsoniter.ConfigCompatibleWithStandardLibrary
+
+client := resty.New()
+client.JSONMarshal = json.Marshal
+client.JSONUnmarshal = json.Unmarshal
+
+// similarly user could do for XML too with -
+client.XMLMarshal
+client.XMLUnmarshal
+```
+
+### Multipart File(s) upload
+
+#### Using io.Reader
+
+```go
+profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png")
+notesBytes, _ := ioutil.ReadFile("/Users/jeeva/text-file.txt")
+
+// Create a Resty Client
+client := resty.New()
+
+resp, err := client.R().
+ SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)).
+ SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)).
+ SetFormData(map[string]string{
+ "first_name": "Jeevanandam",
+ "last_name": "M",
+ }).
+ Post("http://myapp.com/upload")
+```
+
+#### Using File directly from Path
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Single file scenario
+resp, err := client.R().
+ SetFile("profile_img", "/Users/jeeva/test-img.png").
+ Post("http://myapp.com/upload")
+
+// Multiple files scenario
+resp, err := client.R().
+ SetFiles(map[string]string{
+ "profile_img": "/Users/jeeva/test-img.png",
+ "notes": "/Users/jeeva/text-file.txt",
+ }).
+ Post("http://myapp.com/upload")
+
+// Multipart of form fields and files
+resp, err := client.R().
+ SetFiles(map[string]string{
+ "profile_img": "/Users/jeeva/test-img.png",
+ "notes": "/Users/jeeva/text-file.txt",
+ }).
+ SetFormData(map[string]string{
+ "first_name": "Jeevanandam",
+ "last_name": "M",
+ "zip_code": "00001",
+ "city": "my city",
+ "access_token": "C6A79608-782F-4ED0-A11D-BD82FAD829CD",
+ }).
+ Post("http://myapp.com/profile")
+```
+
+#### Sample Form submission
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// just mentioning about POST as an example with simple flow
+// User Login
+resp, err := client.R().
+ SetFormData(map[string]string{
+ "username": "jeeva",
+ "password": "mypass",
+ }).
+ Post("http://myapp.com/login")
+
+// Followed by profile update
+resp, err := client.R().
+ SetFormData(map[string]string{
+ "first_name": "Jeevanandam",
+ "last_name": "M",
+ "zip_code": "00001",
+ "city": "new city update",
+ }).
+ Post("http://myapp.com/profile")
+
+// Multi value form data
+criteria := url.Values{
+ "search_criteria": []string{"book", "glass", "pencil"},
+}
+resp, err := client.R().
+ SetFormDataFromValues(criteria).
+ Post("http://myapp.com/search")
+```
+
+#### Save HTTP Response into File
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Setting output directory path, If directory not exists then resty creates one!
+// This is optional one, if you're planning using absoule path in
+// `Request.SetOutput` and can used together.
+client.SetOutputDirectory("/Users/jeeva/Downloads")
+
+// HTTP response gets saved into file, similar to curl -o flag
+_, err := client.R().
+ SetOutput("plugin/ReplyWithHeader-v5.1-beta.zip").
+ Get("http://bit.ly/1LouEKr")
+
+// OR using absolute path
+// Note: output directory path is not used for absolute path
+_, err := client.R().
+ SetOutput("/MyDownloads/plugin/ReplyWithHeader-v5.1-beta.zip").
+ Get("http://bit.ly/1LouEKr")
+```
+
+#### Request URL Path Params
+
+Resty provides easy to use dynamic request URL path params. Params can be set at client and request level. Client level params value can be overridden at request level.
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+client.R().SetPathParams(map[string]string{
+ "userId": "sample@sample.com",
+ "subAccountId": "100002",
+}).
+Get("/v1/users/{userId}/{subAccountId}/details")
+
+// Result:
+// Composed URL - /v1/users/sample@sample.com/100002/details
+```
+
+#### Request and Response Middleware
+
+Resty provides middleware ability to manipulate for Request and Response. It is more flexible than callback approach.
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Registering Request Middleware
+client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
+ // Now you have access to Client and current Request object
+ // manipulate it as per your need
+
+ return nil // if its success otherwise return error
+ })
+
+// Registering Response Middleware
+client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
+ // Now you have access to Client and current Response object
+ // manipulate it as per your need
+
+ return nil // if its success otherwise return error
+ })
+```
+
+#### OnError Hooks
+
+Resty provides OnError hooks that may be called because:
+
+- The client failed to send the request due to connection timeout, TLS handshake failure, etc...
+- The request was retried the maximum amount of times, and still failed.
+
+If there was a response from the server, the original error will be wrapped in `*resty.ResponseError` which contains the last response received.
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+client.OnError(func(req *resty.Request, err error) {
+ if v, ok := err.(*resty.ResponseError); ok {
+ // v.Response contains the last response from the server
+ // v.Err contains the original error
+ }
+ // Log the error, increment a metric, etc...
+})
+```
+
+#### Redirect Policy
+
+Resty provides few ready to use redirect policy(s) also it supports multiple policies together.
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Assign Client Redirect Policy. Create one as per you need
+client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
+
+// Wanna multiple policies such as redirect count, domain name check, etc
+client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20),
+ resty.DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
+```
+
+##### Custom Redirect Policy
+
+Implement [RedirectPolicy](redirect.go#L20) interface and register it with resty client. Have a look [redirect.go](redirect.go) for more information.
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Using raw func into resty.SetRedirectPolicy
+client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
+ // Implement your logic here
+
+ // return nil for continue redirect otherwise return error to stop/prevent redirect
+ return nil
+}))
+
+//---------------------------------------------------
+
+// Using struct create more flexible redirect policy
+type CustomRedirectPolicy struct {
+ // variables goes here
+}
+
+func (c *CustomRedirectPolicy) Apply(req *http.Request, via []*http.Request) error {
+ // Implement your logic here
+
+ // return nil for continue redirect otherwise return error to stop/prevent redirect
+ return nil
+}
+
+// Registering in resty
+client.SetRedirectPolicy(CustomRedirectPolicy{/* initialize variables */})
+```
+
+#### Custom Root Certificates and Client Certificates
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Custom Root certificates, just supply .pem file.
+// you can add one or more root certificates, its get appended
+client.SetRootCertificate("/path/to/root/pemFile1.pem")
+client.SetRootCertificate("/path/to/root/pemFile2.pem")
+// ... and so on!
+
+// Adding Client Certificates, you add one or more certificates
+// Sample for creating certificate object
+// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
+cert1, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key")
+if err != nil {
+ log.Fatalf("ERROR client certificate: %s", err)
+}
+// ...
+
+// You add one or more certificates
+client.SetCertificates(cert1, cert2, cert3)
+```
+
+#### Custom Root Certificates and Client Certificates from string
+
+```go
+// Custom Root certificates from string
+// You can pass you certificates throught env variables as strings
+// you can add one or more root certificates, its get appended
+client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
+client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
+// ... and so on!
+
+// Adding Client Certificates, you add one or more certificates
+// Sample for creating certificate object
+// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
+cert1, err := tls.X509KeyPair([]byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"), []byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"))
+if err != nil {
+ log.Fatalf("ERROR client certificate: %s", err)
+}
+// ...
+
+// You add one or more certificates
+client.SetCertificates(cert1, cert2, cert3)
+```
+
+#### Proxy Settings - Client as well as at Request Level
+
+Default `Go` supports Proxy via environment variable `HTTP_PROXY`. Resty provides support via `SetProxy` & `RemoveProxy`.
+Choose as per your need.
+
+**Client Level Proxy** settings applied to all the request
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Setting a Proxy URL and Port
+client.SetProxy("http://proxyserver:8888")
+
+// Want to remove proxy setting
+client.RemoveProxy()
+```
+
+#### Retries
+
+Resty uses [backoff](http://www.awsarchitectureblog.com/2015/03/backoff.html)
+to increase retry intervals after each attempt.
+
+Usage example:
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Retries are configured per client
+client.
+ // Set retry count to non zero to enable retries
+ SetRetryCount(3).
+ // You can override initial retry wait time.
+ // Default is 100 milliseconds.
+ SetRetryWaitTime(5 * time.Second).
+ // MaxWaitTime can be overridden as well.
+ // Default is 2 seconds.
+ SetRetryMaxWaitTime(20 * time.Second).
+ // SetRetryAfter sets callback to calculate wait time between retries.
+ // Default (nil) implies exponential backoff with jitter
+ SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
+ return 0, errors.New("quota exceeded")
+ })
+```
+
+Above setup will result in resty retrying requests returned non nil error up to
+3 times with delay increased after each attempt.
+
+You can optionally provide client with [custom retry conditions](https://pkg.go.dev/github.com/go-resty/resty/v2#RetryConditionFunc):
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+client.AddRetryCondition(
+ // RetryConditionFunc type is for retry condition function
+ // input: non-nil Response OR request execution error
+ func(r *resty.Response, err error) bool {
+ return r.StatusCode() == http.StatusTooManyRequests
+ },
+)
+```
+
+Above example will make resty retry requests ended with `429 Too Many Requests`
+status code.
+
+Multiple retry conditions can be added.
+
+It is also possible to use `resty.Backoff(...)` to get arbitrary retry scenarios
+implemented. [Reference](retry_test.go).
+
+#### Allow GET request with Payload
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Allow GET request with Payload. This is disabled by default.
+client.SetAllowGetMethodPayload(true)
+```
+
+#### Wanna Multiple Clients
+
+```go
+// Here you go!
+// Client 1
+client1 := resty.New()
+client1.R().Get("http://httpbin.org")
+// ...
+
+// Client 2
+client2 := resty.New()
+client2.R().Head("http://httpbin.org")
+// ...
+
+// Bend it as per your need!!!
+```
+
+#### Remaining Client Settings & its Options
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Unique settings at Client level
+//--------------------------------
+// Enable debug mode
+client.SetDebug(true)
+
+// Assign Client TLSClientConfig
+// One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
+client.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
+
+// or One can disable security check (https)
+client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
+
+// Set client timeout as per your need
+client.SetTimeout(1 * time.Minute)
+
+
+// You can override all below settings and options at request level if you want to
+//--------------------------------------------------------------------------------
+// Host URL for all request. So you can use relative URL in the request
+client.SetHostURL("http://httpbin.org")
+
+// Headers for all request
+client.SetHeader("Accept", "application/json")
+client.SetHeaders(map[string]string{
+ "Content-Type": "application/json",
+ "User-Agent": "My custom User Agent String",
+ })
+
+// Cookies for all request
+client.SetCookie(&http.Cookie{
+ Name:"go-resty",
+ Value:"This is cookie value",
+ Path: "/",
+ Domain: "sample.com",
+ MaxAge: 36000,
+ HttpOnly: true,
+ Secure: false,
+ })
+client.SetCookies(cookies)
+
+// URL query parameters for all request
+client.SetQueryParam("user_id", "00001")
+client.SetQueryParams(map[string]string{ // sample of those who use this manner
+ "api_key": "api-key-here",
+ "api_secert": "api-secert",
+ })
+client.R().SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
+
+// Form data for all request. Typically used with POST and PUT
+client.SetFormData(map[string]string{
+ "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
+ })
+
+// Basic Auth for all request
+client.SetBasicAuth("myuser", "mypass")
+
+// Bearer Auth Token for all request
+client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
+
+// Enabling Content length value for all request
+client.SetContentLength(true)
+
+// Registering global Error object structure for JSON/XML request
+client.SetError(&Error{}) // or resty.SetError(Error{})
+```
+
+#### Unix Socket
+
+```go
+unixSocket := "/var/run/my_socket.sock"
+
+// Create a Go's http.Transport so we can set it in resty.
+transport := http.Transport{
+ Dial: func(_, _ string) (net.Conn, error) {
+ return net.Dial("unix", unixSocket)
+ },
+}
+
+// Create a Resty Client
+client := resty.New()
+
+// Set the previous transport that we created, set the scheme of the communication to the
+// socket and set the unixSocket as the HostURL.
+client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocket)
+
+// No need to write the host's URL on the request, just the path.
+client.R().Get("/index.html")
+```
+
+#### Bazel Support
+
+Resty can be built, tested and depended upon via [Bazel](https://bazel.build).
+For example, to run all tests:
+
+```shell
+bazel test :resty_test
+```
+
+#### Mocking http requests using [httpmock](https://github.com/jarcoal/httpmock) library
+
+In order to mock the http requests when testing your application you
+could use the `httpmock` library.
+
+When using the default resty client, you should pass the client to the library as follow:
+
+```go
+// Create a Resty Client
+client := resty.New()
+
+// Get the underlying HTTP Client and set it to Mock
+httpmock.ActivateNonDefault(client.GetClient())
+```
+
+More detailed example of mocking resty http requests using ginko could be found [here](https://github.com/jarcoal/httpmock#ginkgo--resty-example).
+
+## Versioning
+
+Resty releases versions according to [Semantic Versioning](http://semver.org)
+
+ * Resty v2 does not use `gopkg.in` service for library versioning.
+ * Resty fully adapted to `go mod` capabilities since `v1.10.0` release.
+ * Resty v1 series was using `gopkg.in` to provide versioning. `gopkg.in/resty.vX` points to appropriate tagged versions; `X` denotes version series number and it's a stable release for production use. For e.g. `gopkg.in/resty.v0`.
+ * Development takes place at the master branch. Although the code in master should always compile and test successfully, it might break API's. I aim to maintain backwards compatibility, but sometimes API's and behavior might be changed to fix a bug.
+
+## Contribution
+
+I would welcome your contribution! If you find any improvement or issue you want to fix, feel free to send a pull request, I like pull requests that include test cases for fix/enhancement. I have done my best to bring pretty good code coverage. Feel free to write tests.
+
+BTW, I'd like to know what you think about `Resty`. Kindly open an issue or send me an email; it'd mean a lot to me.
+
+## Creator
+
+[Jeevanandam M.](https://github.com/jeevatkm) (jeeva@myjeeva.com)
+
+## Core Team
+
+Have a look on [Members](https://github.com/orgs/go-resty/people) page.
+
+## Contributors
+
+Have a look on [Contributors](https://github.com/go-resty/resty/graphs/contributors) page.
+
+## License
+
+Resty released under MIT license, refer [LICENSE](LICENSE) file.
diff --git a/vendor/github.com/go-resty/resty/v2/WORKSPACE b/vendor/github.com/go-resty/resty/v2/WORKSPACE
new file mode 100644
index 000000000..9ef03e95a
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/WORKSPACE
@@ -0,0 +1,31 @@
+workspace(name = "resty")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+ name = "io_bazel_rules_go",
+ sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
+ urls = [
+ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+ "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+ ],
+)
+
+http_archive(
+ name = "bazel_gazelle",
+ sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f",
+ urls = [
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
+ "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
+ ],
+)
+
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
+
+go_rules_dependencies()
+
+go_register_toolchains(version = "1.16")
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
+
+gazelle_dependencies()
diff --git a/vendor/github.com/go-resty/resty/v2/client.go b/vendor/github.com/go-resty/resty/v2/client.go
new file mode 100644
index 000000000..1a03efa37
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/client.go
@@ -0,0 +1,1115 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math"
+ "net/http"
+ "net/url"
+ "reflect"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+)
+
+const (
+ // MethodGet HTTP method
+ MethodGet = "GET"
+
+ // MethodPost HTTP method
+ MethodPost = "POST"
+
+ // MethodPut HTTP method
+ MethodPut = "PUT"
+
+ // MethodDelete HTTP method
+ MethodDelete = "DELETE"
+
+ // MethodPatch HTTP method
+ MethodPatch = "PATCH"
+
+ // MethodHead HTTP method
+ MethodHead = "HEAD"
+
+ // MethodOptions HTTP method
+ MethodOptions = "OPTIONS"
+)
+
+var (
+ hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent")
+ hdrAcceptKey = http.CanonicalHeaderKey("Accept")
+ hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type")
+ hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length")
+ hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding")
+ hdrLocationKey = http.CanonicalHeaderKey("Location")
+
+ plainTextType = "text/plain; charset=utf-8"
+ jsonContentType = "application/json"
+ formContentType = "application/x-www-form-urlencoded"
+
+ jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`)
+ xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`)
+
+ hdrUserAgentValue = "go-resty/" + Version + " (https://github.com/go-resty/resty)"
+ bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
+)
+
+type (
+ // RequestMiddleware type is for request middleware, called before a request is sent
+ RequestMiddleware func(*Client, *Request) error
+
+ // ResponseMiddleware type is for response middleware, called after a response has been received
+ ResponseMiddleware func(*Client, *Response) error
+
+ // PreRequestHook type is for the request hook, called right before the request is sent
+ PreRequestHook func(*Client, *http.Request) error
+
+ // RequestLogCallback type is for request logs, called before the request is logged
+ RequestLogCallback func(*RequestLog) error
+
+ // ResponseLogCallback type is for response logs, called before the response is logged
+ ResponseLogCallback func(*ResponseLog) error
+
+ // ErrorHook type is for reacting to request errors, called after all retries were attempted
+ ErrorHook func(*Request, error)
+)
+
+// Client struct is used to create Resty client with client level settings,
+// these settings are applicable to all the request raised from the client.
+//
+// Resty also provides an options to override most of the client settings
+// at request level.
+type Client struct {
+ BaseURL string
+ HostURL string // Deprecated: use BaseURL instead. To be removed in v3.0.0 release.
+ QueryParam url.Values
+ FormData url.Values
+ PathParams map[string]string
+ Header http.Header
+ UserInfo *User
+ Token string
+ AuthScheme string
+ Cookies []*http.Cookie
+ Error reflect.Type
+ Debug bool
+ DisableWarn bool
+ AllowGetMethodPayload bool
+ RetryCount int
+ RetryWaitTime time.Duration
+ RetryMaxWaitTime time.Duration
+ RetryConditions []RetryConditionFunc
+ RetryHooks []OnRetryFunc
+ RetryAfter RetryAfterFunc
+ JSONMarshal func(v interface{}) ([]byte, error)
+ JSONUnmarshal func(data []byte, v interface{}) error
+ XMLMarshal func(v interface{}) ([]byte, error)
+ XMLUnmarshal func(data []byte, v interface{}) error
+
+ // HeaderAuthorizationKey is used to set/access Request Authorization header
+ // value when `SetAuthToken` option is used.
+ HeaderAuthorizationKey string
+
+ jsonEscapeHTML bool
+ setContentLength bool
+ closeConnection bool
+ notParseResponse bool
+ trace bool
+ debugBodySizeLimit int64
+ outputDirectory string
+ scheme string
+ log Logger
+ httpClient *http.Client
+ proxyURL *url.URL
+ beforeRequest []RequestMiddleware
+ udBeforeRequest []RequestMiddleware
+ preReqHook PreRequestHook
+ afterResponse []ResponseMiddleware
+ requestLog RequestLogCallback
+ responseLog ResponseLogCallback
+ errorHooks []ErrorHook
+}
+
+// User type is to hold an username and password information
+type User struct {
+ Username, Password string
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Client methods
+//___________________________________
+
+// SetHostURL method is to set Host URL in the client instance. It will be used with request
+// raised from this client with relative URL
+// // Setting HTTP address
+// client.SetHostURL("http://myjeeva.com")
+//
+// // Setting HTTPS address
+// client.SetHostURL("https://myjeeva.com")
+//
+// Deprecated: use SetBaseURL instead. To be removed in v3.0.0 release.
+func (c *Client) SetHostURL(url string) *Client {
+ c.SetBaseURL(url)
+ return c
+}
+
+// SetBaseURL method is to set Base URL in the client instance. It will be used with request
+// raised from this client with relative URL
+// // Setting HTTP address
+// client.SetBaseURL("http://myjeeva.com")
+//
+// // Setting HTTPS address
+// client.SetBaseURL("https://myjeeva.com")
+//
+// Since v2.7.0
+func (c *Client) SetBaseURL(url string) *Client {
+ c.BaseURL = strings.TrimRight(url, "/")
+ c.HostURL = c.BaseURL
+ return c
+}
+
+// SetHeader method sets a single header field and its value in the client instance.
+// These headers will be applied to all requests raised from this client instance.
+// Also it can be overridden at request level header options.
+//
+// See `Request.SetHeader` or `Request.SetHeaders`.
+//
+// For Example: To set `Content-Type` and `Accept` as `application/json`
+//
+// client.
+// SetHeader("Content-Type", "application/json").
+// SetHeader("Accept", "application/json")
+func (c *Client) SetHeader(header, value string) *Client {
+ c.Header.Set(header, value)
+ return c
+}
+
+// SetHeaders method sets multiple headers field and its values at one go in the client instance.
+// These headers will be applied to all requests raised from this client instance. Also it can be
+// overridden at request level headers options.
+//
+// See `Request.SetHeaders` or `Request.SetHeader`.
+//
+// For Example: To set `Content-Type` and `Accept` as `application/json`
+//
+// client.SetHeaders(map[string]string{
+// "Content-Type": "application/json",
+// "Accept": "application/json",
+// })
+func (c *Client) SetHeaders(headers map[string]string) *Client {
+ for h, v := range headers {
+ c.Header.Set(h, v)
+ }
+ return c
+}
+
+// SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request.
+//
+// For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
+// client.R().
+// SetHeaderVerbatim("all_lowercase", "available").
+// SetHeaderVerbatim("UPPERCASE", "available")
+//
+// Also you can override header value, which was set at client instance level.
+//
+// Since v2.6.0
+func (c *Client) SetHeaderVerbatim(header, value string) *Client {
+ c.Header[header] = []string{value}
+ return c
+}
+
+// SetCookieJar method sets custom http.CookieJar in the resty client. Its way to override default.
+//
+// For Example: sometimes we don't want to save cookies in api contacting, we can remove the default
+// CookieJar in resty client.
+//
+// client.SetCookieJar(nil)
+func (c *Client) SetCookieJar(jar http.CookieJar) *Client {
+ c.httpClient.Jar = jar
+ return c
+}
+
+// SetCookie method appends a single cookie in the client instance.
+// These cookies will be added to all the request raised from this client instance.
+// client.SetCookie(&http.Cookie{
+// Name:"go-resty",
+// Value:"This is cookie value",
+// })
+func (c *Client) SetCookie(hc *http.Cookie) *Client {
+ c.Cookies = append(c.Cookies, hc)
+ return c
+}
+
+// SetCookies method sets an array of cookies in the client instance.
+// These cookies will be added to all the request raised from this client instance.
+// cookies := []*http.Cookie{
+// &http.Cookie{
+// Name:"go-resty-1",
+// Value:"This is cookie 1 value",
+// },
+// &http.Cookie{
+// Name:"go-resty-2",
+// Value:"This is cookie 2 value",
+// },
+// }
+//
+// // Setting a cookies into resty
+// client.SetCookies(cookies)
+func (c *Client) SetCookies(cs []*http.Cookie) *Client {
+ c.Cookies = append(c.Cookies, cs...)
+ return c
+}
+
+// SetQueryParam method sets single parameter and its value in the client instance.
+// It will be formed as query string for the request.
+//
+// For Example: `search=kitchen%20papers&size=large`
+// in the URL after `?` mark. These query params will be added to all the request raised from
+// this client instance. Also it can be overridden at request level Query Param options.
+//
+// See `Request.SetQueryParam` or `Request.SetQueryParams`.
+// client.
+// SetQueryParam("search", "kitchen papers").
+// SetQueryParam("size", "large")
+func (c *Client) SetQueryParam(param, value string) *Client {
+ c.QueryParam.Set(param, value)
+ return c
+}
+
+// SetQueryParams method sets multiple parameters and their values at one go in the client instance.
+// It will be formed as query string for the request.
+//
+// For Example: `search=kitchen%20papers&size=large`
+// in the URL after `?` mark. These query params will be added to all the request raised from this
+// client instance. Also it can be overridden at request level Query Param options.
+//
+// See `Request.SetQueryParams` or `Request.SetQueryParam`.
+// client.SetQueryParams(map[string]string{
+// "search": "kitchen papers",
+// "size": "large",
+// })
+func (c *Client) SetQueryParams(params map[string]string) *Client {
+ for p, v := range params {
+ c.SetQueryParam(p, v)
+ }
+ return c
+}
+
+// SetFormData method sets Form parameters and their values in the client instance.
+// It's applicable only HTTP method `POST` and `PUT` and requets content type would be set as
+// `application/x-www-form-urlencoded`. These form data will be added to all the request raised from
+// this client instance. Also it can be overridden at request level form data.
+//
+// See `Request.SetFormData`.
+// client.SetFormData(map[string]string{
+// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
+// "user_id": "3455454545",
+// })
+func (c *Client) SetFormData(data map[string]string) *Client {
+ for k, v := range data {
+ c.FormData.Set(k, v)
+ }
+ return c
+}
+
+// SetBasicAuth method sets the basic authentication header in the HTTP request. For Example:
+// Authorization: Basic
+//
+// For Example: To set the header for username "go-resty" and password "welcome"
+// client.SetBasicAuth("go-resty", "welcome")
+//
+// This basic auth information gets added to all the request rasied from this client instance.
+// Also it can be overridden or set one at the request level is supported.
+//
+// See `Request.SetBasicAuth`.
+func (c *Client) SetBasicAuth(username, password string) *Client {
+ c.UserInfo = &User{Username: username, Password: password}
+ return c
+}
+
+// SetAuthToken method sets the auth token of the `Authorization` header for all HTTP requests.
+// The default auth scheme is `Bearer`, it can be customized with the method `SetAuthScheme`. For Example:
+// Authorization:
+//
+// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
+//
+// client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
+//
+// This auth token gets added to all the requests rasied from this client instance.
+// Also it can be overridden or set one at the request level is supported.
+//
+// See `Request.SetAuthToken`.
+func (c *Client) SetAuthToken(token string) *Client {
+ c.Token = token
+ return c
+}
+
+// SetAuthScheme method sets the auth scheme type in the HTTP request. For Example:
+// Authorization:
+//
+// For Example: To set the scheme to use OAuth
+//
+// client.SetAuthScheme("OAuth")
+//
+// This auth scheme gets added to all the requests rasied from this client instance.
+// Also it can be overridden or set one at the request level is supported.
+//
+// Information about auth schemes can be found in RFC7235 which is linked to below
+// along with the page containing the currently defined official authentication schemes:
+// https://tools.ietf.org/html/rfc7235
+// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
+//
+// See `Request.SetAuthToken`.
+func (c *Client) SetAuthScheme(scheme string) *Client {
+ c.AuthScheme = scheme
+ return c
+}
+
+// R method creates a new request instance, its used for Get, Post, Put, Delete, Patch, Head, Options, etc.
+func (c *Client) R() *Request {
+ r := &Request{
+ QueryParam: url.Values{},
+ FormData: url.Values{},
+ Header: http.Header{},
+ Cookies: make([]*http.Cookie, 0),
+
+ client: c,
+ multipartFiles: []*File{},
+ multipartFields: []*MultipartField{},
+ PathParams: map[string]string{},
+ jsonEscapeHTML: true,
+ }
+ return r
+}
+
+// NewRequest is an alias for method `R()`. Creates a new request instance, its used for
+// Get, Post, Put, Delete, Patch, Head, Options, etc.
+func (c *Client) NewRequest() *Request {
+ return c.R()
+}
+
+// OnBeforeRequest method appends request middleware into the before request chain.
+// Its gets applied after default Resty request middlewares and before request
+// been sent from Resty to host server.
+// client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
+// // Now you have access to Client and Request instance
+// // manipulate it as per your need
+//
+// return nil // if its success otherwise return error
+// })
+func (c *Client) OnBeforeRequest(m RequestMiddleware) *Client {
+ c.udBeforeRequest = append(c.udBeforeRequest, m)
+ return c
+}
+
+// OnAfterResponse method appends response middleware into the after response chain.
+// Once we receive response from host server, default Resty response middleware
+// gets applied and then user assigened response middlewares applied.
+// client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
+// // Now you have access to Client and Response instance
+// // manipulate it as per your need
+//
+// return nil // if its success otherwise return error
+// })
+func (c *Client) OnAfterResponse(m ResponseMiddleware) *Client {
+ c.afterResponse = append(c.afterResponse, m)
+ return c
+}
+
+// OnError method adds a callback that will be run whenever a request execution fails.
+// This is called after all retries have been attempted (if any).
+// If there was a response from the server, the error will be wrapped in *ResponseError
+// which has the last response received from the server.
+//
+// client.OnError(func(req *resty.Request, err error) {
+// if v, ok := err.(*resty.ResponseError); ok {
+// // Do something with v.Response
+// }
+// // Log the error, increment a metric, etc...
+// })
+func (c *Client) OnError(h ErrorHook) *Client {
+ c.errorHooks = append(c.errorHooks, h)
+ return c
+}
+
+// SetPreRequestHook method sets the given pre-request function into resty client.
+// It is called right before the request is fired.
+//
+// Note: Only one pre-request hook can be registered. Use `client.OnBeforeRequest` for mutilple.
+func (c *Client) SetPreRequestHook(h PreRequestHook) *Client {
+ if c.preReqHook != nil {
+ c.log.Warnf("Overwriting an existing pre-request hook: %s", functionName(h))
+ }
+ c.preReqHook = h
+ return c
+}
+
+// SetDebug method enables the debug mode on Resty client. Client logs details of every request and response.
+// For `Request` it logs information such as HTTP verb, Relative URL path, Host, Headers, Body if it has one.
+// For `Response` it logs information such as Status, Response Time, Headers, Body if it has one.
+// client.SetDebug(true)
+func (c *Client) SetDebug(d bool) *Client {
+ c.Debug = d
+ return c
+}
+
+// SetDebugBodyLimit sets the maximum size for which the response and request body will be logged in debug mode.
+// client.SetDebugBodyLimit(1000000)
+func (c *Client) SetDebugBodyLimit(sl int64) *Client {
+ c.debugBodySizeLimit = sl
+ return c
+}
+
+// OnRequestLog method used to set request log callback into Resty. Registered callback gets
+// called before the resty actually logs the information.
+func (c *Client) OnRequestLog(rl RequestLogCallback) *Client {
+ if c.requestLog != nil {
+ c.log.Warnf("Overwriting an existing on-request-log callback from=%s to=%s",
+ functionName(c.requestLog), functionName(rl))
+ }
+ c.requestLog = rl
+ return c
+}
+
+// OnResponseLog method used to set response log callback into Resty. Registered callback gets
+// called before the resty actually logs the information.
+func (c *Client) OnResponseLog(rl ResponseLogCallback) *Client {
+ if c.responseLog != nil {
+ c.log.Warnf("Overwriting an existing on-response-log callback from=%s to=%s",
+ functionName(c.responseLog), functionName(rl))
+ }
+ c.responseLog = rl
+ return c
+}
+
+// SetDisableWarn method disables the warning message on Resty client.
+//
+// For Example: Resty warns the user when BasicAuth used on non-TLS mode.
+// client.SetDisableWarn(true)
+func (c *Client) SetDisableWarn(d bool) *Client {
+ c.DisableWarn = d
+ return c
+}
+
+// SetAllowGetMethodPayload method allows the GET method with payload on Resty client.
+//
+// For Example: Resty allows the user sends request with a payload on HTTP GET method.
+// client.SetAllowGetMethodPayload(true)
+func (c *Client) SetAllowGetMethodPayload(a bool) *Client {
+ c.AllowGetMethodPayload = a
+ return c
+}
+
+// SetLogger method sets given writer for logging Resty request and response details.
+//
+// Compliant to interface `resty.Logger`.
+func (c *Client) SetLogger(l Logger) *Client {
+ c.log = l
+ return c
+}
+
+// SetContentLength method enables the HTTP header `Content-Length` value for every request.
+// By default Resty won't set `Content-Length`.
+// client.SetContentLength(true)
+//
+// Also you have an option to enable for particular request. See `Request.SetContentLength`
+func (c *Client) SetContentLength(l bool) *Client {
+ c.setContentLength = l
+ return c
+}
+
+// SetTimeout method sets timeout for request raised from client.
+// client.SetTimeout(time.Duration(1 * time.Minute))
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.httpClient.Timeout = timeout
+ return c
+}
+
+// SetError method is to register the global or client common `Error` object into Resty.
+// It is used for automatic unmarshalling if response status code is greater than 399 and
+// content type either JSON or XML. Can be pointer or non-pointer.
+// client.SetError(&Error{})
+// // OR
+// client.SetError(Error{})
+func (c *Client) SetError(err interface{}) *Client {
+ c.Error = typeOf(err)
+ return c
+}
+
+// SetRedirectPolicy method sets the client redirect poilicy. Resty provides ready to use
+// redirect policies. Wanna create one for yourself refer to `redirect.go`.
+//
+// client.SetRedirectPolicy(FlexibleRedirectPolicy(20))
+//
+// // Need multiple redirect policies together
+// client.SetRedirectPolicy(FlexibleRedirectPolicy(20), DomainCheckRedirectPolicy("host1.com", "host2.net"))
+func (c *Client) SetRedirectPolicy(policies ...interface{}) *Client {
+ for _, p := range policies {
+ if _, ok := p.(RedirectPolicy); !ok {
+ c.log.Errorf("%v does not implement resty.RedirectPolicy (missing Apply method)",
+ functionName(p))
+ }
+ }
+
+ c.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ for _, p := range policies {
+ if err := p.(RedirectPolicy).Apply(req, via); err != nil {
+ return err
+ }
+ }
+ return nil // looks good, go ahead
+ }
+
+ return c
+}
+
+// SetRetryCount method enables retry on Resty client and allows you
+// to set no. of retry count. Resty uses a Backoff mechanism.
+func (c *Client) SetRetryCount(count int) *Client {
+ c.RetryCount = count
+ return c
+}
+
+// SetRetryWaitTime method sets default wait time to sleep before retrying
+// request.
+//
+// Default is 100 milliseconds.
+func (c *Client) SetRetryWaitTime(waitTime time.Duration) *Client {
+ c.RetryWaitTime = waitTime
+ return c
+}
+
+// SetRetryMaxWaitTime method sets max wait time to sleep before retrying
+// request.
+//
+// Default is 2 seconds.
+func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client {
+ c.RetryMaxWaitTime = maxWaitTime
+ return c
+}
+
+// SetRetryAfter sets callback to calculate wait time between retries.
+// Default (nil) implies exponential backoff with jitter
+func (c *Client) SetRetryAfter(callback RetryAfterFunc) *Client {
+ c.RetryAfter = callback
+ return c
+}
+
+// AddRetryCondition method adds a retry condition function to array of functions
+// that are checked to determine if the request is retried. The request will
+// retry if any of the functions return true and error is nil.
+//
+// Note: These retry conditions are applied on all Request made using this Client.
+// For Request specific retry conditions check *Request.AddRetryCondition
+func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client {
+ c.RetryConditions = append(c.RetryConditions, condition)
+ return c
+}
+
+// AddRetryAfterErrorCondition adds the basic condition of retrying after encountering
+// an error from the http response
+//
+// Since v2.6.0
+func (c *Client) AddRetryAfterErrorCondition() *Client {
+ c.AddRetryCondition(func(response *Response, err error) bool {
+ return response.IsError()
+ })
+ return c
+}
+
+// AddRetryHook adds a side-effecting retry hook to an array of hooks
+// that will be executed on each retry.
+//
+// Since v2.6.0
+func (c *Client) AddRetryHook(hook OnRetryFunc) *Client {
+ c.RetryHooks = append(c.RetryHooks, hook)
+ return c
+}
+
+// SetTLSClientConfig method sets TLSClientConfig for underling client Transport.
+//
+// For Example:
+// // One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
+// client.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
+//
+// // or One can disable security check (https)
+// client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
+//
+// Note: This method overwrites existing `TLSClientConfig`.
+func (c *Client) SetTLSClientConfig(config *tls.Config) *Client {
+ transport, err := c.transport()
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+ transport.TLSClientConfig = config
+ return c
+}
+
+// SetProxy method sets the Proxy URL and Port for Resty client.
+// client.SetProxy("http://proxyserver:8888")
+//
+// OR Without this `SetProxy` method, you could also set Proxy via environment variable.
+//
+// Refer to godoc `http.ProxyFromEnvironment`.
+func (c *Client) SetProxy(proxyURL string) *Client {
+ transport, err := c.transport()
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+
+ pURL, err := url.Parse(proxyURL)
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+
+ c.proxyURL = pURL
+ transport.Proxy = http.ProxyURL(c.proxyURL)
+ return c
+}
+
+// RemoveProxy method removes the proxy configuration from Resty client
+// client.RemoveProxy()
+func (c *Client) RemoveProxy() *Client {
+ transport, err := c.transport()
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+ c.proxyURL = nil
+ transport.Proxy = nil
+ return c
+}
+
+// SetCertificates method helps to set client certificates into Resty conveniently.
+func (c *Client) SetCertificates(certs ...tls.Certificate) *Client {
+ config, err := c.tlsConfig()
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+ config.Certificates = append(config.Certificates, certs...)
+ return c
+}
+
+// SetRootCertificate method helps to add one or more root certificates into Resty client
+// client.SetRootCertificate("/path/to/root/pemFile.pem")
+func (c *Client) SetRootCertificate(pemFilePath string) *Client {
+ rootPemData, err := ioutil.ReadFile(pemFilePath)
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+
+ config, err := c.tlsConfig()
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+ if config.RootCAs == nil {
+ config.RootCAs = x509.NewCertPool()
+ }
+
+ config.RootCAs.AppendCertsFromPEM(rootPemData)
+ return c
+}
+
+// SetRootCertificateFromString method helps to add one or more root certificates into Resty client
+// client.SetRootCertificateFromString("pem file content")
+func (c *Client) SetRootCertificateFromString(pemContent string) *Client {
+ config, err := c.tlsConfig()
+ if err != nil {
+ c.log.Errorf("%v", err)
+ return c
+ }
+ if config.RootCAs == nil {
+ config.RootCAs = x509.NewCertPool()
+ }
+
+ config.RootCAs.AppendCertsFromPEM([]byte(pemContent))
+ return c
+}
+
+// SetOutputDirectory method sets output directory for saving HTTP response into file.
+// If the output directory not exists then resty creates one. This setting is optional one,
+// if you're planning using absolute path in `Request.SetOutput` and can used together.
+// client.SetOutputDirectory("/save/http/response/here")
+func (c *Client) SetOutputDirectory(dirPath string) *Client {
+ c.outputDirectory = dirPath
+ return c
+}
+
+// SetTransport method sets custom `*http.Transport` or any `http.RoundTripper`
+// compatible interface implementation in the resty client.
+//
+// Note:
+//
+// - If transport is not type of `*http.Transport` then you may not be able to
+// take advantage of some of the Resty client settings.
+//
+// - It overwrites the Resty client transport instance and it's configurations.
+//
+// transport := &http.Transport{
+// // somthing like Proxying to httptest.Server, etc...
+// Proxy: func(req *http.Request) (*url.URL, error) {
+// return url.Parse(server.URL)
+// },
+// }
+//
+// client.SetTransport(transport)
+func (c *Client) SetTransport(transport http.RoundTripper) *Client {
+ if transport != nil {
+ c.httpClient.Transport = transport
+ }
+ return c
+}
+
+// SetScheme method sets custom scheme in the Resty client. It's way to override default.
+// client.SetScheme("http")
+func (c *Client) SetScheme(scheme string) *Client {
+ if !IsStringEmpty(scheme) {
+ c.scheme = strings.TrimSpace(scheme)
+ }
+ return c
+}
+
+// SetCloseConnection method sets variable `Close` in http request struct with the given
+// value. More info: https://golang.org/src/net/http/request.go
+func (c *Client) SetCloseConnection(close bool) *Client {
+ c.closeConnection = close
+ return c
+}
+
+// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically.
+// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body,
+// otherwise you might get into connection leaks, no connection reuse.
+//
+// Note: Response middlewares are not applicable, if you use this option. Basically you have
+// taken over the control of response parsing from `Resty`.
+func (c *Client) SetDoNotParseResponse(parse bool) *Client {
+ c.notParseResponse = parse
+ return c
+}
+
+// SetPathParam method sets single URL path key-value pair in the
+// Resty client instance.
+// client.SetPathParam("userId", "sample@sample.com")
+//
+// Result:
+// URL - /v1/users/{userId}/details
+// Composed URL - /v1/users/sample@sample.com/details
+// It replaces the value of the key while composing the request URL.
+//
+// Also it can be overridden at request level Path Params options,
+// see `Request.SetPathParam` or `Request.SetPathParams`.
+func (c *Client) SetPathParam(param, value string) *Client {
+ c.PathParams[param] = value
+ return c
+}
+
+// SetPathParams method sets multiple URL path key-value pairs at one go in the
+// Resty client instance.
+// client.SetPathParams(map[string]string{
+// "userId": "sample@sample.com",
+// "subAccountId": "100002",
+// })
+//
+// Result:
+// URL - /v1/users/{userId}/{subAccountId}/details
+// Composed URL - /v1/users/sample@sample.com/100002/details
+// It replaces the value of the key while composing the request URL.
+//
+// Also it can be overridden at request level Path Params options,
+// see `Request.SetPathParam` or `Request.SetPathParams`.
+func (c *Client) SetPathParams(params map[string]string) *Client {
+ for p, v := range params {
+ c.SetPathParam(p, v)
+ }
+ return c
+}
+
+// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal.
+//
+// Note: This option only applicable to standard JSON Marshaller.
+func (c *Client) SetJSONEscapeHTML(b bool) *Client {
+ c.jsonEscapeHTML = b
+ return c
+}
+
+// EnableTrace method enables the Resty client trace for the requests fired from
+// the client using `httptrace.ClientTrace` and provides insights.
+//
+// client := resty.New().EnableTrace()
+//
+// resp, err := client.R().Get("https://httpbin.org/get")
+// fmt.Println("Error:", err)
+// fmt.Println("Trace Info:", resp.Request.TraceInfo())
+//
+// Also `Request.EnableTrace` available too to get trace info for single request.
+//
+// Since v2.0.0
+func (c *Client) EnableTrace() *Client {
+ c.trace = true
+ return c
+}
+
+// DisableTrace method disables the Resty client trace. Refer to `Client.EnableTrace`.
+//
+// Since v2.0.0
+func (c *Client) DisableTrace() *Client {
+ c.trace = false
+ return c
+}
+
+// IsProxySet method returns the true is proxy is set from resty client otherwise
+// false. By default proxy is set from environment, refer to `http.ProxyFromEnvironment`.
+func (c *Client) IsProxySet() bool {
+ return c.proxyURL != nil
+}
+
+// GetClient method returns the current `http.Client` used by the resty client.
+func (c *Client) GetClient() *http.Client {
+ return c.httpClient
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Client Unexported methods
+//_______________________________________________________________________
+
+// Executes method executes the given `Request` object and returns response
+// error.
+func (c *Client) execute(req *Request) (*Response, error) {
+ // Apply Request middleware
+ var err error
+
+ // user defined on before request methods
+ // to modify the *resty.Request object
+ for _, f := range c.udBeforeRequest {
+ if err = f(c, req); err != nil {
+ return nil, wrapNoRetryErr(err)
+ }
+ }
+
+ // resty middlewares
+ for _, f := range c.beforeRequest {
+ if err = f(c, req); err != nil {
+ return nil, wrapNoRetryErr(err)
+ }
+ }
+
+ if hostHeader := req.Header.Get("Host"); hostHeader != "" {
+ req.RawRequest.Host = hostHeader
+ }
+
+ // call pre-request if defined
+ if c.preReqHook != nil {
+ if err = c.preReqHook(c, req.RawRequest); err != nil {
+ return nil, wrapNoRetryErr(err)
+ }
+ }
+
+ if err = requestLogger(c, req); err != nil {
+ return nil, wrapNoRetryErr(err)
+ }
+
+ req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf)
+
+ req.Time = time.Now()
+ resp, err := c.httpClient.Do(req.RawRequest)
+
+ response := &Response{
+ Request: req,
+ RawResponse: resp,
+ }
+
+ if err != nil || req.notParseResponse || c.notParseResponse {
+ response.setReceivedAt()
+ return response, err
+ }
+
+ if !req.isSaveResponse {
+ defer closeq(resp.Body)
+ body := resp.Body
+
+ // GitHub #142 & #187
+ if strings.EqualFold(resp.Header.Get(hdrContentEncodingKey), "gzip") && resp.ContentLength != 0 {
+ if _, ok := body.(*gzip.Reader); !ok {
+ body, err = gzip.NewReader(body)
+ if err != nil {
+ response.setReceivedAt()
+ return response, err
+ }
+ defer closeq(body)
+ }
+ }
+
+ if response.body, err = ioutil.ReadAll(body); err != nil {
+ response.setReceivedAt()
+ return response, err
+ }
+
+ response.size = int64(len(response.body))
+ }
+
+ response.setReceivedAt() // after we read the body
+
+ // Apply Response middleware
+ for _, f := range c.afterResponse {
+ if err = f(c, response); err != nil {
+ break
+ }
+ }
+
+ return response, wrapNoRetryErr(err)
+}
+
+// getting TLS client config if not exists then create one
+func (c *Client) tlsConfig() (*tls.Config, error) {
+ transport, err := c.transport()
+ if err != nil {
+ return nil, err
+ }
+ if transport.TLSClientConfig == nil {
+ transport.TLSClientConfig = &tls.Config{}
+ }
+ return transport.TLSClientConfig, nil
+}
+
+// Transport method returns `*http.Transport` currently in use or error
+// in case currently used `transport` is not a `*http.Transport`.
+func (c *Client) transport() (*http.Transport, error) {
+ if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
+ return transport, nil
+ }
+ return nil, errors.New("current transport is not an *http.Transport instance")
+}
+
+// just an internal helper method
+func (c *Client) outputLogTo(w io.Writer) *Client {
+ c.log.(*logger).l.SetOutput(w)
+ return c
+}
+
+// ResponseError is a wrapper for including the server response with an error.
+// Neither the err nor the response should be nil.
+type ResponseError struct {
+ Response *Response
+ Err error
+}
+
+func (e *ResponseError) Error() string {
+ return e.Err.Error()
+}
+
+func (e *ResponseError) Unwrap() error {
+ return e.Err
+}
+
+// Helper to run onErrorHooks hooks.
+// It wraps the error in a ResponseError if the resp is not nil
+// so hooks can access it.
+func (c *Client) onErrorHooks(req *Request, resp *Response, err error) {
+ if err != nil {
+ if resp != nil { // wrap with ResponseError
+ err = &ResponseError{Response: resp, Err: err}
+ }
+ for _, h := range c.errorHooks {
+ h(req, err)
+ }
+ }
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// File struct and its methods
+//_______________________________________________________________________
+
+// File struct represent file information for multipart request
+type File struct {
+ Name string
+ ParamName string
+ io.Reader
+}
+
+// String returns string value of current file details
+func (f *File) String() string {
+ return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name)
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// MultipartField struct
+//_______________________________________________________________________
+
+// MultipartField struct represent custom data part for multipart request
+type MultipartField struct {
+ Param string
+ FileName string
+ ContentType string
+ io.Reader
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Unexported package methods
+//_______________________________________________________________________
+
+func createClient(hc *http.Client) *Client {
+ if hc.Transport == nil {
+ hc.Transport = createTransport(nil)
+ }
+
+ c := &Client{ // not setting lang default values
+ QueryParam: url.Values{},
+ FormData: url.Values{},
+ Header: http.Header{},
+ Cookies: make([]*http.Cookie, 0),
+ RetryWaitTime: defaultWaitTime,
+ RetryMaxWaitTime: defaultMaxWaitTime,
+ PathParams: make(map[string]string),
+ JSONMarshal: json.Marshal,
+ JSONUnmarshal: json.Unmarshal,
+ XMLMarshal: xml.Marshal,
+ XMLUnmarshal: xml.Unmarshal,
+ HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"),
+
+ jsonEscapeHTML: true,
+ httpClient: hc,
+ debugBodySizeLimit: math.MaxInt32,
+ }
+
+ // Logger
+ c.SetLogger(createLogger())
+
+ // default before request middlewares
+ c.beforeRequest = []RequestMiddleware{
+ parseRequestURL,
+ parseRequestHeader,
+ parseRequestBody,
+ createHTTPRequest,
+ addCredentials,
+ }
+
+ // user defined request middlewares
+ c.udBeforeRequest = []RequestMiddleware{}
+
+ // default after response middlewares
+ c.afterResponse = []ResponseMiddleware{
+ responseLogger,
+ parseResponseBody,
+ saveResponseIntoFile,
+ }
+
+ return c
+}
diff --git a/vendor/github.com/go-resty/resty/v2/middleware.go b/vendor/github.com/go-resty/resty/v2/middleware.go
new file mode 100644
index 000000000..0e8ac2b69
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/middleware.go
@@ -0,0 +1,543 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "time"
+)
+
+const debugRequestLogKey = "__restyDebugRequestLog"
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Request Middleware(s)
+//_______________________________________________________________________
+
+func parseRequestURL(c *Client, r *Request) error {
+ // GitHub #103 Path Params
+ if len(r.PathParams) > 0 {
+ for p, v := range r.PathParams {
+ r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
+ }
+ }
+ if len(c.PathParams) > 0 {
+ for p, v := range c.PathParams {
+ r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
+ }
+ }
+
+ // Parsing request URL
+ reqURL, err := url.Parse(r.URL)
+ if err != nil {
+ return err
+ }
+
+ // If Request.URL is relative path then added c.HostURL into
+ // the request URL otherwise Request.URL will be used as-is
+ if !reqURL.IsAbs() {
+ r.URL = reqURL.String()
+ if len(r.URL) > 0 && r.URL[0] != '/' {
+ r.URL = "/" + r.URL
+ }
+
+ reqURL, err = url.Parse(c.HostURL + r.URL)
+ if err != nil {
+ return err
+ }
+ }
+
+ // GH #407 && #318
+ if reqURL.Scheme == "" && len(c.scheme) > 0 {
+ reqURL.Scheme = c.scheme
+ }
+
+ // Adding Query Param
+ query := make(url.Values)
+ for k, v := range c.QueryParam {
+ for _, iv := range v {
+ query.Add(k, iv)
+ }
+ }
+
+ for k, v := range r.QueryParam {
+ // remove query param from client level by key
+ // since overrides happens for that key in the request
+ query.Del(k)
+
+ for _, iv := range v {
+ query.Add(k, iv)
+ }
+ }
+
+ // GitHub #123 Preserve query string order partially.
+ // Since not feasible in `SetQuery*` resty methods, because
+ // standard package `url.Encode(...)` sorts the query params
+ // alphabetically
+ if len(query) > 0 {
+ if IsStringEmpty(reqURL.RawQuery) {
+ reqURL.RawQuery = query.Encode()
+ } else {
+ reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode()
+ }
+ }
+
+ r.URL = reqURL.String()
+
+ return nil
+}
+
+func parseRequestHeader(c *Client, r *Request) error {
+ hdr := make(http.Header)
+ for k := range c.Header {
+ hdr[k] = append(hdr[k], c.Header[k]...)
+ }
+
+ for k := range r.Header {
+ hdr.Del(k)
+ hdr[k] = append(hdr[k], r.Header[k]...)
+ }
+
+ if IsStringEmpty(hdr.Get(hdrUserAgentKey)) {
+ hdr.Set(hdrUserAgentKey, hdrUserAgentValue)
+ }
+
+ ct := hdr.Get(hdrContentTypeKey)
+ if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(ct) &&
+ (IsJSONType(ct) || IsXMLType(ct)) {
+ hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey))
+ }
+
+ r.Header = hdr
+
+ return nil
+}
+
+func parseRequestBody(c *Client, r *Request) (err error) {
+ if isPayloadSupported(r.Method, c.AllowGetMethodPayload) {
+ // Handling Multipart
+ if r.isMultiPart && !(r.Method == MethodPatch) {
+ if err = handleMultipart(c, r); err != nil {
+ return
+ }
+
+ goto CL
+ }
+
+ // Handling Form Data
+ if len(c.FormData) > 0 || len(r.FormData) > 0 {
+ handleFormData(c, r)
+
+ goto CL
+ }
+
+ // Handling Request body
+ if r.Body != nil {
+ handleContentType(c, r)
+
+ if err = handleRequestBody(c, r); err != nil {
+ return
+ }
+ }
+ }
+
+CL:
+ // by default resty won't set content length, you can if you want to :)
+ if (c.setContentLength || r.setContentLength) && r.bodyBuf != nil {
+ r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len()))
+ }
+
+ return
+}
+
+func createHTTPRequest(c *Client, r *Request) (err error) {
+ if r.bodyBuf == nil {
+ if reader, ok := r.Body.(io.Reader); ok {
+ r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader)
+ } else if c.setContentLength || r.setContentLength {
+ r.RawRequest, err = http.NewRequest(r.Method, r.URL, http.NoBody)
+ } else {
+ r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil)
+ }
+ } else {
+ r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf)
+ }
+
+ if err != nil {
+ return
+ }
+
+ // Assign close connection option
+ r.RawRequest.Close = c.closeConnection
+
+ // Add headers into http request
+ r.RawRequest.Header = r.Header
+
+ // Add cookies from client instance into http request
+ for _, cookie := range c.Cookies {
+ r.RawRequest.AddCookie(cookie)
+ }
+
+ // Add cookies from request instance into http request
+ for _, cookie := range r.Cookies {
+ r.RawRequest.AddCookie(cookie)
+ }
+
+ // Enable trace
+ if c.trace || r.trace {
+ r.clientTrace = &clientTrace{}
+ r.ctx = r.clientTrace.createContext(r.Context())
+ }
+
+ // Use context if it was specified
+ if r.ctx != nil {
+ r.RawRequest = r.RawRequest.WithContext(r.ctx)
+ }
+
+ bodyCopy, err := getBodyCopy(r)
+ if err != nil {
+ return err
+ }
+
+ // assign get body func for the underlying raw request instance
+ r.RawRequest.GetBody = func() (io.ReadCloser, error) {
+ if bodyCopy != nil {
+ return ioutil.NopCloser(bytes.NewReader(bodyCopy.Bytes())), nil
+ }
+ return nil, nil
+ }
+
+ return
+}
+
+func addCredentials(c *Client, r *Request) error {
+ var isBasicAuth bool
+ // Basic Auth
+ if r.UserInfo != nil { // takes precedence
+ r.RawRequest.SetBasicAuth(r.UserInfo.Username, r.UserInfo.Password)
+ isBasicAuth = true
+ } else if c.UserInfo != nil {
+ r.RawRequest.SetBasicAuth(c.UserInfo.Username, c.UserInfo.Password)
+ isBasicAuth = true
+ }
+
+ if !c.DisableWarn {
+ if isBasicAuth && !strings.HasPrefix(r.URL, "https") {
+ c.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS")
+ }
+ }
+
+ // Set the Authorization Header Scheme
+ var authScheme string
+ if !IsStringEmpty(r.AuthScheme) {
+ authScheme = r.AuthScheme
+ } else if !IsStringEmpty(c.AuthScheme) {
+ authScheme = c.AuthScheme
+ } else {
+ authScheme = "Bearer"
+ }
+
+ // Build the Token Auth header
+ if !IsStringEmpty(r.Token) { // takes precedence
+ r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+r.Token)
+ } else if !IsStringEmpty(c.Token) {
+ r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+c.Token)
+ }
+
+ return nil
+}
+
+func requestLogger(c *Client, r *Request) error {
+ if c.Debug {
+ rr := r.RawRequest
+ rl := &RequestLog{Header: copyHeaders(rr.Header), Body: r.fmtBodyString(c.debugBodySizeLimit)}
+ if c.requestLog != nil {
+ if err := c.requestLog(rl); err != nil {
+ return err
+ }
+ }
+ // fmt.Sprintf("COOKIES:\n%s\n", composeCookies(c.GetClient().Jar, *rr.URL)) +
+
+ reqLog := "\n==============================================================================\n" +
+ "~~~ REQUEST ~~~\n" +
+ fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) +
+ fmt.Sprintf("HOST : %s\n", rr.URL.Host) +
+ fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) +
+ fmt.Sprintf("BODY :\n%v\n", rl.Body) +
+ "------------------------------------------------------------------------------\n"
+
+ r.initValuesMap()
+ r.values[debugRequestLogKey] = reqLog
+ }
+
+ return nil
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Response Middleware(s)
+//_______________________________________________________________________
+
+func responseLogger(c *Client, res *Response) error {
+ if c.Debug {
+ rl := &ResponseLog{Header: copyHeaders(res.Header()), Body: res.fmtBodyString(c.debugBodySizeLimit)}
+ if c.responseLog != nil {
+ if err := c.responseLog(rl); err != nil {
+ return err
+ }
+ }
+
+ debugLog := res.Request.values[debugRequestLogKey].(string)
+ debugLog += "~~~ RESPONSE ~~~\n" +
+ fmt.Sprintf("STATUS : %s\n", res.Status()) +
+ fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) +
+ fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) +
+ fmt.Sprintf("TIME DURATION: %v\n", res.Time()) +
+ "HEADERS :\n" +
+ composeHeaders(c, res.Request, rl.Header) + "\n"
+ if res.Request.isSaveResponse {
+ debugLog += "BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n"
+ } else {
+ debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body)
+ }
+ debugLog += "==============================================================================\n"
+
+ c.log.Debugf("%s", debugLog)
+ }
+
+ return nil
+}
+
+func parseResponseBody(c *Client, res *Response) (err error) {
+ if res.StatusCode() == http.StatusNoContent {
+ return
+ }
+ // Handles only JSON or XML content type
+ ct := firstNonEmpty(res.Request.forceContentType, res.Header().Get(hdrContentTypeKey), res.Request.fallbackContentType)
+ if IsJSONType(ct) || IsXMLType(ct) {
+ // HTTP status code > 199 and < 300, considered as Result
+ if res.IsSuccess() {
+ res.Request.Error = nil
+ if res.Request.Result != nil {
+ err = Unmarshalc(c, ct, res.body, res.Request.Result)
+ return
+ }
+ }
+
+ // HTTP status code > 399, considered as Error
+ if res.IsError() {
+ // global error interface
+ if res.Request.Error == nil && c.Error != nil {
+ res.Request.Error = reflect.New(c.Error).Interface()
+ }
+
+ if res.Request.Error != nil {
+ err = Unmarshalc(c, ct, res.body, res.Request.Error)
+ }
+ }
+ }
+
+ return
+}
+
+func handleMultipart(c *Client, r *Request) (err error) {
+ r.bodyBuf = acquireBuffer()
+ w := multipart.NewWriter(r.bodyBuf)
+
+ for k, v := range c.FormData {
+ for _, iv := range v {
+ if err = w.WriteField(k, iv); err != nil {
+ return err
+ }
+ }
+ }
+
+ for k, v := range r.FormData {
+ for _, iv := range v {
+ if strings.HasPrefix(k, "@") { // file
+ err = addFile(w, k[1:], iv)
+ if err != nil {
+ return
+ }
+ } else { // form value
+ if err = w.WriteField(k, iv); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ // #21 - adding io.Reader support
+ if len(r.multipartFiles) > 0 {
+ for _, f := range r.multipartFiles {
+ err = addFileReader(w, f)
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ // GitHub #130 adding multipart field support with content type
+ if len(r.multipartFields) > 0 {
+ for _, mf := range r.multipartFields {
+ if err = addMultipartFormField(w, mf); err != nil {
+ return
+ }
+ }
+ }
+
+ r.Header.Set(hdrContentTypeKey, w.FormDataContentType())
+ err = w.Close()
+
+ return
+}
+
+func handleFormData(c *Client, r *Request) {
+ formData := url.Values{}
+
+ for k, v := range c.FormData {
+ for _, iv := range v {
+ formData.Add(k, iv)
+ }
+ }
+
+ for k, v := range r.FormData {
+ // remove form data field from client level by key
+ // since overrides happens for that key in the request
+ formData.Del(k)
+
+ for _, iv := range v {
+ formData.Add(k, iv)
+ }
+ }
+
+ r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode()))
+ r.Header.Set(hdrContentTypeKey, formContentType)
+ r.isFormData = true
+}
+
+func handleContentType(c *Client, r *Request) {
+ contentType := r.Header.Get(hdrContentTypeKey)
+ if IsStringEmpty(contentType) {
+ contentType = DetectContentType(r.Body)
+ r.Header.Set(hdrContentTypeKey, contentType)
+ }
+}
+
+func handleRequestBody(c *Client, r *Request) (err error) {
+ var bodyBytes []byte
+ contentType := r.Header.Get(hdrContentTypeKey)
+ kind := kindOf(r.Body)
+ r.bodyBuf = nil
+
+ if reader, ok := r.Body.(io.Reader); ok {
+ if c.setContentLength || r.setContentLength { // keep backward compatibility
+ r.bodyBuf = acquireBuffer()
+ _, err = r.bodyBuf.ReadFrom(reader)
+ r.Body = nil
+ } else {
+ // Otherwise buffer less processing for `io.Reader`, sounds good.
+ return
+ }
+ } else if b, ok := r.Body.([]byte); ok {
+ bodyBytes = b
+ } else if s, ok := r.Body.(string); ok {
+ bodyBytes = []byte(s)
+ } else if IsJSONType(contentType) &&
+ (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
+ r.bodyBuf, err = jsonMarshal(c, r, r.Body)
+ if err != nil {
+ return
+ }
+ } else if IsXMLType(contentType) && (kind == reflect.Struct) {
+ bodyBytes, err = c.XMLMarshal(r.Body)
+ if err != nil {
+ return
+ }
+ }
+
+ if bodyBytes == nil && r.bodyBuf == nil {
+ err = errors.New("unsupported 'Body' type/value")
+ }
+
+ // if any errors during body bytes handling, return it
+ if err != nil {
+ return
+ }
+
+ // []byte into Buffer
+ if bodyBytes != nil && r.bodyBuf == nil {
+ r.bodyBuf = acquireBuffer()
+ _, _ = r.bodyBuf.Write(bodyBytes)
+ }
+
+ return
+}
+
+func saveResponseIntoFile(c *Client, res *Response) error {
+ if res.Request.isSaveResponse {
+ file := ""
+
+ if len(c.outputDirectory) > 0 && !filepath.IsAbs(res.Request.outputFile) {
+ file += c.outputDirectory + string(filepath.Separator)
+ }
+
+ file = filepath.Clean(file + res.Request.outputFile)
+ if err := createDirectory(filepath.Dir(file)); err != nil {
+ return err
+ }
+
+ outFile, err := os.Create(file)
+ if err != nil {
+ return err
+ }
+ defer closeq(outFile)
+
+ // io.Copy reads maximum 32kb size, it is perfect for large file download too
+ defer closeq(res.RawResponse.Body)
+
+ written, err := io.Copy(outFile, res.RawResponse.Body)
+ if err != nil {
+ return err
+ }
+
+ res.size = written
+ }
+
+ return nil
+}
+
+func getBodyCopy(r *Request) (*bytes.Buffer, error) {
+ // If r.bodyBuf present, return the copy
+ if r.bodyBuf != nil {
+ return bytes.NewBuffer(r.bodyBuf.Bytes()), nil
+ }
+
+ // Maybe body is `io.Reader`.
+ // Note: Resty user have to watchout for large body size of `io.Reader`
+ if r.RawRequest.Body != nil {
+ b, err := ioutil.ReadAll(r.RawRequest.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ // Restore the Body
+ closeq(r.RawRequest.Body)
+ r.RawRequest.Body = ioutil.NopCloser(bytes.NewBuffer(b))
+
+ // Return the Body bytes
+ return bytes.NewBuffer(b), nil
+ }
+ return nil, nil
+}
diff --git a/vendor/github.com/go-resty/resty/v2/redirect.go b/vendor/github.com/go-resty/resty/v2/redirect.go
new file mode 100644
index 000000000..7d7e43bc1
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/redirect.go
@@ -0,0 +1,101 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+)
+
+type (
+ // RedirectPolicy to regulate the redirects in the resty client.
+ // Objects implementing the RedirectPolicy interface can be registered as
+ //
+ // Apply function should return nil to continue the redirect jounery, otherwise
+ // return error to stop the redirect.
+ RedirectPolicy interface {
+ Apply(req *http.Request, via []*http.Request) error
+ }
+
+ // The RedirectPolicyFunc type is an adapter to allow the use of ordinary functions as RedirectPolicy.
+ // If f is a function with the appropriate signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls f.
+ RedirectPolicyFunc func(*http.Request, []*http.Request) error
+)
+
+// Apply calls f(req, via).
+func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error {
+ return f(req, via)
+}
+
+// NoRedirectPolicy is used to disable redirects in the HTTP client
+// resty.SetRedirectPolicy(NoRedirectPolicy())
+func NoRedirectPolicy() RedirectPolicy {
+ return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
+ return errors.New("auto redirect is disabled")
+ })
+}
+
+// FlexibleRedirectPolicy is convenient method to create No of redirect policy for HTTP client.
+// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
+func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy {
+ return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
+ if len(via) >= noOfRedirect {
+ return fmt.Errorf("stopped after %d redirects", noOfRedirect)
+ }
+ checkHostAndAddHeaders(req, via[0])
+ return nil
+ })
+}
+
+// DomainCheckRedirectPolicy is convenient method to define domain name redirect rule in resty client.
+// Redirect is allowed for only mentioned host in the policy.
+// resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
+func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy {
+ hosts := make(map[string]bool)
+ for _, h := range hostnames {
+ hosts[strings.ToLower(h)] = true
+ }
+
+ fn := RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
+ if ok := hosts[getHostname(req.URL.Host)]; !ok {
+ return errors.New("redirect is not allowed as per DomainCheckRedirectPolicy")
+ }
+
+ return nil
+ })
+
+ return fn
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Package Unexported methods
+//_______________________________________________________________________
+
+func getHostname(host string) (hostname string) {
+ if strings.Index(host, ":") > 0 {
+ host, _, _ = net.SplitHostPort(host)
+ }
+ hostname = strings.ToLower(host)
+ return
+}
+
+// By default Golang will not redirect request headers
+// after go throughing various discussion comments from thread
+// https://github.com/golang/go/issues/4800
+// Resty will add all the headers during a redirect for the same host
+func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) {
+ curHostname := getHostname(cur.URL.Host)
+ preHostname := getHostname(pre.URL.Host)
+ if strings.EqualFold(curHostname, preHostname) {
+ for key, val := range pre.Header {
+ cur.Header[key] = val
+ }
+ } else { // only library User-Agent header is added
+ cur.Header.Set(hdrUserAgentKey, hdrUserAgentValue)
+ }
+}
diff --git a/vendor/github.com/go-resty/resty/v2/request.go b/vendor/github.com/go-resty/resty/v2/request.go
new file mode 100644
index 000000000..672df88c3
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/request.go
@@ -0,0 +1,896 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+)
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Request struct and methods
+//_______________________________________________________________________
+
+// Request struct is used to compose and fire individual request from
+// resty client. Request provides an options to override client level
+// settings and also an options for the request composition.
+type Request struct {
+ URL string
+ Method string
+ Token string
+ AuthScheme string
+ QueryParam url.Values
+ FormData url.Values
+ PathParams map[string]string
+ Header http.Header
+ Time time.Time
+ Body interface{}
+ Result interface{}
+ Error interface{}
+ RawRequest *http.Request
+ SRV *SRVRecord
+ UserInfo *User
+ Cookies []*http.Cookie
+
+ // Attempt is to represent the request attempt made during a Resty
+ // request execution flow, including retry count.
+ //
+ // Since v2.4.0
+ Attempt int
+
+ isMultiPart bool
+ isFormData bool
+ setContentLength bool
+ isSaveResponse bool
+ notParseResponse bool
+ jsonEscapeHTML bool
+ trace bool
+ outputFile string
+ fallbackContentType string
+ forceContentType string
+ ctx context.Context
+ values map[string]interface{}
+ client *Client
+ bodyBuf *bytes.Buffer
+ clientTrace *clientTrace
+ multipartFiles []*File
+ multipartFields []*MultipartField
+ retryConditions []RetryConditionFunc
+}
+
+// Context method returns the Context if its already set in request
+// otherwise it creates new one using `context.Background()`.
+func (r *Request) Context() context.Context {
+ if r.ctx == nil {
+ return context.Background()
+ }
+ return r.ctx
+}
+
+// SetContext method sets the context.Context for current Request. It allows
+// to interrupt the request execution if ctx.Done() channel is closed.
+// See https://blog.golang.org/context article and the "context" package
+// documentation.
+func (r *Request) SetContext(ctx context.Context) *Request {
+ r.ctx = ctx
+ return r
+}
+
+// SetHeader method is to set a single header field and its value in the current request.
+//
+// For Example: To set `Content-Type` and `Accept` as `application/json`.
+// client.R().
+// SetHeader("Content-Type", "application/json").
+// SetHeader("Accept", "application/json")
+//
+// Also you can override header value, which was set at client instance level.
+func (r *Request) SetHeader(header, value string) *Request {
+ r.Header.Set(header, value)
+ return r
+}
+
+// SetHeaders method sets multiple headers field and its values at one go in the current request.
+//
+// For Example: To set `Content-Type` and `Accept` as `application/json`
+//
+// client.R().
+// SetHeaders(map[string]string{
+// "Content-Type": "application/json",
+// "Accept": "application/json",
+// })
+// Also you can override header value, which was set at client instance level.
+func (r *Request) SetHeaders(headers map[string]string) *Request {
+ for h, v := range headers {
+ r.SetHeader(h, v)
+ }
+ return r
+}
+
+// SetHeaderMultiValues sets multiple headers fields and its values is list of strings at one go in the current request.
+//
+// For Example: To set `Accept` as `text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8`
+//
+// client.R().
+// SetHeaderMultiValues(map[string][]string{
+// "Accept": []string{"text/html", "application/xhtml+xml", "application/xml;q=0.9", "image/webp", "*/*;q=0.8"},
+// })
+// Also you can override header value, which was set at client instance level.
+func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request {
+ for key, values := range headers {
+ r.SetHeader(key, strings.Join(values, ", "))
+ }
+ return r
+}
+
+// SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request.
+//
+// For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
+// client.R().
+// SetHeaderVerbatim("all_lowercase", "available").
+// SetHeaderVerbatim("UPPERCASE", "available")
+//
+// Also you can override header value, which was set at client instance level.
+//
+// Since v2.6.0
+func (r *Request) SetHeaderVerbatim(header, value string) *Request {
+ r.Header[header] = []string{value}
+ return r
+}
+
+// SetQueryParam method sets single parameter and its value in the current request.
+// It will be formed as query string for the request.
+//
+// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
+// client.R().
+// SetQueryParam("search", "kitchen papers").
+// SetQueryParam("size", "large")
+// Also you can override query params value, which was set at client instance level.
+func (r *Request) SetQueryParam(param, value string) *Request {
+ r.QueryParam.Set(param, value)
+ return r
+}
+
+// SetQueryParams method sets multiple parameters and its values at one go in the current request.
+// It will be formed as query string for the request.
+//
+// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
+// client.R().
+// SetQueryParams(map[string]string{
+// "search": "kitchen papers",
+// "size": "large",
+// })
+// Also you can override query params value, which was set at client instance level.
+func (r *Request) SetQueryParams(params map[string]string) *Request {
+ for p, v := range params {
+ r.SetQueryParam(p, v)
+ }
+ return r
+}
+
+// SetQueryParamsFromValues method appends multiple parameters with multi-value
+// (`url.Values`) at one go in the current request. It will be formed as
+// query string for the request.
+//
+// For Example: `status=pending&status=approved&status=open` in the URL after `?` mark.
+// client.R().
+// SetQueryParamsFromValues(url.Values{
+// "status": []string{"pending", "approved", "open"},
+// })
+// Also you can override query params value, which was set at client instance level.
+func (r *Request) SetQueryParamsFromValues(params url.Values) *Request {
+ for p, v := range params {
+ for _, pv := range v {
+ r.QueryParam.Add(p, pv)
+ }
+ }
+ return r
+}
+
+// SetQueryString method provides ability to use string as an input to set URL query string for the request.
+//
+// Using String as an input
+// client.R().
+// SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
+func (r *Request) SetQueryString(query string) *Request {
+ params, err := url.ParseQuery(strings.TrimSpace(query))
+ if err == nil {
+ for p, v := range params {
+ for _, pv := range v {
+ r.QueryParam.Add(p, pv)
+ }
+ }
+ } else {
+ r.client.log.Errorf("%v", err)
+ }
+ return r
+}
+
+// SetFormData method sets Form parameters and their values in the current request.
+// It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as
+// `application/x-www-form-urlencoded`.
+// client.R().
+// SetFormData(map[string]string{
+// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
+// "user_id": "3455454545",
+// })
+// Also you can override form data value, which was set at client instance level.
+func (r *Request) SetFormData(data map[string]string) *Request {
+ for k, v := range data {
+ r.FormData.Set(k, v)
+ }
+ return r
+}
+
+// SetFormDataFromValues method appends multiple form parameters with multi-value
+// (`url.Values`) at one go in the current request.
+// client.R().
+// SetFormDataFromValues(url.Values{
+// "search_criteria": []string{"book", "glass", "pencil"},
+// })
+// Also you can override form data value, which was set at client instance level.
+func (r *Request) SetFormDataFromValues(data url.Values) *Request {
+ for k, v := range data {
+ for _, kv := range v {
+ r.FormData.Add(k, kv)
+ }
+ }
+ return r
+}
+
+// SetBody method sets the request body for the request. It supports various realtime needs as easy.
+// We can say its quite handy or powerful. Supported request body data types is `string`,
+// `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer.
+// Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`.
+//
+// Note: `io.Reader` is processed as bufferless mode while sending request.
+//
+// For Example: Struct as a body input, based on content type, it will be marshalled.
+// client.R().
+// SetBody(User{
+// Username: "jeeva@myjeeva.com",
+// Password: "welcome2resty",
+// })
+//
+// Map as a body input, based on content type, it will be marshalled.
+// client.R().
+// SetBody(map[string]interface{}{
+// "username": "jeeva@myjeeva.com",
+// "password": "welcome2resty",
+// "address": &Address{
+// Address1: "1111 This is my street",
+// Address2: "Apt 201",
+// City: "My City",
+// State: "My State",
+// ZipCode: 00000,
+// },
+// })
+//
+// String as a body input. Suitable for any need as a string input.
+// client.R().
+// SetBody(`{
+// "username": "jeeva@getrightcare.com",
+// "password": "admin"
+// }`)
+//
+// []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc.
+// client.R().
+// SetBody([]byte("This is my raw request, sent as-is"))
+func (r *Request) SetBody(body interface{}) *Request {
+ r.Body = body
+ return r
+}
+
+// SetResult method is to register the response `Result` object for automatic unmarshalling for the request,
+// if response status code is between 200 and 299 and content type either JSON or XML.
+//
+// Note: Result object can be pointer or non-pointer.
+// client.R().SetResult(&AuthToken{})
+// // OR
+// client.R().SetResult(AuthToken{})
+//
+// Accessing a result value from response instance.
+// response.Result().(*AuthToken)
+func (r *Request) SetResult(res interface{}) *Request {
+ r.Result = getPointer(res)
+ return r
+}
+
+// SetError method is to register the request `Error` object for automatic unmarshalling for the request,
+// if response status code is greater than 399 and content type either JSON or XML.
+//
+// Note: Error object can be pointer or non-pointer.
+// client.R().SetError(&AuthError{})
+// // OR
+// client.R().SetError(AuthError{})
+//
+// Accessing a error value from response instance.
+// response.Error().(*AuthError)
+func (r *Request) SetError(err interface{}) *Request {
+ r.Error = getPointer(err)
+ return r
+}
+
+// SetFile method is to set single file field name and its path for multipart upload.
+// client.R().
+// SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf")
+func (r *Request) SetFile(param, filePath string) *Request {
+ r.isMultiPart = true
+ r.FormData.Set("@"+param, filePath)
+ return r
+}
+
+// SetFiles method is to set multiple file field name and its path for multipart upload.
+// client.R().
+// SetFiles(map[string]string{
+// "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf",
+// "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf",
+// "my_file3": "/Users/jeeva/Water Bill - Sep.pdf",
+// })
+func (r *Request) SetFiles(files map[string]string) *Request {
+ r.isMultiPart = true
+ for f, fp := range files {
+ r.FormData.Set("@"+f, fp)
+ }
+ return r
+}
+
+// SetFileReader method is to set single file using io.Reader for multipart upload.
+// client.R().
+// SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)).
+// SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes))
+func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request {
+ r.isMultiPart = true
+ r.multipartFiles = append(r.multipartFiles, &File{
+ Name: fileName,
+ ParamName: param,
+ Reader: reader,
+ })
+ return r
+}
+
+// SetMultipartFormData method allows simple form data to be attached to the request as `multipart:form-data`
+func (r *Request) SetMultipartFormData(data map[string]string) *Request {
+ for k, v := range data {
+ r = r.SetMultipartField(k, "", "", strings.NewReader(v))
+ }
+
+ return r
+}
+
+// SetMultipartField method is to set custom data using io.Reader for multipart upload.
+func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request {
+ r.isMultiPart = true
+ r.multipartFields = append(r.multipartFields, &MultipartField{
+ Param: param,
+ FileName: fileName,
+ ContentType: contentType,
+ Reader: reader,
+ })
+ return r
+}
+
+// SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload.
+//
+// For Example:
+// client.R().SetMultipartFields(
+// &resty.MultipartField{
+// Param: "uploadManifest1",
+// FileName: "upload-file-1.json",
+// ContentType: "application/json",
+// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`),
+// },
+// &resty.MultipartField{
+// Param: "uploadManifest2",
+// FileName: "upload-file-2.json",
+// ContentType: "application/json",
+// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`),
+// })
+//
+// If you have slice already, then simply call-
+// client.R().SetMultipartFields(fields...)
+func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request {
+ r.isMultiPart = true
+ r.multipartFields = append(r.multipartFields, fields...)
+ return r
+}
+
+// SetContentLength method sets the HTTP header `Content-Length` value for current request.
+// By default Resty won't set `Content-Length`. Also you have an option to enable for every
+// request.
+//
+// See `Client.SetContentLength`
+// client.R().SetContentLength(true)
+func (r *Request) SetContentLength(l bool) *Request {
+ r.setContentLength = l
+ return r
+}
+
+// SetBasicAuth method sets the basic authentication header in the current HTTP request.
+//
+// For Example:
+// Authorization: Basic
+//
+// To set the header for username "go-resty" and password "welcome"
+// client.R().SetBasicAuth("go-resty", "welcome")
+//
+// This method overrides the credentials set by method `Client.SetBasicAuth`.
+func (r *Request) SetBasicAuth(username, password string) *Request {
+ r.UserInfo = &User{Username: username, Password: password}
+ return r
+}
+
+// SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example:
+// Authorization: Bearer
+//
+// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
+//
+// client.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
+//
+// This method overrides the Auth token set by method `Client.SetAuthToken`.
+func (r *Request) SetAuthToken(token string) *Request {
+ r.Token = token
+ return r
+}
+
+// SetAuthScheme method sets the auth token scheme type in the HTTP request. For Example:
+// Authorization:
+//
+// For Example: To set the scheme to use OAuth
+//
+// client.R().SetAuthScheme("OAuth")
+//
+// This auth header scheme gets added to all the request rasied from this client instance.
+// Also it can be overridden or set one at the request level is supported.
+//
+// Information about Auth schemes can be found in RFC7235 which is linked to below along with the page containing
+// the currently defined official authentication schemes:
+// https://tools.ietf.org/html/rfc7235
+// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
+//
+// This method overrides the Authorization scheme set by method `Client.SetAuthScheme`.
+func (r *Request) SetAuthScheme(scheme string) *Request {
+ r.AuthScheme = scheme
+ return r
+}
+
+// SetOutput method sets the output file for current HTTP request. Current HTTP response will be
+// saved into given file. It is similar to `curl -o` flag. Absolute path or relative path can be used.
+// If is it relative path then output file goes under the output directory, as mentioned
+// in the `Client.SetOutputDirectory`.
+// client.R().
+// SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip").
+// Get("http://bit.ly/1LouEKr")
+//
+// Note: In this scenario `Response.Body` might be nil.
+func (r *Request) SetOutput(file string) *Request {
+ r.outputFile = file
+ r.isSaveResponse = true
+ return r
+}
+
+// SetSRV method sets the details to query the service SRV record and execute the
+// request.
+// client.R().
+// SetSRV(SRVRecord{"web", "testservice.com"}).
+// Get("/get")
+func (r *Request) SetSRV(srv *SRVRecord) *Request {
+ r.SRV = srv
+ return r
+}
+
+// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically.
+// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body,
+// otherwise you might get into connection leaks, no connection reuse.
+//
+// Note: Response middlewares are not applicable, if you use this option. Basically you have
+// taken over the control of response parsing from `Resty`.
+func (r *Request) SetDoNotParseResponse(parse bool) *Request {
+ r.notParseResponse = parse
+ return r
+}
+
+// SetPathParam method sets single URL path key-value pair in the
+// Resty current request instance.
+// client.R().SetPathParam("userId", "sample@sample.com")
+//
+// Result:
+// URL - /v1/users/{userId}/details
+// Composed URL - /v1/users/sample@sample.com/details
+// It replaces the value of the key while composing the request URL. Also you can
+// override Path Params value, which was set at client instance level.
+func (r *Request) SetPathParam(param, value string) *Request {
+ r.PathParams[param] = value
+ return r
+}
+
+// SetPathParams method sets multiple URL path key-value pairs at one go in the
+// Resty current request instance.
+// client.R().SetPathParams(map[string]string{
+// "userId": "sample@sample.com",
+// "subAccountId": "100002",
+// })
+//
+// Result:
+// URL - /v1/users/{userId}/{subAccountId}/details
+// Composed URL - /v1/users/sample@sample.com/100002/details
+// It replaces the value of the key while composing request URL. Also you can
+// override Path Params value, which was set at client instance level.
+func (r *Request) SetPathParams(params map[string]string) *Request {
+ for p, v := range params {
+ r.SetPathParam(p, v)
+ }
+ return r
+}
+
+// ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling
+// when `Content-Type` response header is unavailable.
+func (r *Request) ExpectContentType(contentType string) *Request {
+ r.fallbackContentType = contentType
+ return r
+}
+
+// ForceContentType method provides a strong sense of response `Content-Type` for automatic unmarshalling.
+// Resty gives this a higher priority than the `Content-Type` response header. This means that if both
+// `Request.ForceContentType` is set and the response `Content-Type` is available, `ForceContentType` will win.
+func (r *Request) ForceContentType(contentType string) *Request {
+ r.forceContentType = contentType
+ return r
+}
+
+// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal.
+//
+// Note: This option only applicable to standard JSON Marshaller.
+func (r *Request) SetJSONEscapeHTML(b bool) *Request {
+ r.jsonEscapeHTML = b
+ return r
+}
+
+// SetCookie method appends a single cookie in the current request instance.
+// client.R().SetCookie(&http.Cookie{
+// Name:"go-resty",
+// Value:"This is cookie value",
+// })
+//
+// Note: Method appends the Cookie value into existing Cookie if already existing.
+//
+// Since v2.1.0
+func (r *Request) SetCookie(hc *http.Cookie) *Request {
+ r.Cookies = append(r.Cookies, hc)
+ return r
+}
+
+// SetCookies method sets an array of cookies in the current request instance.
+// cookies := []*http.Cookie{
+// &http.Cookie{
+// Name:"go-resty-1",
+// Value:"This is cookie 1 value",
+// },
+// &http.Cookie{
+// Name:"go-resty-2",
+// Value:"This is cookie 2 value",
+// },
+// }
+//
+// // Setting a cookies into resty's current request
+// client.R().SetCookies(cookies)
+//
+// Note: Method appends the Cookie value into existing Cookie if already existing.
+//
+// Since v2.1.0
+func (r *Request) SetCookies(rs []*http.Cookie) *Request {
+ r.Cookies = append(r.Cookies, rs...)
+ return r
+}
+
+// AddRetryCondition method adds a retry condition function to the request's
+// array of functions that are checked to determine if the request is retried.
+// The request will retry if any of the functions return true and error is nil.
+//
+// Note: These retry conditions are checked before all retry conditions of the client.
+//
+// Since v2.7.0
+func (r *Request) AddRetryCondition(condition RetryConditionFunc) *Request {
+ r.retryConditions = append(r.retryConditions, condition)
+ return r
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// HTTP request tracing
+//_______________________________________________________________________
+
+// EnableTrace method enables trace for the current request
+// using `httptrace.ClientTrace` and provides insights.
+//
+// client := resty.New()
+//
+// resp, err := client.R().EnableTrace().Get("https://httpbin.org/get")
+// fmt.Println("Error:", err)
+// fmt.Println("Trace Info:", resp.Request.TraceInfo())
+//
+// See `Client.EnableTrace` available too to get trace info for all requests.
+//
+// Since v2.0.0
+func (r *Request) EnableTrace() *Request {
+ r.trace = true
+ return r
+}
+
+// TraceInfo method returns the trace info for the request.
+// If either the Client or Request EnableTrace function has not been called
+// prior to the request being made, an empty TraceInfo object will be returned.
+//
+// Since v2.0.0
+func (r *Request) TraceInfo() TraceInfo {
+ ct := r.clientTrace
+
+ if ct == nil {
+ return TraceInfo{}
+ }
+
+ ti := TraceInfo{
+ DNSLookup: ct.dnsDone.Sub(ct.dnsStart),
+ TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart),
+ ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn),
+ IsConnReused: ct.gotConnInfo.Reused,
+ IsConnWasIdle: ct.gotConnInfo.WasIdle,
+ ConnIdleTime: ct.gotConnInfo.IdleTime,
+ RequestAttempt: r.Attempt,
+ }
+
+ // Calculate the total time accordingly,
+ // when connection is reused
+ if ct.gotConnInfo.Reused {
+ ti.TotalTime = ct.endTime.Sub(ct.getConn)
+ } else {
+ ti.TotalTime = ct.endTime.Sub(ct.dnsStart)
+ }
+
+ // Only calculate on successful connections
+ if !ct.connectDone.IsZero() {
+ ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)
+ }
+
+ // Only calculate on successful connections
+ if !ct.gotConn.IsZero() {
+ ti.ConnTime = ct.gotConn.Sub(ct.getConn)
+ }
+
+ // Only calculate on successful connections
+ if !ct.gotFirstResponseByte.IsZero() {
+ ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)
+ }
+
+ // Capture remote address info when connection is non-nil
+ if ct.gotConnInfo.Conn != nil {
+ ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
+ }
+
+ return ti
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// HTTP verb method starts here
+//_______________________________________________________________________
+
+// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231.
+func (r *Request) Get(url string) (*Response, error) {
+ return r.Execute(MethodGet, url)
+}
+
+// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231.
+func (r *Request) Head(url string) (*Response, error) {
+ return r.Execute(MethodHead, url)
+}
+
+// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231.
+func (r *Request) Post(url string) (*Response, error) {
+ return r.Execute(MethodPost, url)
+}
+
+// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231.
+func (r *Request) Put(url string) (*Response, error) {
+ return r.Execute(MethodPut, url)
+}
+
+// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231.
+func (r *Request) Delete(url string) (*Response, error) {
+ return r.Execute(MethodDelete, url)
+}
+
+// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231.
+func (r *Request) Options(url string) (*Response, error) {
+ return r.Execute(MethodOptions, url)
+}
+
+// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789.
+func (r *Request) Patch(url string) (*Response, error) {
+ return r.Execute(MethodPatch, url)
+}
+
+// Send method performs the HTTP request using the method and URL already defined
+// for current `Request`.
+// req := client.R()
+// req.Method = resty.GET
+// req.URL = "http://httpbin.org/get"
+// resp, err := client.R().Send()
+func (r *Request) Send() (*Response, error) {
+ return r.Execute(r.Method, r.URL)
+}
+
+// Execute method performs the HTTP request with given HTTP method and URL
+// for current `Request`.
+// resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get")
+func (r *Request) Execute(method, url string) (*Response, error) {
+ var addrs []*net.SRV
+ var resp *Response
+ var err error
+
+ if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) {
+ // No OnError hook here since this is a request validation error
+ return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method)
+ }
+
+ if r.SRV != nil {
+ _, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain)
+ if err != nil {
+ r.client.onErrorHooks(r, nil, err)
+ return nil, err
+ }
+ }
+
+ r.Method = method
+ r.URL = r.selectAddr(addrs, url, 0)
+
+ if r.client.RetryCount == 0 {
+ r.Attempt = 1
+ resp, err = r.client.execute(r)
+ r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
+ return resp, unwrapNoRetryErr(err)
+ }
+
+ err = Backoff(
+ func() (*Response, error) {
+ r.Attempt++
+
+ r.URL = r.selectAddr(addrs, url, r.Attempt)
+
+ resp, err = r.client.execute(r)
+ if err != nil {
+ r.client.log.Errorf("%v, Attempt %v", err, r.Attempt)
+ }
+
+ return resp, err
+ },
+ Retries(r.client.RetryCount),
+ WaitTime(r.client.RetryWaitTime),
+ MaxWaitTime(r.client.RetryMaxWaitTime),
+ RetryConditions(append(r.retryConditions, r.client.RetryConditions...)),
+ RetryHooks(r.client.RetryHooks),
+ )
+
+ r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
+
+ return resp, unwrapNoRetryErr(err)
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// SRVRecord struct
+//_______________________________________________________________________
+
+// SRVRecord struct holds the data to query the SRV record for the
+// following service.
+type SRVRecord struct {
+ Service string
+ Domain string
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Request Unexported methods
+//_______________________________________________________________________
+
+func (r *Request) fmtBodyString(sl int64) (body string) {
+ body = "***** NO CONTENT *****"
+ if !isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) {
+ return
+ }
+
+ if _, ok := r.Body.(io.Reader); ok {
+ body = "***** BODY IS io.Reader *****"
+ return
+ }
+
+ // multipart or form-data
+ if r.isMultiPart || r.isFormData {
+ bodySize := int64(r.bodyBuf.Len())
+ if bodySize > sl {
+ body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize)
+ return
+ }
+ body = r.bodyBuf.String()
+ return
+ }
+
+ // request body data
+ if r.Body == nil {
+ return
+ }
+ var prtBodyBytes []byte
+ var err error
+
+ contentType := r.Header.Get(hdrContentTypeKey)
+ kind := kindOf(r.Body)
+ if canJSONMarshal(contentType, kind) {
+ prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ")
+ } else if IsXMLType(contentType) && (kind == reflect.Struct) {
+ prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ")
+ } else if b, ok := r.Body.(string); ok {
+ if IsJSONType(contentType) {
+ bodyBytes := []byte(b)
+ out := acquireBuffer()
+ defer releaseBuffer(out)
+ if err = json.Indent(out, bodyBytes, "", " "); err == nil {
+ prtBodyBytes = out.Bytes()
+ }
+ } else {
+ body = b
+ }
+ } else if b, ok := r.Body.([]byte); ok {
+ body = fmt.Sprintf("***** BODY IS byte(s) (size - %d) *****", len(b))
+ return
+ }
+
+ if prtBodyBytes != nil && err == nil {
+ body = string(prtBodyBytes)
+ }
+
+ if len(body) > 0 {
+ bodySize := int64(len([]byte(body)))
+ if bodySize > sl {
+ body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize)
+ }
+ }
+
+ return
+}
+
+func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string {
+ if addrs == nil {
+ return path
+ }
+
+ idx := attempt % len(addrs)
+ domain := strings.TrimRight(addrs[idx].Target, ".")
+ path = strings.TrimLeft(path, "/")
+
+ return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path)
+}
+
+func (r *Request) initValuesMap() {
+ if r.values == nil {
+ r.values = make(map[string]interface{})
+ }
+}
+
+var noescapeJSONMarshal = func(v interface{}) (*bytes.Buffer, error) {
+ buf := acquireBuffer()
+ encoder := json.NewEncoder(buf)
+ encoder.SetEscapeHTML(false)
+ if err := encoder.Encode(v); err != nil {
+ releaseBuffer(buf)
+ return nil, err
+ }
+
+ return buf, nil
+}
diff --git a/vendor/github.com/go-resty/resty/v2/response.go b/vendor/github.com/go-resty/resty/v2/response.go
new file mode 100644
index 000000000..8ae0e10ba
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/response.go
@@ -0,0 +1,175 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+)
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Response struct and methods
+//_______________________________________________________________________
+
+// Response struct holds response values of executed request.
+type Response struct {
+ Request *Request
+ RawResponse *http.Response
+
+ body []byte
+ size int64
+ receivedAt time.Time
+}
+
+// Body method returns HTTP response as []byte array for the executed request.
+//
+// Note: `Response.Body` might be nil, if `Request.SetOutput` is used.
+func (r *Response) Body() []byte {
+ if r.RawResponse == nil {
+ return []byte{}
+ }
+ return r.body
+}
+
+// Status method returns the HTTP status string for the executed request.
+// Example: 200 OK
+func (r *Response) Status() string {
+ if r.RawResponse == nil {
+ return ""
+ }
+ return r.RawResponse.Status
+}
+
+// StatusCode method returns the HTTP status code for the executed request.
+// Example: 200
+func (r *Response) StatusCode() int {
+ if r.RawResponse == nil {
+ return 0
+ }
+ return r.RawResponse.StatusCode
+}
+
+// Proto method returns the HTTP response protocol used for the request.
+func (r *Response) Proto() string {
+ if r.RawResponse == nil {
+ return ""
+ }
+ return r.RawResponse.Proto
+}
+
+// Result method returns the response value as an object if it has one
+func (r *Response) Result() interface{} {
+ return r.Request.Result
+}
+
+// Error method returns the error object if it has one
+func (r *Response) Error() interface{} {
+ return r.Request.Error
+}
+
+// Header method returns the response headers
+func (r *Response) Header() http.Header {
+ if r.RawResponse == nil {
+ return http.Header{}
+ }
+ return r.RawResponse.Header
+}
+
+// Cookies method to access all the response cookies
+func (r *Response) Cookies() []*http.Cookie {
+ if r.RawResponse == nil {
+ return make([]*http.Cookie, 0)
+ }
+ return r.RawResponse.Cookies()
+}
+
+// String method returns the body of the server response as String.
+func (r *Response) String() string {
+ if r.body == nil {
+ return ""
+ }
+ return strings.TrimSpace(string(r.body))
+}
+
+// Time method returns the time of HTTP response time that from request we sent and received a request.
+//
+// See `Response.ReceivedAt` to know when client received response and see `Response.Request.Time` to know
+// when client sent a request.
+func (r *Response) Time() time.Duration {
+ if r.Request.clientTrace != nil {
+ return r.Request.TraceInfo().TotalTime
+ }
+ return r.receivedAt.Sub(r.Request.Time)
+}
+
+// ReceivedAt method returns when response got received from server for the request.
+func (r *Response) ReceivedAt() time.Time {
+ return r.receivedAt
+}
+
+// Size method returns the HTTP response size in bytes. Ya, you can relay on HTTP `Content-Length` header,
+// however it won't be good for chucked transfer/compressed response. Since Resty calculates response size
+// at the client end. You will get actual size of the http response.
+func (r *Response) Size() int64 {
+ return r.size
+}
+
+// RawBody method exposes the HTTP raw response body. Use this method in-conjunction with `SetDoNotParseResponse`
+// option otherwise you get an error as `read err: http: read on closed response body`.
+//
+// Do not forget to close the body, otherwise you might get into connection leaks, no connection reuse.
+// Basically you have taken over the control of response parsing from `Resty`.
+func (r *Response) RawBody() io.ReadCloser {
+ if r.RawResponse == nil {
+ return nil
+ }
+ return r.RawResponse.Body
+}
+
+// IsSuccess method returns true if HTTP status `code >= 200 and <= 299` otherwise false.
+func (r *Response) IsSuccess() bool {
+ return r.StatusCode() > 199 && r.StatusCode() < 300
+}
+
+// IsError method returns true if HTTP status `code >= 400` otherwise false.
+func (r *Response) IsError() bool {
+ return r.StatusCode() > 399
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Response Unexported methods
+//_______________________________________________________________________
+
+func (r *Response) setReceivedAt() {
+ r.receivedAt = time.Now()
+ if r.Request.clientTrace != nil {
+ r.Request.clientTrace.endTime = r.receivedAt
+ }
+}
+
+func (r *Response) fmtBodyString(sl int64) string {
+ if r.body != nil {
+ if int64(len(r.body)) > sl {
+ return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.body))
+ }
+ ct := r.Header().Get(hdrContentTypeKey)
+ if IsJSONType(ct) {
+ out := acquireBuffer()
+ defer releaseBuffer(out)
+ err := json.Indent(out, r.body, "", " ")
+ if err != nil {
+ return fmt.Sprintf("*** Error: Unable to format response body - \"%s\" ***\n\nLog Body as-is:\n%s", err, r.String())
+ }
+ return out.String()
+ }
+ return r.String()
+ }
+
+ return "***** NO CONTENT *****"
+}
diff --git a/vendor/github.com/go-resty/resty/v2/resty.go b/vendor/github.com/go-resty/resty/v2/resty.go
new file mode 100644
index 000000000..6f9c8b4cd
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/resty.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// Package resty provides Simple HTTP and REST client library for Go.
+package resty
+
+import (
+ "net"
+ "net/http"
+ "net/http/cookiejar"
+
+ "golang.org/x/net/publicsuffix"
+)
+
+// Version # of resty
+const Version = "2.7.0"
+
+// New method creates a new Resty client.
+func New() *Client {
+ cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
+ return createClient(&http.Client{
+ Jar: cookieJar,
+ })
+}
+
+// NewWithClient method creates a new Resty client with given `http.Client`.
+func NewWithClient(hc *http.Client) *Client {
+ return createClient(hc)
+}
+
+// NewWithLocalAddr method creates a new Resty client with given Local Address
+// to dial from.
+func NewWithLocalAddr(localAddr net.Addr) *Client {
+ cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
+ return createClient(&http.Client{
+ Jar: cookieJar,
+ Transport: createTransport(localAddr),
+ })
+}
diff --git a/vendor/github.com/go-resty/resty/v2/retry.go b/vendor/github.com/go-resty/resty/v2/retry.go
new file mode 100644
index 000000000..00b8514a5
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/retry.go
@@ -0,0 +1,221 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "context"
+ "math"
+ "math/rand"
+ "sync"
+ "time"
+)
+
+const (
+ defaultMaxRetries = 3
+ defaultWaitTime = time.Duration(100) * time.Millisecond
+ defaultMaxWaitTime = time.Duration(2000) * time.Millisecond
+)
+
+type (
+ // Option is to create convenient retry options like wait time, max retries, etc.
+ Option func(*Options)
+
+ // RetryConditionFunc type is for retry condition function
+ // input: non-nil Response OR request execution error
+ RetryConditionFunc func(*Response, error) bool
+
+ // OnRetryFunc is for side-effecting functions triggered on retry
+ OnRetryFunc func(*Response, error)
+
+ // RetryAfterFunc returns time to wait before retry
+ // For example, it can parse HTTP Retry-After header
+ // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ // Non-nil error is returned if it is found that request is not retryable
+ // (0, nil) is a special result means 'use default algorithm'
+ RetryAfterFunc func(*Client, *Response) (time.Duration, error)
+
+ // Options struct is used to hold retry settings.
+ Options struct {
+ maxRetries int
+ waitTime time.Duration
+ maxWaitTime time.Duration
+ retryConditions []RetryConditionFunc
+ retryHooks []OnRetryFunc
+ }
+)
+
+// Retries sets the max number of retries
+func Retries(value int) Option {
+ return func(o *Options) {
+ o.maxRetries = value
+ }
+}
+
+// WaitTime sets the default wait time to sleep between requests
+func WaitTime(value time.Duration) Option {
+ return func(o *Options) {
+ o.waitTime = value
+ }
+}
+
+// MaxWaitTime sets the max wait time to sleep between requests
+func MaxWaitTime(value time.Duration) Option {
+ return func(o *Options) {
+ o.maxWaitTime = value
+ }
+}
+
+// RetryConditions sets the conditions that will be checked for retry.
+func RetryConditions(conditions []RetryConditionFunc) Option {
+ return func(o *Options) {
+ o.retryConditions = conditions
+ }
+}
+
+// RetryHooks sets the hooks that will be executed after each retry
+func RetryHooks(hooks []OnRetryFunc) Option {
+ return func(o *Options) {
+ o.retryHooks = hooks
+ }
+}
+
+// Backoff retries with increasing timeout duration up until X amount of retries
+// (Default is 3 attempts, Override with option Retries(n))
+func Backoff(operation func() (*Response, error), options ...Option) error {
+ // Defaults
+ opts := Options{
+ maxRetries: defaultMaxRetries,
+ waitTime: defaultWaitTime,
+ maxWaitTime: defaultMaxWaitTime,
+ retryConditions: []RetryConditionFunc{},
+ }
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ var (
+ resp *Response
+ err error
+ )
+
+ for attempt := 0; attempt <= opts.maxRetries; attempt++ {
+ resp, err = operation()
+ ctx := context.Background()
+ if resp != nil && resp.Request.ctx != nil {
+ ctx = resp.Request.ctx
+ }
+ if ctx.Err() != nil {
+ return err
+ }
+
+ err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback.
+ needsRetry := err != nil && err == err1 // retry on a few operation errors by default
+
+ for _, condition := range opts.retryConditions {
+ needsRetry = condition(resp, err1)
+ if needsRetry {
+ break
+ }
+ }
+
+ if !needsRetry {
+ return err
+ }
+
+ for _, hook := range opts.retryHooks {
+ hook(resp, err)
+ }
+
+ // Don't need to wait when no retries left.
+ // Still run retry hooks even on last retry to keep compatibility.
+ if attempt == opts.maxRetries {
+ return err
+ }
+
+ waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
+ if err2 != nil {
+ if err == nil {
+ err = err2
+ }
+ return err
+ }
+
+ select {
+ case <-time.After(waitTime):
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+
+ return err
+}
+
+func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) {
+ const maxInt = 1<<31 - 1 // max int for arch 386
+ if max < 0 {
+ max = maxInt
+ }
+ if resp == nil {
+ return jitterBackoff(min, max, attempt), nil
+ }
+
+ retryAfterFunc := resp.Request.client.RetryAfter
+
+ // Check for custom callback
+ if retryAfterFunc == nil {
+ return jitterBackoff(min, max, attempt), nil
+ }
+
+ result, err := retryAfterFunc(resp.Request.client, resp)
+ if err != nil {
+ return 0, err // i.e. 'API quota exceeded'
+ }
+ if result == 0 {
+ return jitterBackoff(min, max, attempt), nil
+ }
+ if result < 0 || max < result {
+ result = max
+ }
+ if result < min {
+ result = min
+ }
+ return result, nil
+}
+
+// Return capped exponential backoff with jitter
+// http://www.awsarchitectureblog.com/2015/03/backoff.html
+func jitterBackoff(min, max time.Duration, attempt int) time.Duration {
+ base := float64(min)
+ capLevel := float64(max)
+
+ temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
+ ri := time.Duration(temp / 2)
+ result := randDuration(ri)
+
+ if result < min {
+ result = min
+ }
+
+ return result
+}
+
+var rnd = newRnd()
+var rndMu sync.Mutex
+
+func randDuration(center time.Duration) time.Duration {
+ rndMu.Lock()
+ defer rndMu.Unlock()
+
+ var ri = int64(center)
+ var jitter = rnd.Int63n(ri)
+ return time.Duration(math.Abs(float64(ri + jitter)))
+}
+
+func newRnd() *rand.Rand {
+ var seed = time.Now().UnixNano()
+ var src = rand.NewSource(seed)
+ return rand.New(src)
+}
diff --git a/vendor/github.com/go-resty/resty/v2/trace.go b/vendor/github.com/go-resty/resty/v2/trace.go
new file mode 100644
index 000000000..23cf70335
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/trace.go
@@ -0,0 +1,130 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "context"
+ "crypto/tls"
+ "net"
+ "net/http/httptrace"
+ "time"
+)
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// TraceInfo struct
+//_______________________________________________________________________
+
+// TraceInfo struct is used provide request trace info such as DNS lookup
+// duration, Connection obtain duration, Server processing duration, etc.
+//
+// Since v2.0.0
+type TraceInfo struct {
+ // DNSLookup is a duration that transport took to perform
+ // DNS lookup.
+ DNSLookup time.Duration
+
+ // ConnTime is a duration that took to obtain a successful connection.
+ ConnTime time.Duration
+
+ // TCPConnTime is a duration that took to obtain the TCP connection.
+ TCPConnTime time.Duration
+
+ // TLSHandshake is a duration that TLS handshake took place.
+ TLSHandshake time.Duration
+
+ // ServerTime is a duration that server took to respond first byte.
+ ServerTime time.Duration
+
+ // ResponseTime is a duration since first response byte from server to
+ // request completion.
+ ResponseTime time.Duration
+
+ // TotalTime is a duration that total request took end-to-end.
+ TotalTime time.Duration
+
+ // IsConnReused is whether this connection has been previously
+ // used for another HTTP request.
+ IsConnReused bool
+
+ // IsConnWasIdle is whether this connection was obtained from an
+ // idle pool.
+ IsConnWasIdle bool
+
+ // ConnIdleTime is a duration how long the connection was previously
+ // idle, if IsConnWasIdle is true.
+ ConnIdleTime time.Duration
+
+ // RequestAttempt is to represent the request attempt made during a Resty
+ // request execution flow, including retry count.
+ RequestAttempt int
+
+ // RemoteAddr returns the remote network address.
+ RemoteAddr net.Addr
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// ClientTrace struct and its methods
+//_______________________________________________________________________
+
+// tracer struct maps the `httptrace.ClientTrace` hooks into Fields
+// with same naming for easy understanding. Plus additional insights
+// Request.
+type clientTrace struct {
+ getConn time.Time
+ dnsStart time.Time
+ dnsDone time.Time
+ connectDone time.Time
+ tlsHandshakeStart time.Time
+ tlsHandshakeDone time.Time
+ gotConn time.Time
+ gotFirstResponseByte time.Time
+ endTime time.Time
+ gotConnInfo httptrace.GotConnInfo
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Trace unexported methods
+//_______________________________________________________________________
+
+func (t *clientTrace) createContext(ctx context.Context) context.Context {
+ return httptrace.WithClientTrace(
+ ctx,
+ &httptrace.ClientTrace{
+ DNSStart: func(_ httptrace.DNSStartInfo) {
+ t.dnsStart = time.Now()
+ },
+ DNSDone: func(_ httptrace.DNSDoneInfo) {
+ t.dnsDone = time.Now()
+ },
+ ConnectStart: func(_, _ string) {
+ if t.dnsDone.IsZero() {
+ t.dnsDone = time.Now()
+ }
+ if t.dnsStart.IsZero() {
+ t.dnsStart = t.dnsDone
+ }
+ },
+ ConnectDone: func(net, addr string, err error) {
+ t.connectDone = time.Now()
+ },
+ GetConn: func(_ string) {
+ t.getConn = time.Now()
+ },
+ GotConn: func(ci httptrace.GotConnInfo) {
+ t.gotConn = time.Now()
+ t.gotConnInfo = ci
+ },
+ GotFirstResponseByte: func() {
+ t.gotFirstResponseByte = time.Now()
+ },
+ TLSHandshakeStart: func() {
+ t.tlsHandshakeStart = time.Now()
+ },
+ TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
+ t.tlsHandshakeDone = time.Now()
+ },
+ },
+ )
+}
diff --git a/vendor/github.com/go-resty/resty/v2/transport.go b/vendor/github.com/go-resty/resty/v2/transport.go
new file mode 100644
index 000000000..e15b48c55
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/transport.go
@@ -0,0 +1,35 @@
+// +build go1.13
+
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "net"
+ "net/http"
+ "runtime"
+ "time"
+)
+
+func createTransport(localAddr net.Addr) *http.Transport {
+ dialer := &net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ DualStack: true,
+ }
+ if localAddr != nil {
+ dialer.LocalAddr = localAddr
+ }
+ return &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: dialer.DialContext,
+ ForceAttemptHTTP2: true,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
+ }
+}
diff --git a/vendor/github.com/go-resty/resty/v2/transport112.go b/vendor/github.com/go-resty/resty/v2/transport112.go
new file mode 100644
index 000000000..fbbbc5911
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/transport112.go
@@ -0,0 +1,34 @@
+// +build !go1.13
+
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "net"
+ "net/http"
+ "runtime"
+ "time"
+)
+
+func createTransport(localAddr net.Addr) *http.Transport {
+ dialer := &net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ DualStack: true,
+ }
+ if localAddr != nil {
+ dialer.LocalAddr = localAddr
+ }
+ return &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: dialer.DialContext,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
+ }
+}
diff --git a/vendor/github.com/go-resty/resty/v2/util.go b/vendor/github.com/go-resty/resty/v2/util.go
new file mode 100644
index 000000000..1d563befd
--- /dev/null
+++ b/vendor/github.com/go-resty/resty/v2/util.go
@@ -0,0 +1,391 @@
+// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+)
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Logger interface
+//_______________________________________________________________________
+
+// Logger interface is to abstract the logging from Resty. Gives control to
+// the Resty users, choice of the logger.
+type Logger interface {
+ Errorf(format string, v ...interface{})
+ Warnf(format string, v ...interface{})
+ Debugf(format string, v ...interface{})
+}
+
+func createLogger() *logger {
+ l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
+ return l
+}
+
+var _ Logger = (*logger)(nil)
+
+type logger struct {
+ l *log.Logger
+}
+
+func (l *logger) Errorf(format string, v ...interface{}) {
+ l.output("ERROR RESTY "+format, v...)
+}
+
+func (l *logger) Warnf(format string, v ...interface{}) {
+ l.output("WARN RESTY "+format, v...)
+}
+
+func (l *logger) Debugf(format string, v ...interface{}) {
+ l.output("DEBUG RESTY "+format, v...)
+}
+
+func (l *logger) output(format string, v ...interface{}) {
+ if len(v) == 0 {
+ l.l.Print(format)
+ return
+ }
+ l.l.Printf(format, v...)
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Package Helper methods
+//_______________________________________________________________________
+
+// IsStringEmpty method tells whether given string is empty or not
+func IsStringEmpty(str string) bool {
+ return len(strings.TrimSpace(str)) == 0
+}
+
+// DetectContentType method is used to figure out `Request.Body` content type for request header
+func DetectContentType(body interface{}) string {
+ contentType := plainTextType
+ kind := kindOf(body)
+ switch kind {
+ case reflect.Struct, reflect.Map:
+ contentType = jsonContentType
+ case reflect.String:
+ contentType = plainTextType
+ default:
+ if b, ok := body.([]byte); ok {
+ contentType = http.DetectContentType(b)
+ } else if kind == reflect.Slice {
+ contentType = jsonContentType
+ }
+ }
+
+ return contentType
+}
+
+// IsJSONType method is to check JSON content type or not
+func IsJSONType(ct string) bool {
+ return jsonCheck.MatchString(ct)
+}
+
+// IsXMLType method is to check XML content type or not
+func IsXMLType(ct string) bool {
+ return xmlCheck.MatchString(ct)
+}
+
+// Unmarshalc content into object from JSON or XML
+func Unmarshalc(c *Client, ct string, b []byte, d interface{}) (err error) {
+ if IsJSONType(ct) {
+ err = c.JSONUnmarshal(b, d)
+ } else if IsXMLType(ct) {
+ err = c.XMLUnmarshal(b, d)
+ }
+
+ return
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// RequestLog and ResponseLog type
+//_______________________________________________________________________
+
+// RequestLog struct is used to collected information from resty request
+// instance for debug logging. It sent to request log callback before resty
+// actually logs the information.
+type RequestLog struct {
+ Header http.Header
+ Body string
+}
+
+// ResponseLog struct is used to collected information from resty response
+// instance for debug logging. It sent to response log callback before resty
+// actually logs the information.
+type ResponseLog struct {
+ Header http.Header
+ Body string
+}
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Package Unexported methods
+//_______________________________________________________________________
+
+// way to disable the HTML escape as opt-in
+func jsonMarshal(c *Client, r *Request, d interface{}) (*bytes.Buffer, error) {
+ if !r.jsonEscapeHTML || !c.jsonEscapeHTML {
+ return noescapeJSONMarshal(d)
+ }
+
+ data, err := c.JSONMarshal(d)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := acquireBuffer()
+ _, _ = buf.Write(data)
+ return buf, nil
+}
+
+func firstNonEmpty(v ...string) string {
+ for _, s := range v {
+ if !IsStringEmpty(s) {
+ return s
+ }
+ }
+ return ""
+}
+
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
+func escapeQuotes(s string) string {
+ return quoteEscaper.Replace(s)
+}
+
+func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader {
+ hdr := make(textproto.MIMEHeader)
+
+ var contentDispositionValue string
+ if IsStringEmpty(fileName) {
+ contentDispositionValue = fmt.Sprintf(`form-data; name="%s"`, param)
+ } else {
+ contentDispositionValue = fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
+ param, escapeQuotes(fileName))
+ }
+ hdr.Set("Content-Disposition", contentDispositionValue)
+
+ if !IsStringEmpty(contentType) {
+ hdr.Set(hdrContentTypeKey, contentType)
+ }
+ return hdr
+}
+
+func addMultipartFormField(w *multipart.Writer, mf *MultipartField) error {
+ partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType))
+ if err != nil {
+ return err
+ }
+
+ _, err = io.Copy(partWriter, mf.Reader)
+ return err
+}
+
+func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error {
+ // Auto detect actual multipart content type
+ cbuf := make([]byte, 512)
+ size, err := r.Read(cbuf)
+ if err != nil && err != io.EOF {
+ return err
+ }
+
+ partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf)))
+ if err != nil {
+ return err
+ }
+
+ if _, err = partWriter.Write(cbuf[:size]); err != nil {
+ return err
+ }
+
+ _, err = io.Copy(partWriter, r)
+ return err
+}
+
+func addFile(w *multipart.Writer, fieldName, path string) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer closeq(file)
+ return writeMultipartFormFile(w, fieldName, filepath.Base(path), file)
+}
+
+func addFileReader(w *multipart.Writer, f *File) error {
+ return writeMultipartFormFile(w, f.ParamName, f.Name, f.Reader)
+}
+
+func getPointer(v interface{}) interface{} {
+ vv := valueOf(v)
+ if vv.Kind() == reflect.Ptr {
+ return v
+ }
+ return reflect.New(vv.Type()).Interface()
+}
+
+func isPayloadSupported(m string, allowMethodGet bool) bool {
+ return !(m == MethodHead || m == MethodOptions || (m == MethodGet && !allowMethodGet))
+}
+
+func typeOf(i interface{}) reflect.Type {
+ return indirect(valueOf(i)).Type()
+}
+
+func valueOf(i interface{}) reflect.Value {
+ return reflect.ValueOf(i)
+}
+
+func indirect(v reflect.Value) reflect.Value {
+ return reflect.Indirect(v)
+}
+
+func kindOf(v interface{}) reflect.Kind {
+ return typeOf(v).Kind()
+}
+
+func createDirectory(dir string) (err error) {
+ if _, err = os.Stat(dir); err != nil {
+ if os.IsNotExist(err) {
+ if err = os.MkdirAll(dir, 0755); err != nil {
+ return
+ }
+ }
+ }
+ return
+}
+
+func canJSONMarshal(contentType string, kind reflect.Kind) bool {
+ return IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice)
+}
+
+func functionName(i interface{}) string {
+ return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
+}
+
+func acquireBuffer() *bytes.Buffer {
+ return bufPool.Get().(*bytes.Buffer)
+}
+
+func releaseBuffer(buf *bytes.Buffer) {
+ if buf != nil {
+ buf.Reset()
+ bufPool.Put(buf)
+ }
+}
+
+// requestBodyReleaser wraps requests's body and implements custom Close for it.
+// The Close method closes original body and releases request body back to sync.Pool.
+type requestBodyReleaser struct {
+ releaseOnce sync.Once
+ reqBuf *bytes.Buffer
+ io.ReadCloser
+}
+
+func newRequestBodyReleaser(respBody io.ReadCloser, reqBuf *bytes.Buffer) io.ReadCloser {
+ if reqBuf == nil {
+ return respBody
+ }
+
+ return &requestBodyReleaser{
+ reqBuf: reqBuf,
+ ReadCloser: respBody,
+ }
+}
+
+func (rr *requestBodyReleaser) Close() error {
+ err := rr.ReadCloser.Close()
+ rr.releaseOnce.Do(func() {
+ releaseBuffer(rr.reqBuf)
+ })
+
+ return err
+}
+
+func closeq(v interface{}) {
+ if c, ok := v.(io.Closer); ok {
+ silently(c.Close())
+ }
+}
+
+func silently(_ ...interface{}) {}
+
+func composeHeaders(c *Client, r *Request, hdrs http.Header) string {
+ str := make([]string, 0, len(hdrs))
+ for _, k := range sortHeaderKeys(hdrs) {
+ var v string
+ if k == "Cookie" {
+ cv := strings.TrimSpace(strings.Join(hdrs[k], ", "))
+ if c.GetClient().Jar != nil {
+ for _, c := range c.GetClient().Jar.Cookies(r.RawRequest.URL) {
+ if cv != "" {
+ cv = cv + "; " + c.String()
+ } else {
+ cv = c.String()
+ }
+ }
+ }
+ v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, cv))
+ } else {
+ v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", ")))
+ }
+ if v != "" {
+ str = append(str, "\t"+v)
+ }
+ }
+ return strings.Join(str, "\n")
+}
+
+func sortHeaderKeys(hdrs http.Header) []string {
+ keys := make([]string, 0, len(hdrs))
+ for key := range hdrs {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+func copyHeaders(hdrs http.Header) http.Header {
+ nh := http.Header{}
+ for k, v := range hdrs {
+ nh[k] = v
+ }
+ return nh
+}
+
+type noRetryErr struct {
+ err error
+}
+
+func (e *noRetryErr) Error() string {
+ return e.err.Error()
+}
+
+func wrapNoRetryErr(err error) error {
+ if err != nil {
+ err = &noRetryErr{err: err}
+ }
+ return err
+}
+
+func unwrapNoRetryErr(err error) error {
+ if e, ok := err.(*noRetryErr); ok {
+ err = e.err
+ }
+ return err
+}
diff --git a/vendor/golang.org/x/net/publicsuffix/list.go b/vendor/golang.org/x/net/publicsuffix/list.go
new file mode 100644
index 000000000..200617ea8
--- /dev/null
+++ b/vendor/golang.org/x/net/publicsuffix/list.go
@@ -0,0 +1,181 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:generate go run gen.go
+
+// Package publicsuffix provides a public suffix list based on data from
+// https://publicsuffix.org/
+//
+// A public suffix is one under which Internet users can directly register
+// names. It is related to, but different from, a TLD (top level domain).
+//
+// "com" is a TLD (top level domain). Top level means it has no dots.
+//
+// "com" is also a public suffix. Amazon and Google have registered different
+// siblings under that domain: "amazon.com" and "google.com".
+//
+// "au" is another TLD, again because it has no dots. But it's not "amazon.au".
+// Instead, it's "amazon.com.au".
+//
+// "com.au" isn't an actual TLD, because it's not at the top level (it has
+// dots). But it is an eTLD (effective TLD), because that's the branching point
+// for domain name registrars.
+//
+// Another name for "an eTLD" is "a public suffix". Often, what's more of
+// interest is the eTLD+1, or one more label than the public suffix. For
+// example, browsers partition read/write access to HTTP cookies according to
+// the eTLD+1. Web pages served from "amazon.com.au" can't read cookies from
+// "google.com.au", but web pages served from "maps.google.com" can share
+// cookies from "www.google.com", so you don't have to sign into Google Maps
+// separately from signing into Google Web Search. Note that all four of those
+// domains have 3 labels and 2 dots. The first two domains are each an eTLD+1,
+// the last two are not (but share the same eTLD+1: "google.com").
+//
+// All of these domains have the same eTLD+1:
+// - "www.books.amazon.co.uk"
+// - "books.amazon.co.uk"
+// - "amazon.co.uk"
+// Specifically, the eTLD+1 is "amazon.co.uk", because the eTLD is "co.uk".
+//
+// There is no closed form algorithm to calculate the eTLD of a domain.
+// Instead, the calculation is data driven. This package provides a
+// pre-compiled snapshot of Mozilla's PSL (Public Suffix List) data at
+// https://publicsuffix.org/
+package publicsuffix // import "golang.org/x/net/publicsuffix"
+
+// TODO: specify case sensitivity and leading/trailing dot behavior for
+// func PublicSuffix and func EffectiveTLDPlusOne.
+
+import (
+ "fmt"
+ "net/http/cookiejar"
+ "strings"
+)
+
+// List implements the cookiejar.PublicSuffixList interface by calling the
+// PublicSuffix function.
+var List cookiejar.PublicSuffixList = list{}
+
+type list struct{}
+
+func (list) PublicSuffix(domain string) string {
+ ps, _ := PublicSuffix(domain)
+ return ps
+}
+
+func (list) String() string {
+ return version
+}
+
+// PublicSuffix returns the public suffix of the domain using a copy of the
+// publicsuffix.org database compiled into the library.
+//
+// icann is whether the public suffix is managed by the Internet Corporation
+// for Assigned Names and Numbers. If not, the public suffix is either a
+// privately managed domain (and in practice, not a top level domain) or an
+// unmanaged top level domain (and not explicitly mentioned in the
+// publicsuffix.org list). For example, "foo.org" and "foo.co.uk" are ICANN
+// domains, "foo.dyndns.org" and "foo.blogspot.co.uk" are private domains and
+// "cromulent" is an unmanaged top level domain.
+//
+// Use cases for distinguishing ICANN domains like "foo.com" from private
+// domains like "foo.appspot.com" can be found at
+// https://wiki.mozilla.org/Public_Suffix_List/Use_Cases
+func PublicSuffix(domain string) (publicSuffix string, icann bool) {
+ lo, hi := uint32(0), uint32(numTLD)
+ s, suffix, icannNode, wildcard := domain, len(domain), false, false
+loop:
+ for {
+ dot := strings.LastIndex(s, ".")
+ if wildcard {
+ icann = icannNode
+ suffix = 1 + dot
+ }
+ if lo == hi {
+ break
+ }
+ f := find(s[1+dot:], lo, hi)
+ if f == notFound {
+ break
+ }
+
+ u := nodes[f] >> (nodesBitsTextOffset + nodesBitsTextLength)
+ icannNode = u&(1<>= nodesBitsICANN
+ u = children[u&(1<>= childrenBitsLo
+ hi = u & (1<>= childrenBitsHi
+ switch u & (1<>= childrenBitsNodeType
+ wildcard = u&(1<>= nodesBitsTextLength
+ offset := x & (1<