From b839fd052e19aa80d538190a532ebb206ed527ad Mon Sep 17 00:00:00 2001 From: Vaibhav Date: Wed, 11 Dec 2019 21:04:48 -0500 Subject: [PATCH] Release 0.12 (#423) --- .argo-ci/ci.yaml | 120 +- .travis.yml | 6 +- Gopkg.lock | 480 ++- Gopkg.toml | 6 +- Makefile | 94 +- README.md | 29 +- VERSION | 2 +- api/event-source.html | 1755 ++++++++ api/event-source.md | 3489 ++++++++++++++++ api/gateway.html | 731 ++++ api/gateway.md | 1451 +++++++ api/generate.sh | 5 + api/sensor.html | 1816 ++++++++ api/sensor.md | 3661 +++++++++++++++++ common/common.go | 154 +- common/logger.go | 31 +- common/util.go | 30 +- common/util_test.go | 19 +- controllers/common/informer.go | 86 - controllers/common/informer_test.go | 191 - controllers/common/util.go | 58 +- controllers/common/util_test.go | 66 +- .../gateway/cmd}/main.go | 8 +- .../gateway/common.go | 28 +- controllers/gateway/config.go | 29 +- controllers/gateway/config_test.go | 65 +- controllers/gateway/controller.go | 145 +- controllers/gateway/controller_test.go | 120 +- controllers/gateway/informer.go | 37 +- controllers/gateway/informer_test.go | 40 +- controllers/gateway/operator.go | 408 +- controllers/gateway/operator_test.go | 450 +- controllers/gateway/resource.go | 309 +- controllers/gateway/resource_test.go | 316 ++ controllers/gateway/state.go | 50 - controllers/gateway/state_test.go | 44 - controllers/gateway/validate.go | 50 +- controllers/gateway/validate_test.go | 31 +- .../sensor => controllers/sensor/cmd}/main.go | 10 +- .../sensor/common.go | 35 +- controllers/sensor/config.go | 57 +- controllers/sensor/config_test.go | 60 +- controllers/sensor/controller.go | 193 +- controllers/sensor/controller_test.go | 118 +- controllers/sensor/informer.go | 48 +- controllers/sensor/informer_test.go | 28 +- controllers/sensor/{state.go => node.go} | 53 +- controllers/sensor/node_test.go | 93 + controllers/sensor/operator.go | 485 +-- controllers/sensor/operator_test.go | 462 +-- controllers/sensor/resource.go | 270 +- controllers/sensor/resource_test.go | 170 + controllers/sensor/state_test.go | 65 - controllers/sensor/validate.go | 1 - controllers/sensor/validate_test.go | 28 +- docs/assets/argo.png | Bin 0 -> 27999 bytes docs/assets/gateway.png | Bin 0 -> 97755 bytes docs/assets/gateways.png | Bin 120284 -> 0 bytes docs/assets/sensor.png | Bin 82058 -> 86958 bytes docs/communication.md | 55 - docs/concepts/event_source.md | 7 + docs/concepts/gateway.md | 20 + docs/{ => concepts}/parameterization.md | 0 docs/concepts/sensor.md | 16 + docs/{ => concepts}/trigger.md | 5 +- docs/controllers.md | 24 +- docs/developer_guide.md | 89 + docs/gateway.md | 179 - docs/gateways/artifact.md | 32 - docs/gateways/aws-sns.md | 26 - docs/gateways/aws-sqs.md | 25 - docs/gateways/calendar.md | 11 - docs/gateways/file.md | 22 - docs/gateways/gcp-pubsub.md | 15 - docs/gateways/github.md | 24 - docs/gateways/gitlab.md | 21 - docs/gateways/resource.md | 11 - docs/gateways/slack.md | 15 - docs/gateways/storage-grid.md | 46 - docs/gateways/streams.md | 20 - docs/gateways/webhook.md | 35 - docs/getting_started.md | 29 +- docs/index.md | 34 +- docs/installation.md | 25 +- docs/sensor.md | 453 -- examples/event-sources/amqp.yaml | 62 +- examples/event-sources/artifact.yaml | 61 - examples/event-sources/aws-sns.yaml | 124 +- examples/event-sources/aws-sqs.yaml | 75 +- examples/event-sources/calendar.yaml | 71 +- examples/event-sources/file.yaml | 44 +- examples/event-sources/gcp-pubsub.yaml | 34 +- examples/event-sources/github.yaml | 138 +- examples/event-sources/gitlab.yaml | 100 +- examples/event-sources/hdfs.yaml | 56 +- examples/event-sources/kafka.yaml | 56 +- examples/event-sources/minio.yaml | 57 + examples/event-sources/mqtt.yaml | 58 +- examples/event-sources/nats.yaml | 54 +- examples/event-sources/resource.yaml | 144 +- examples/event-sources/slack.yaml | 96 +- examples/event-sources/storage-grid.yaml | 66 +- examples/event-sources/webhook.yaml | 60 +- examples/gateways/amqp.yaml | 12 +- examples/gateways/aws-sns.yaml | 11 +- examples/gateways/aws-sqs.yaml | 11 +- examples/gateways/calendar.yaml | 9 +- examples/gateways/file.yaml | 9 +- examples/gateways/gcp-pubsub.yaml | 9 +- examples/gateways/github.yaml | 9 +- examples/gateways/gitlab.yaml | 9 +- examples/gateways/hdfs.yaml | 27 +- examples/gateways/kafka.yaml | 9 +- ...standard.yaml => minio-nats-standard.yaml} | 23 +- ...reaming.yaml => minio-nats-streaming.yaml} | 23 +- .../gateways/{artifact.yaml => minio.yaml} | 26 +- examples/gateways/mqtt.yaml | 9 +- examples/gateways/multi-watchers.yaml | 21 +- examples/gateways/nats.yaml | 9 +- examples/gateways/resource.yaml | 9 +- examples/gateways/secure-webhook.yaml | 9 +- .../sensor-in-different-namespace.yaml | 9 +- examples/gateways/slack.yaml | 9 +- examples/gateways/storage-grid.yaml | 11 +- examples/gateways/webhook-nats-standard.yaml | 9 +- examples/gateways/webhook-nats-streaming.yaml | 9 +- examples/gateways/webhook.yaml | 31 +- examples/sensors/amqp.yaml | 27 +- .../artifact-with-param-nats-standard.yaml | 51 - .../artifact-with-param-nats-streaming.yaml | 53 - examples/sensors/aws-sns.yaml | 6 +- examples/sensors/aws-sqs.yaml | 10 +- examples/sensors/calendar.yaml | 5 +- .../complete-trigger-parameterization.yaml | 3 - examples/sensors/context-filter-webhook.yaml | 3 - examples/sensors/data-filter-webhook.yaml | 9 +- .../sensors/dependencies-circuit-complex.yaml | 148 - examples/sensors/dependencies-circuit.yaml | 17 +- examples/sensors/file.yaml | 5 +- examples/sensors/gcp-pubsub.yaml | 9 +- examples/sensors/github.yaml | 5 +- examples/sensors/gitlab.yaml | 7 +- examples/sensors/hdfs.yaml | 37 +- examples/sensors/kafka.yaml | 5 +- .../sensors/{artifact.yaml => minio.yaml} | 15 +- examples/sensors/mqtt-sensor.yaml | 5 +- examples/sensors/multi-signal-sensor.yaml | 11 +- examples/sensors/multi-trigger-sensor.yaml | 35 +- examples/sensors/nats.yaml | 5 +- examples/sensors/resource.yaml | 5 +- examples/sensors/slack.yaml | 9 +- examples/sensors/storage-grid.yaml | 5 +- examples/sensors/time-filter-webhook.yaml | 35 +- examples/sensors/trigger-gateway.yaml | 96 - examples/sensors/trigger-resource.yaml | 54 - .../sensors/trigger-source-configmap.yaml | 7 +- examples/sensors/trigger-source-file.yaml | 3 - examples/sensors/trigger-source-git.yaml | 5 +- .../trigger-standard-k8s-resource.yaml | 9 +- examples/sensors/trigger-with-backoff.yaml | 13 +- examples/sensors/url-sensor.yaml | 5 +- examples/sensors/webhook-nats-streaming.yaml | 5 +- examples/sensors/webhook-nats.yaml | 5 +- examples/sensors/webhook.yaml | 5 +- gateways/{ => client}/Dockerfile | 0 gateways/{cmd/main.go => client/client.go} | 15 +- gateways/client/context.go | 158 + gateways/client/event-source_test.go | 240 ++ gateways/client/event-sources.go | 356 ++ gateways/{ => client}/state.go | 95 +- gateways/{ => client}/state_test.go | 44 +- gateways/client/transformer.go | 165 + gateways/{ => client}/transformer_test.go | 22 +- gateways/{ => client}/watcher.go | 91 +- gateways/common.go | 14 + gateways/common/fake.go | 100 - gateways/common/validate.go | 47 - gateways/common/webhook.go | 314 -- gateways/common/webhook_test.go | 151 - gateways/community/aws-sns/config_test.go | 65 - gateways/community/aws-sns/start.go | 188 - gateways/community/aws-sns/start_test.go | 120 - gateways/community/aws-sns/validate.go | 44 - gateways/community/aws-sns/validate_test.go | 58 - gateways/community/aws-sqs/config.go | 63 - gateways/community/aws-sqs/config_test.go | 59 - gateways/community/aws-sqs/start.go | 117 - gateways/community/aws-sqs/start_test.go | 90 - gateways/community/aws-sqs/validate.go | 47 - gateways/community/aws-sqs/validate_test.go | 58 - gateways/community/gcp-pubsub/config.go | 51 - gateways/community/gcp-pubsub/start.go | 128 - gateways/community/gcp-pubsub/start_test.go | 57 - gateways/community/gcp-pubsub/validate.go | 50 - .../community/gcp-pubsub/validate_test.go | 59 - gateways/community/github/config.go | 90 - gateways/community/github/config_test.go | 47 - gateways/community/github/start.go | 247 -- gateways/community/github/validate.go | 51 - gateways/community/github/validate_test.go | 58 - gateways/community/gitlab/cmd/main.go | 44 - gateways/community/gitlab/config.go | 87 - gateways/community/gitlab/config_test.go | 48 - gateways/community/gitlab/start.go | 157 - gateways/community/gitlab/start_test.go | 111 - gateways/community/gitlab/validate.go | 47 - gateways/community/gitlab/validate_test.go | 59 - gateways/community/hdfs/cmd/main.go | 28 - gateways/community/hdfs/config.go | 99 - gateways/community/hdfs/start.go | 154 - gateways/community/hdfs/validate.go | 59 - gateways/community/hdfs/validate_test.go | 42 - gateways/community/slack/config.go | 63 - gateways/community/slack/validate.go | 41 - gateways/community/slack/validate_test.go | 58 - gateways/community/storagegrid/cmd/main.go | 29 - gateways/community/storagegrid/config_test.go | 44 - gateways/community/storagegrid/start.go | 183 - gateways/community/storagegrid/validate.go | 36 - .../community/storagegrid/validate_test.go | 58 - gateways/config.go | 168 - gateways/core/artifact/Dockerfile | 3 - gateways/core/artifact/config.go | 44 - gateways/core/artifact/config_test.go | 51 - gateways/core/artifact/start.go | 88 - gateways/core/artifact/validate.go | 60 - gateways/core/artifact/validate_test.go | 58 - gateways/core/calendar/config.go | 77 - gateways/core/calendar/config_test.go | 37 - gateways/core/calendar/start.go | 143 - gateways/core/calendar/validate.go | 44 - gateways/core/calendar/validate_test.go | 58 - gateways/core/file/config.go | 49 - gateways/core/file/config_test.go | 38 - gateways/core/file/start.go | 122 - gateways/core/file/validate.go | 42 - gateways/core/file/validate_test.go | 58 - gateways/core/resource/config.go | 79 - gateways/core/resource/config_test.go | 49 - gateways/core/resource/validate.go | 44 - gateways/core/resource/validate_test.go | 59 - gateways/core/stream/amqp/config.go | 71 - gateways/core/stream/amqp/config_test.go | 39 - gateways/core/stream/amqp/start.go | 148 - gateways/core/stream/amqp/validate.go | 49 - gateways/core/stream/amqp/validate_test.go | 58 - gateways/core/stream/kafka/config.go | 54 - gateways/core/stream/kafka/config_test.go | 38 - gateways/core/stream/kafka/start.go | 121 - gateways/core/stream/kafka/validate.go | 47 - gateways/core/stream/kafka/validate_test.go | 58 - gateways/core/stream/mqtt/config.go | 54 - gateways/core/stream/mqtt/start.go | 91 - gateways/core/stream/mqtt/validate.go | 46 - gateways/core/stream/mqtt/validate_test.go | 58 - gateways/core/stream/nats/config.go | 52 - gateways/core/stream/nats/config_test.go | 37 - gateways/core/stream/nats/start.go | 90 - gateways/core/stream/nats/validate.go | 43 - gateways/core/stream/nats/validate_test.go | 58 - gateways/core/webhook/cmd/main.go | 29 - gateways/core/webhook/config.go | 43 - gateways/core/webhook/start.go | 120 - gateways/core/webhook/start_test.go | 74 - gateways/core/webhook/validate.go | 51 - gateways/core/webhook/validate_test.go | 72 - gateways/event-source_test.go | 170 - gateways/event-sources.go | 275 -- gateways/eventing.pb.go | 62 +- gateways/eventing.proto | 10 +- .../{core/stream => server}/amqp/Dockerfile | 0 .../{core/file => server/amqp}/cmd/main.go | 8 +- gateways/server/amqp/start.go | 135 + gateways/server/amqp/validate.go | 78 + gateways/server/amqp/validate_test.go | 64 + .../{community => server}/aws-sns/Dockerfile | 0 .../{community => server}/aws-sns/cmd/main.go | 16 +- gateways/server/aws-sns/start.go | 187 + .../config.go => server/aws-sns/types.go} | 59 +- gateways/server/aws-sns/validate.go | 71 + gateways/server/aws-sns/validate_test.go | 64 + .../{community => server}/aws-sqs/Dockerfile | 0 .../artifact => server/aws-sqs}/cmd/main.go | 14 +- gateways/server/aws-sqs/start.go | 121 + gateways/server/aws-sqs/validate.go | 73 + gateways/server/aws-sqs/validate_test.go | 64 + gateways/{core => server}/calendar/Dockerfile | 0 gateways/server/calendar/cmd/main.go | 29 + gateways/server/calendar/start.go | 164 + .../{core => server}/calendar/start_test.go | 47 +- gateways/server/calendar/validate.go | 74 + gateways/server/calendar/validate_test.go | 64 + gateways/{common => server/common/aws}/aws.go | 16 +- .../{common => server/common/aws}/aws_test.go | 2 +- gateways/server/common/fake.go | 41 + .../common/fsevent}/config.go | 2 +- .../common/fsevent}/config_test.go | 2 +- .../{ => server}/common/fsevent/fileevent.go | 0 .../{ => server}/common/naivewatcher/mutex.go | 0 .../common/naivewatcher/watcher.go | 2 +- .../common/naivewatcher/watcher_test.go | 2 +- gateways/server/common/webhook/fake.go | 76 + gateways/server/common/webhook/types.go | 92 + gateways/server/common/webhook/validate.go | 62 + gateways/server/common/webhook/webhook.go | 193 + .../common/webhook/webhook_test.go} | 28 +- gateways/{core => server}/file/Dockerfile | 0 .../stream/nats => server/file}/cmd/main.go | 8 +- gateways/server/file/start.go | 141 + gateways/server/file/validate.go | 68 + gateways/server/file/validate_test.go | 64 + .../gcp-pubsub/Dockerfile | 0 .../gcp-pubsub}/cmd/main.go | 8 +- gateways/server/gcp-pubsub/start.go | 156 + gateways/server/gcp-pubsub/validate.go | 76 + gateways/server/gcp-pubsub/validate_test.go | 64 + .../{community => server}/github/Dockerfile | 0 .../{community => server}/github/cmd/main.go | 15 +- .../{community => server}/github/hook_util.go | 0 .../github/hook_util_test.go | 4 +- gateways/server/github/start.go | 299 ++ .../github/start_test.go | 92 +- .../{community => server}/github/tokenauth.go | 0 gateways/server/github/types.go | 54 + gateways/server/github/validate.go | 79 + gateways/server/github/validate_test.go | 64 + .../{community => server}/gitlab/Dockerfile | 0 .../aws-sqs => server/gitlab}/cmd/main.go | 15 +- gateways/server/gitlab/start.go | 202 + gateways/server/gitlab/types.go | 54 + gateways/server/gitlab/validate.go | 76 + gateways/server/gitlab/validate_test.go | 66 + .../{community => server}/hdfs/Dockerfile | 0 gateways/{community => server}/hdfs/client.go | 27 +- gateways/server/hdfs/cmd/main.go | 23 + gateways/server/hdfs/start.go | 182 + gateways/server/hdfs/validate.go | 101 + gateways/server/hdfs/validate_test.go | 50 + .../{core/stream => server}/kafka/Dockerfile | 0 .../stream/amqp => server/kafka}/cmd/main.go | 8 +- gateways/server/kafka/start.go | 143 + gateways/server/kafka/validate.go | 75 + gateways/server/kafka/validate_test.go | 64 + gateways/server/minio/Dockerfile | 3 + .../slack => server/minio}/cmd/main.go | 16 +- gateways/server/minio/start.go | 108 + .../artifact => server/minio}/start_test.go | 53 +- gateways/server/minio/validate.go | 84 + gateways/server/minio/validate_test.go | 66 + .../{core/stream => server}/mqtt/Dockerfile | 0 .../{core/stream => server}/mqtt/cmd/main.go | 8 +- gateways/server/mqtt/start.go | 110 + gateways/server/mqtt/validate.go | 75 + gateways/server/mqtt/validate_test.go | 64 + .../{core/stream => server}/nats/Dockerfile | 0 gateways/server/nats/cmd/main.go | 29 + gateways/server/nats/start.go | 109 + gateways/server/nats/validate.go | 72 + gateways/server/nats/validate_test.go | 64 + gateways/{core => server}/resource/Dockerfile | 0 .../{core => server}/resource/cmd/main.go | 8 +- gateways/{core => server}/resource/start.go | 88 +- .../{core => server}/resource/start_test.go | 24 +- gateways/server/resource/validate.go | 72 + gateways/server/resource/validate_test.go | 66 + gateways/{gateway.go => server/server.go} | 20 +- .../server_test.go} | 12 +- .../{community => server}/slack/Dockerfile | 0 gateways/server/slack/cmd/main.go | 39 + gateways/{community => server}/slack/start.go | 143 +- .../{community => server}/slack/start_test.go | 97 +- gateways/server/slack/types.go | 46 + gateways/server/slack/validate.go | 70 + gateways/server/slack/validate_test.go | 64 + .../storagegrid/Dockerfile | 0 .../kafka => server/storagegrid}/cmd/main.go | 8 +- gateways/server/storagegrid/start.go | 199 + .../storagegrid/start_test.go | 93 +- .../config.go => server/storagegrid/types.go} | 61 +- gateways/server/storagegrid/validate.go | 66 + gateways/server/storagegrid/validate_test.go | 66 + gateways/{ => server}/utils.go | 2 +- gateways/{ => server}/utils_test.go | 2 +- gateways/{core => server}/webhook/Dockerfile | 0 gateways/server/webhook/cmd/main.go | 29 + gateways/server/webhook/start.go | 145 + gateways/server/webhook/validate.go | 74 + gateways/server/webhook/validate_test.go | 64 + gateways/transformer.go | 175 - .../gateway-controller-deployment.yaml | 20 +- .../sensor-controller-deployment.yaml | 20 +- .../manifests/argo-events-cluster-roles.yaml | 10 +- hack/k8s/manifests/argo-events-role.yaml | 82 + hack/k8s/manifests/event-source-crd.yaml | 16 + .../gateway-controller-deployment.yaml | 20 +- hack/k8s/manifests/gateway-crd.yaml | 4 +- hack/k8s/manifests/installation.yaml | 217 + .../sensor-controller-deployment.yaml | 21 +- hack/k8s/manifests/sensor-crd.yaml | 4 +- hack/k8s/manifests/workflow-crd.yaml | 12 - hack/update-api-docs.sh | 34 + hack/update-codegen.sh | 20 +- hack/update-openapigen.sh | 7 + mkdocs.yml | 14 +- pkg/apis/common/deepcopy_generated.go | 18 - pkg/apis/common/event-sources.go | 41 + pkg/apis/common/event.go | 20 +- pkg/apis/common/s3.go | 2 +- .../apis/eventsources/register.go | 23 +- pkg/apis/eventsources/v1alpha1/doc.go | 21 + .../v1alpha1/openapi_generated.go | 1391 +++++++ pkg/apis/eventsources/v1alpha1/register.go | 56 + pkg/apis/eventsources/v1alpha1/types.go | 387 ++ .../apis/eventsources/v1alpha1/validate.go | 20 +- .../v1alpha1/zz_generated.deepcopy.go | 681 +++ .../gateway/v1alpha1/openapi_generated.go | 90 +- pkg/apis/gateway/v1alpha1/types.go | 58 +- .../gateway/v1alpha1/zz_generated.deepcopy.go | 63 +- pkg/apis/sensor/v1alpha1/openapi_generated.go | 61 +- pkg/apis/sensor/v1alpha1/types.go | 104 +- .../sensor/v1alpha1/zz_generated.deepcopy.go | 35 +- .../clientset/versioned/clientset.go | 89 + .../eventsources/clientset/versioned/doc.go | 19 + .../versioned/fake/clientset_generated.go | 81 + .../clientset/versioned/fake/doc.go | 19 + .../clientset/versioned/fake/register.go | 55 + .../clientset/versioned/scheme/doc.go | 19 + .../clientset/versioned/scheme/register.go | 55 + .../typed/eventsources/v1alpha1/doc.go | 19 + .../eventsources/v1alpha1/eventsource.go | 190 + .../v1alpha1/eventsources_client.go | 88 + .../typed/eventsources/v1alpha1/fake/doc.go | 19 + .../v1alpha1/fake/fake_eventsource.go | 139 + .../v1alpha1/fake/fake_eventsources_client.go | 39 + .../v1alpha1/generated_expansion.go | 8 +- .../eventsources/interface.go | 45 + .../eventsources/v1alpha1/eventsource.go | 88 + .../eventsources/v1alpha1/interface.go | 44 + .../informers/externalversions/factory.go | 179 + .../informers/externalversions/generic.go | 61 + .../internalinterfaces/factory_interfaces.go | 39 + .../eventsources/v1alpha1/eventsource.go | 93 + .../v1alpha1/expansion_generated.go | 28 +- .../gateway/clientset/versioned/clientset.go | 6 +- .../clientset/versioned/fake/register.go | 6 +- .../clientset/versioned/scheme/register.go | 6 +- .../gateway/v1alpha1/fake/fake_gateway.go | 12 +- .../typed/gateway/v1alpha1/gateway.go | 10 +- .../typed/gateway/v1alpha1/gateway_client.go | 4 +- .../informers/externalversions/factory.go | 16 +- .../internalinterfaces/factory_interfaces.go | 8 +- .../sensor/clientset/versioned/clientset.go | 6 +- .../clientset/versioned/fake/register.go | 6 +- .../clientset/versioned/scheme/register.go | 6 +- .../typed/sensor/v1alpha1/fake/fake_sensor.go | 12 +- .../versioned/typed/sensor/v1alpha1/sensor.go | 10 +- .../typed/sensor/v1alpha1/sensor_client.go | 4 +- .../informers/externalversions/factory.go | 16 +- .../internalinterfaces/factory_interfaces.go | 8 +- sensors/cmd/client.go | 2 +- sensors/event-handler.go | 12 +- sensors/event-handler_test.go | 475 --- sensors/signal-filter_test.go | 343 -- sensors/trigger-params_test.go | 229 -- sensors/trigger.go | 11 +- sensors/trigger_test.go | 457 -- store/configmap_test.go | 2 +- store/creds.go | 4 +- store/creds_test.go | 4 +- store/git_test.go | 4 +- store/resource.go | 2 +- store/store_test.go | 94 +- test/e2e/common/client.go | 129 - test/e2e/core/main_test.go | 165 - .../webhook-gateway-event-source.yaml | 15 - .../general-use-case/webhook-gateway.yaml | 56 - .../general-use-case/webhook-sensor.yaml | 45 - version.go | 2 +- 478 files changed, 28737 insertions(+), 15890 deletions(-) create mode 100644 api/event-source.html create mode 100644 api/event-source.md create mode 100644 api/gateway.html create mode 100644 api/gateway.md create mode 100644 api/generate.sh create mode 100644 api/sensor.html create mode 100644 api/sensor.md delete mode 100644 controllers/common/informer.go delete mode 100644 controllers/common/informer_test.go rename {cmd/controllers/gateway => controllers/gateway/cmd}/main.go (82%) rename gateways/community/gcp-pubsub/config_test.go => controllers/gateway/common.go (54%) create mode 100644 controllers/gateway/resource_test.go delete mode 100644 controllers/gateway/state.go delete mode 100644 controllers/gateway/state_test.go rename {cmd/controllers/sensor => controllers/sensor/cmd}/main.go (78%) rename gateways/community/slack/config_test.go => controllers/sensor/common.go (50%) rename controllers/sensor/{state.go => node.go} (65%) create mode 100644 controllers/sensor/node_test.go create mode 100644 controllers/sensor/resource_test.go delete mode 100644 controllers/sensor/state_test.go create mode 100644 docs/assets/argo.png create mode 100644 docs/assets/gateway.png delete mode 100644 docs/assets/gateways.png delete mode 100644 docs/communication.md create mode 100644 docs/concepts/event_source.md create mode 100644 docs/concepts/gateway.md rename docs/{ => concepts}/parameterization.md (100%) create mode 100644 docs/concepts/sensor.md rename docs/{ => concepts}/trigger.md (95%) create mode 100644 docs/developer_guide.md delete mode 100644 docs/gateway.md delete mode 100644 docs/gateways/artifact.md delete mode 100644 docs/gateways/aws-sns.md delete mode 100644 docs/gateways/aws-sqs.md delete mode 100644 docs/gateways/calendar.md delete mode 100644 docs/gateways/file.md delete mode 100644 docs/gateways/gcp-pubsub.md delete mode 100644 docs/gateways/github.md delete mode 100644 docs/gateways/gitlab.md delete mode 100644 docs/gateways/resource.md delete mode 100644 docs/gateways/slack.md delete mode 100644 docs/gateways/storage-grid.md delete mode 100644 docs/gateways/streams.md delete mode 100644 docs/gateways/webhook.md delete mode 100644 docs/sensor.md delete mode 100644 examples/event-sources/artifact.yaml create mode 100644 examples/event-sources/minio.yaml rename examples/gateways/{artifact-nats-standard.yaml => minio-nats-standard.yaml} (59%) rename examples/gateways/{artifact-nats-streaming.yaml => minio-nats-streaming.yaml} (61%) rename examples/gateways/{artifact.yaml => minio.yaml} (59%) delete mode 100644 examples/sensors/artifact-with-param-nats-standard.yaml delete mode 100644 examples/sensors/artifact-with-param-nats-streaming.yaml delete mode 100644 examples/sensors/dependencies-circuit-complex.yaml rename examples/sensors/{artifact.yaml => minio.yaml} (77%) delete mode 100644 examples/sensors/trigger-gateway.yaml delete mode 100644 examples/sensors/trigger-resource.yaml rename gateways/{ => client}/Dockerfile (100%) rename gateways/{cmd/main.go => client/client.go} (78%) create mode 100644 gateways/client/context.go create mode 100644 gateways/client/event-source_test.go create mode 100644 gateways/client/event-sources.go rename gateways/{ => client}/state.go (50%) rename gateways/{ => client}/state_test.go (75%) create mode 100644 gateways/client/transformer.go rename gateways/{ => client}/transformer_test.go (82%) rename gateways/{ => client}/watcher.go (54%) create mode 100644 gateways/common.go delete mode 100644 gateways/common/fake.go delete mode 100644 gateways/common/validate.go delete mode 100644 gateways/common/webhook.go delete mode 100644 gateways/common/webhook_test.go delete mode 100644 gateways/community/aws-sns/config_test.go delete mode 100644 gateways/community/aws-sns/start.go delete mode 100644 gateways/community/aws-sns/start_test.go delete mode 100644 gateways/community/aws-sns/validate.go delete mode 100644 gateways/community/aws-sns/validate_test.go delete mode 100644 gateways/community/aws-sqs/config.go delete mode 100644 gateways/community/aws-sqs/config_test.go delete mode 100644 gateways/community/aws-sqs/start.go delete mode 100644 gateways/community/aws-sqs/start_test.go delete mode 100644 gateways/community/aws-sqs/validate.go delete mode 100644 gateways/community/aws-sqs/validate_test.go delete mode 100644 gateways/community/gcp-pubsub/config.go delete mode 100644 gateways/community/gcp-pubsub/start.go delete mode 100644 gateways/community/gcp-pubsub/start_test.go delete mode 100644 gateways/community/gcp-pubsub/validate.go delete mode 100644 gateways/community/gcp-pubsub/validate_test.go delete mode 100644 gateways/community/github/config.go delete mode 100644 gateways/community/github/config_test.go delete mode 100644 gateways/community/github/start.go delete mode 100644 gateways/community/github/validate.go delete mode 100644 gateways/community/github/validate_test.go delete mode 100644 gateways/community/gitlab/cmd/main.go delete mode 100644 gateways/community/gitlab/config.go delete mode 100644 gateways/community/gitlab/config_test.go delete mode 100644 gateways/community/gitlab/start.go delete mode 100644 gateways/community/gitlab/start_test.go delete mode 100644 gateways/community/gitlab/validate.go delete mode 100644 gateways/community/gitlab/validate_test.go delete mode 100644 gateways/community/hdfs/cmd/main.go delete mode 100644 gateways/community/hdfs/config.go delete mode 100644 gateways/community/hdfs/start.go delete mode 100644 gateways/community/hdfs/validate.go delete mode 100644 gateways/community/hdfs/validate_test.go delete mode 100644 gateways/community/slack/config.go delete mode 100644 gateways/community/slack/validate.go delete mode 100644 gateways/community/slack/validate_test.go delete mode 100644 gateways/community/storagegrid/cmd/main.go delete mode 100644 gateways/community/storagegrid/config_test.go delete mode 100644 gateways/community/storagegrid/start.go delete mode 100644 gateways/community/storagegrid/validate.go delete mode 100644 gateways/community/storagegrid/validate_test.go delete mode 100644 gateways/config.go delete mode 100644 gateways/core/artifact/Dockerfile delete mode 100644 gateways/core/artifact/config.go delete mode 100644 gateways/core/artifact/config_test.go delete mode 100644 gateways/core/artifact/start.go delete mode 100644 gateways/core/artifact/validate.go delete mode 100644 gateways/core/artifact/validate_test.go delete mode 100644 gateways/core/calendar/config.go delete mode 100644 gateways/core/calendar/config_test.go delete mode 100644 gateways/core/calendar/start.go delete mode 100644 gateways/core/calendar/validate.go delete mode 100644 gateways/core/calendar/validate_test.go delete mode 100644 gateways/core/file/config.go delete mode 100644 gateways/core/file/config_test.go delete mode 100644 gateways/core/file/start.go delete mode 100644 gateways/core/file/validate.go delete mode 100644 gateways/core/file/validate_test.go delete mode 100644 gateways/core/resource/config.go delete mode 100644 gateways/core/resource/config_test.go delete mode 100644 gateways/core/resource/validate.go delete mode 100644 gateways/core/resource/validate_test.go delete mode 100644 gateways/core/stream/amqp/config.go delete mode 100644 gateways/core/stream/amqp/config_test.go delete mode 100644 gateways/core/stream/amqp/start.go delete mode 100644 gateways/core/stream/amqp/validate.go delete mode 100644 gateways/core/stream/amqp/validate_test.go delete mode 100644 gateways/core/stream/kafka/config.go delete mode 100644 gateways/core/stream/kafka/config_test.go delete mode 100644 gateways/core/stream/kafka/start.go delete mode 100644 gateways/core/stream/kafka/validate.go delete mode 100644 gateways/core/stream/kafka/validate_test.go delete mode 100644 gateways/core/stream/mqtt/config.go delete mode 100644 gateways/core/stream/mqtt/start.go delete mode 100644 gateways/core/stream/mqtt/validate.go delete mode 100644 gateways/core/stream/mqtt/validate_test.go delete mode 100644 gateways/core/stream/nats/config.go delete mode 100644 gateways/core/stream/nats/config_test.go delete mode 100644 gateways/core/stream/nats/start.go delete mode 100644 gateways/core/stream/nats/validate.go delete mode 100644 gateways/core/stream/nats/validate_test.go delete mode 100644 gateways/core/webhook/cmd/main.go delete mode 100644 gateways/core/webhook/config.go delete mode 100644 gateways/core/webhook/start.go delete mode 100644 gateways/core/webhook/start_test.go delete mode 100644 gateways/core/webhook/validate.go delete mode 100644 gateways/core/webhook/validate_test.go delete mode 100644 gateways/event-source_test.go delete mode 100644 gateways/event-sources.go rename gateways/{core/stream => server}/amqp/Dockerfile (100%) rename gateways/{core/file => server/amqp}/cmd/main.go (77%) create mode 100644 gateways/server/amqp/start.go create mode 100644 gateways/server/amqp/validate.go create mode 100644 gateways/server/amqp/validate_test.go rename gateways/{community => server}/aws-sns/Dockerfile (100%) rename gateways/{community => server}/aws-sns/cmd/main.go (70%) create mode 100644 gateways/server/aws-sns/start.go rename gateways/{community/aws-sns/config.go => server/aws-sns/types.go} (61%) create mode 100644 gateways/server/aws-sns/validate.go create mode 100644 gateways/server/aws-sns/validate_test.go rename gateways/{community => server}/aws-sqs/Dockerfile (100%) rename gateways/{core/artifact => server/aws-sqs}/cmd/main.go (76%) create mode 100644 gateways/server/aws-sqs/start.go create mode 100644 gateways/server/aws-sqs/validate.go create mode 100644 gateways/server/aws-sqs/validate_test.go rename gateways/{core => server}/calendar/Dockerfile (100%) create mode 100644 gateways/server/calendar/cmd/main.go create mode 100644 gateways/server/calendar/start.go rename gateways/{core => server}/calendar/start_test.go (66%) create mode 100644 gateways/server/calendar/validate.go create mode 100644 gateways/server/calendar/validate_test.go rename gateways/{common => server/common/aws}/aws.go (77%) rename gateways/{common => server/common/aws}/aws_test.go (99%) create mode 100644 gateways/server/common/fake.go rename gateways/{common => server/common/fsevent}/config.go (98%) rename gateways/{common => server/common/fsevent}/config_test.go (98%) rename gateways/{ => server}/common/fsevent/fileevent.go (100%) rename gateways/{ => server}/common/naivewatcher/mutex.go (100%) rename gateways/{ => server}/common/naivewatcher/watcher.go (98%) rename gateways/{ => server}/common/naivewatcher/watcher_test.go (98%) create mode 100644 gateways/server/common/webhook/fake.go create mode 100644 gateways/server/common/webhook/types.go create mode 100644 gateways/server/common/webhook/validate.go create mode 100644 gateways/server/common/webhook/webhook.go rename gateways/{core/webhook/config_test.go => server/common/webhook/webhook_test.go} (61%) rename gateways/{core => server}/file/Dockerfile (100%) rename gateways/{core/stream/nats => server/file}/cmd/main.go (76%) create mode 100644 gateways/server/file/start.go create mode 100644 gateways/server/file/validate.go create mode 100644 gateways/server/file/validate_test.go rename gateways/{community => server}/gcp-pubsub/Dockerfile (100%) rename gateways/{core/calendar => server/gcp-pubsub}/cmd/main.go (76%) create mode 100644 gateways/server/gcp-pubsub/start.go create mode 100644 gateways/server/gcp-pubsub/validate.go create mode 100644 gateways/server/gcp-pubsub/validate_test.go rename gateways/{community => server}/github/Dockerfile (100%) rename gateways/{community => server}/github/cmd/main.go (76%) rename gateways/{community => server}/github/hook_util.go (100%) rename gateways/{community => server}/github/hook_util_test.go (100%) create mode 100644 gateways/server/github/start.go rename gateways/{community => server}/github/start_test.go (60%) rename gateways/{community => server}/github/tokenauth.go (100%) create mode 100644 gateways/server/github/types.go create mode 100644 gateways/server/github/validate.go create mode 100644 gateways/server/github/validate_test.go rename gateways/{community => server}/gitlab/Dockerfile (100%) rename gateways/{community/aws-sqs => server/gitlab}/cmd/main.go (70%) create mode 100644 gateways/server/gitlab/start.go create mode 100644 gateways/server/gitlab/types.go create mode 100644 gateways/server/gitlab/validate.go create mode 100644 gateways/server/gitlab/validate_test.go rename gateways/{community => server}/hdfs/Dockerfile (100%) rename gateways/{community => server}/hdfs/client.go (79%) create mode 100644 gateways/server/hdfs/cmd/main.go create mode 100644 gateways/server/hdfs/start.go create mode 100644 gateways/server/hdfs/validate.go create mode 100644 gateways/server/hdfs/validate_test.go rename gateways/{core/stream => server}/kafka/Dockerfile (100%) rename gateways/{core/stream/amqp => server/kafka}/cmd/main.go (76%) create mode 100644 gateways/server/kafka/start.go create mode 100644 gateways/server/kafka/validate.go create mode 100644 gateways/server/kafka/validate_test.go create mode 100644 gateways/server/minio/Dockerfile rename gateways/{community/slack => server/minio}/cmd/main.go (74%) create mode 100644 gateways/server/minio/start.go rename gateways/{core/artifact => server/minio}/start_test.go (59%) create mode 100644 gateways/server/minio/validate.go create mode 100644 gateways/server/minio/validate_test.go rename gateways/{core/stream => server}/mqtt/Dockerfile (100%) rename gateways/{core/stream => server}/mqtt/cmd/main.go (76%) create mode 100644 gateways/server/mqtt/start.go create mode 100644 gateways/server/mqtt/validate.go create mode 100644 gateways/server/mqtt/validate_test.go rename gateways/{core/stream => server}/nats/Dockerfile (100%) create mode 100644 gateways/server/nats/cmd/main.go create mode 100644 gateways/server/nats/start.go create mode 100644 gateways/server/nats/validate.go create mode 100644 gateways/server/nats/validate_test.go rename gateways/{core => server}/resource/Dockerfile (100%) rename gateways/{core => server}/resource/cmd/main.go (79%) rename gateways/{core => server}/resource/start.go (62%) rename gateways/{core => server}/resource/start_test.go (72%) create mode 100644 gateways/server/resource/validate.go create mode 100644 gateways/server/resource/validate_test.go rename gateways/{gateway.go => server/server.go} (80%) rename gateways/{gateway_test.go => server/server_test.go} (93%) rename gateways/{community => server}/slack/Dockerfile (100%) create mode 100644 gateways/server/slack/cmd/main.go rename gateways/{community => server}/slack/start.go (54%) rename gateways/{community => server}/slack/start_test.go (75%) create mode 100644 gateways/server/slack/types.go create mode 100644 gateways/server/slack/validate.go create mode 100644 gateways/server/slack/validate_test.go rename gateways/{community => server}/storagegrid/Dockerfile (100%) rename gateways/{core/stream/kafka => server/storagegrid}/cmd/main.go (76%) create mode 100644 gateways/server/storagegrid/start.go rename gateways/{community => server}/storagegrid/start_test.go (61%) rename gateways/{community/storagegrid/config.go => server/storagegrid/types.go} (57%) create mode 100644 gateways/server/storagegrid/validate.go create mode 100644 gateways/server/storagegrid/validate_test.go rename gateways/{ => server}/utils.go (98%) rename gateways/{ => server}/utils_test.go (98%) rename gateways/{core => server}/webhook/Dockerfile (100%) create mode 100644 gateways/server/webhook/cmd/main.go create mode 100644 gateways/server/webhook/start.go create mode 100644 gateways/server/webhook/validate.go create mode 100644 gateways/server/webhook/validate_test.go delete mode 100644 gateways/transformer.go create mode 100644 hack/k8s/manifests/argo-events-role.yaml create mode 100644 hack/k8s/manifests/event-source-crd.yaml create mode 100644 hack/k8s/manifests/installation.yaml delete mode 100644 hack/k8s/manifests/workflow-crd.yaml create mode 100644 hack/update-api-docs.sh create mode 100644 pkg/apis/common/event-sources.go rename gateways/common/validate_test.go => pkg/apis/eventsources/register.go (68%) create mode 100644 pkg/apis/eventsources/v1alpha1/doc.go create mode 100644 pkg/apis/eventsources/v1alpha1/openapi_generated.go create mode 100644 pkg/apis/eventsources/v1alpha1/register.go create mode 100644 pkg/apis/eventsources/v1alpha1/types.go rename gateways/community/gcp-pubsub/cmd/main.go => pkg/apis/eventsources/v1alpha1/validate.go (62%) create mode 100644 pkg/apis/eventsources/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/client/eventsources/clientset/versioned/clientset.go create mode 100644 pkg/client/eventsources/clientset/versioned/doc.go create mode 100644 pkg/client/eventsources/clientset/versioned/fake/clientset_generated.go create mode 100644 pkg/client/eventsources/clientset/versioned/fake/doc.go create mode 100644 pkg/client/eventsources/clientset/versioned/fake/register.go create mode 100644 pkg/client/eventsources/clientset/versioned/scheme/doc.go create mode 100644 pkg/client/eventsources/clientset/versioned/scheme/register.go create mode 100644 pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/doc.go create mode 100644 pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsource.go create mode 100644 pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsources_client.go create mode 100644 pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/doc.go create mode 100644 pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsource.go create mode 100644 pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsources_client.go rename gateways/common/doc.go => pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/generated_expansion.go (77%) create mode 100644 pkg/client/eventsources/informers/externalversions/eventsources/interface.go create mode 100644 pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/eventsource.go create mode 100644 pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/interface.go create mode 100644 pkg/client/eventsources/informers/externalversions/factory.go create mode 100644 pkg/client/eventsources/informers/externalversions/generic.go create mode 100644 pkg/client/eventsources/informers/externalversions/internalinterfaces/factory_interfaces.go create mode 100644 pkg/client/eventsources/listers/eventsources/v1alpha1/eventsource.go rename gateways/core/stream/mqtt/config_test.go => pkg/client/eventsources/listers/eventsources/v1alpha1/expansion_generated.go (56%) delete mode 100644 sensors/event-handler_test.go delete mode 100644 sensors/signal-filter_test.go delete mode 100644 sensors/trigger-params_test.go delete mode 100644 sensors/trigger_test.go delete mode 100644 test/e2e/common/client.go delete mode 100644 test/e2e/core/main_test.go delete mode 100644 test/e2e/core/manifests/general-use-case/webhook-gateway-event-source.yaml delete mode 100644 test/e2e/core/manifests/general-use-case/webhook-gateway.yaml delete mode 100644 test/e2e/core/manifests/general-use-case/webhook-sensor.yaml diff --git a/.argo-ci/ci.yaml b/.argo-ci/ci.yaml index 1ca24a44e0..8192403390 100644 --- a/.argo-ci/ci.yaml +++ b/.argo-ci/ci.yaml @@ -6,68 +6,68 @@ spec: entrypoint: argo-events-ci arguments: parameters: - - name: revision - value: master - - name: repo - value: https://github.com/argoproj/argo-events.git + - name: revision + value: master + - name: repo + value: https://github.com/argoproj/argo-events.git templates: - - name: argo-events-ci - steps: - - - name: build - template: ci-dind - arguments: - parameters: + - name: argo-events-ci + steps: + - - name: build + template: ci-dind + arguments: + parameters: + - name: cmd + value: "{{item}}" + withItems: + - dep ensure && make + - name: test + template: ci-builder + arguments: + parameters: + - name: cmd + value: "{{item}}" + withItems: + - dep ensure && make test + - name: ci-builder + inputs: + parameters: - name: cmd - value: "{{item}}" - withItems: - - dep ensure && make - - name: test - template: ci-builder - arguments: - parameters: - - name: cmd - value: "{{item}}" - withItems: - - dep ensure && make test - - name: ci-builder - inputs: - parameters: - - name: cmd - artifacts: - - name: code - path: /go/src/github.com/argoproj/argo-events - git: - repo: "{{workflow.parameters.repo}}" - revision: "{{workflow.parameters.revision}}" - container: - image: argoproj/argo-events-ci-builder:1.0 - command: [sh, -c] - args: ["{{inputs.parameters.cmd}}"] - workingDir: /go/src/github.com/argoproj/argo-events + artifacts: + - name: code + path: /go/src/github.com/argoproj/argo-events + git: + repo: "{{workflow.parameters.repo}}" + revision: "{{workflow.parameters.revision}}" + container: + image: argoproj/argo-events-ci-builder:1.0 + command: [sh, -c] + args: ["{{inputs.parameters.cmd}}"] + workingDir: /go/src/github.com/argoproj/argo-events - - name: ci-dind - inputs: - parameters: - - name: cmd - artifacts: - - name: code - path: /go/src/github.com/argoproj/argo-events - git: - repo: "{{workflow.parameters.repo}}" - revision: "{{workflow.parameters.revision}}" - container: - image: argoproj/argo-events-ci-builder:1.0 - command: [sh, -c] - args: ["until docker ps; do sleep 3; done && {{inputs.parameters.cmd}}"] - workingDir: /go/src/github.com/argoproj/argo-events - env: - - name: DOCKER_HOST - value: 127.0.0.1 - sidecars: - - name: dind - image: docker:17.10-dind - securityContext: - privileged: true - mirrorVolumeMounts: true + - name: ci-dind + inputs: + parameters: + - name: cmd + artifacts: + - name: code + path: /go/src/github.com/argoproj/argo-events + git: + repo: "{{workflow.parameters.repo}}" + revision: "{{workflow.parameters.revision}}" + container: + image: argoproj/argo-events-ci-builder:1.0 + command: [sh, -c] + args: ["until docker ps; do sleep 3; done && {{inputs.parameters.cmd}}"] + workingDir: /go/src/github.com/argoproj/argo-events + env: + - name: DOCKER_HOST + value: 127.0.0.1 + sidecars: + - name: dind + image: docker:17.10-dind + securityContext: + privileged: true + mirrorVolumeMounts: true diff --git a/.travis.yml b/.travis.yml index 9dc6ec5edc..f71788633f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,9 +21,9 @@ before_install: - if [[ -d $HOME/docker ]]; then ls $HOME/docker/*.tar.gz | xargs -I {file} sh -c "zcat {file} | docker load"; fi - go get github.com/mattn/goveralls - if [ ! -d $HOME/bin/kubectl ]; then - mkdir -p $HOME/bin; - curl -o $HOME/bin/kubectl -L https://storage.googleapis.com/kubernetes-release/release/v1.13.4/bin/linux/amd64/kubectl; - chmod +x $HOME/bin/kubectl; + mkdir -p $HOME/bin; + curl -o $HOME/bin/kubectl -L https://storage.googleapis.com/kubernetes-release/release/v1.13.4/bin/linux/amd64/kubectl; + chmod +x $HOME/bin/kubectl; fi before_cache: diff --git a/Gopkg.lock b/Gopkg.lock index 4935d93e52..faa890f74c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,6 +3,7 @@ [[projects]] branch = "master" + digest = "1:2ff9987255c3f00d7aa928f67977b23def1bf86ab60a7c862579bf2ee29d85bb" name = "cloud.google.com/go" packages = [ ".", @@ -12,51 +13,65 @@ "internal/version", "pubsub", "pubsub/apiv1", - "pubsub/internal/distribution" + "pubsub/internal/distribution", ] - revision = "90c61cb6b2d275ce6037ff82aa8cef13996bd861" + pruneopts = "UT" + revision = "08bca813144c81c4a70e9f73ce42c98599238e54" [[projects]] + digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761" name = "github.com/BurntSushi/toml" packages = ["."] + pruneopts = "UT" revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005" version = "v0.3.1" [[projects]] branch = "master" + digest = "1:dc648facc1e7aac5086f749c84c9b9263345c08161fadd9cf92ae3309c9fcaa6" name = "github.com/Knetic/govaluate" packages = ["."] + pruneopts = "UT" revision = "9aa49832a739dcd78a5542ff189fb82c3e423116" [[projects]] + digest = "1:a2682518d905d662d984ef9959984ef87cecb777d379bfa9d9fe40e78069b3e4" name = "github.com/PuerkitoBio/purell" packages = ["."] + pruneopts = "UT" revision = "44968752391892e1b0d0b821ee79e9a85fa13049" version = "v1.1.1" [[projects]] branch = "master" + digest = "1:c739832d67eb1e9cc478a19cc1a1ccd78df0397bf8a32978b759152e205f644b" name = "github.com/PuerkitoBio/urlesc" packages = ["."] + pruneopts = "UT" revision = "de5bf2ad457846296e2031421a34e2568e304e35" [[projects]] + digest = "1:05705adc86e7ba79c48d17cfbe1760e41d227f43e2880345dd7226e0ce67e4be" name = "github.com/Shopify/sarama" packages = ["."] + pruneopts = "UT" revision = "675b0b1ff204c259877004140a540d6adf38db17" version = "v1.24.1" [[projects]] + digest = "1:252cca52c71315b507cfbad0b0d34c6a525c8c2427f0b09e0bf07baa70b39a51" name = "github.com/argoproj/argo" packages = [ "pkg/apis/workflow", - "pkg/apis/workflow/v1alpha1" + "pkg/apis/workflow/v1alpha1", ] + pruneopts = "UT" revision = "675c66267f0c916de0f233d8101aa0646acb46d4" version = "v2.4.2" [[projects]] branch = "master" + digest = "1:3dc369d4d676d019a541e5bd27829e689cd6218343843cd84fe5477eb3899ca2" name = "github.com/aws/aws-sdk-go" packages = [ "aws", @@ -92,78 +107,98 @@ "service/sns", "service/sqs", "service/sts", - "service/sts/stsiface" + "service/sts/stsiface", ] - revision = "ab596ec53119dade64e6f3cf60dab4b45e613890" + pruneopts = "UT" + revision = "36d1d7065765e55ac085734aa68be6de7c380b66" [[projects]] + digest = "1:357f4baa5f50bb2a9d9d01600c8dadebf1cb890b59b53a4c810301fc7bf3736c" name = "github.com/colinmarc/hdfs" packages = [ ".", "protocol/hadoop_common", "protocol/hadoop_hdfs", - "rpc" + "rpc", ] + pruneopts = "UT" revision = "48eb8d6c34a97ffc73b406356f0f2e1c569b42a5" [[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "UT" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" [[projects]] branch = "master" + digest = "1:ecdc8e0fe3bc7d549af1c9c36acf3820523b707d6c071b6d0c3860882c6f7b42" name = "github.com/docker/spdystream" packages = [ ".", - "spdy" + "spdy", ] + pruneopts = "UT" revision = "6480d4af844c189cf5dd913db24ddd339d3a4f85" [[projects]] + digest = "1:6f9339c912bbdda81302633ad7e99a28dfa5a639c864061f1929510a9a64aa74" name = "github.com/dustin/go-humanize" packages = ["."] + pruneopts = "UT" revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e" version = "v1.0.0" [[projects]] + digest = "1:1f0c7ab489b407a7f8f9ad16c25a504d28ab461517a971d341388a56156c1bd7" name = "github.com/eapache/go-resiliency" packages = ["breaker"] + pruneopts = "UT" revision = "5efd2ed019fd331ec2defc6f3bd98882f1e3e636" version = "v1.2.0" [[projects]] branch = "master" + digest = "1:79f16588b5576b1b3cd90e48d2374cc9a1a8776862d28d8fd0f23b0e15534967" name = "github.com/eapache/go-xerial-snappy" packages = ["."] + pruneopts = "UT" revision = "776d5712da21bc4762676d614db1d8a64f4238b0" [[projects]] + digest = "1:444b82bfe35c83bbcaf84e310fb81a1f9ece03edfed586483c869e2c046aef69" name = "github.com/eapache/queue" packages = ["."] + pruneopts = "UT" revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98" version = "v1.1.0" [[projects]] + digest = "1:bb89a2542933056fcebc2950bb15ec636e623cc43c96597288aa2009f15b0ce1" name = "github.com/eclipse/paho.mqtt.golang" packages = [ ".", - "packets" + "packets", ] + pruneopts = "UT" revision = "adca289fdcf8c883800aafa545bc263452290bae" version = "v1.2.0" [[projects]] + digest = "1:e15b0065da1011473634ffa0dda17731ea1d5f6fb8bb87905216d95fa29eaec0" name = "github.com/emicklei/go-restful" packages = [ ".", - "log" + "log", ] + pruneopts = "UT" revision = "99f05a26a0a1c71e664ebe6a76d29b2c80333056" version = "v2.11.1" [[projects]] + digest = "1:b498b36dbb2b306d1c5205ee5236c9e60352be8f9eea9bf08186723a9f75b4f3" name = "github.com/emirpasic/gods" packages = [ "containers", @@ -171,54 +206,70 @@ "lists/arraylist", "trees", "trees/binaryheap", - "utils" + "utils", ] + pruneopts = "UT" revision = "1615341f118ae12f353cc8a983f35b584342c9b3" version = "v1.12.0" [[projects]] + digest = "1:ac425d784b13d49b37a5bbed3ce022677f8f3073b216f05d6adcb9303e27fa0f" name = "github.com/evanphx/json-patch" packages = ["."] + pruneopts = "UT" revision = "026c730a0dcc5d11f93f1cf1cc65b01247ea7b6f" version = "v4.5.0" [[projects]] branch = "master" + digest = "1:925a2ad8acf10a486cdae4366eaf45847b16d6d7448e654814d8f1d51adeefe4" name = "github.com/fsnotify/fsnotify" packages = ["."] + pruneopts = "UT" revision = "4bf2d1fec78374803a39307bfb8d340688f4f28e" [[projects]] branch = "master" + digest = "1:08188cf7ce7027b22e88cc23da27f17349a0ba7746271a60cbe0a70266c2346f" name = "github.com/ghodss/yaml" packages = ["."] + pruneopts = "UT" revision = "25d852aebe32c875e9c044af3eef9c7dc6bc777f" [[projects]] + digest = "1:ed15647db08b6d63666bf9755d337725960c302bbfa5e23754b4b915a4797e42" name = "github.com/go-openapi/jsonpointer" packages = ["."] + pruneopts = "UT" revision = "ed123515f087412cd7ef02e49b0b0a5e6a79a360" version = "v0.19.3" [[projects]] + digest = "1:451fe53c19443c6941be5d4295edc973a3eb16baccb940efee94284024be03b0" name = "github.com/go-openapi/jsonreference" packages = ["."] + pruneopts = "UT" revision = "82f31475a8f7a12bc26962f6e26ceade8ea6f66a" version = "v0.19.3" [[projects]] + digest = "1:55d1c09fc8b3320b6842565249a9e4d0f363bead6f9b8be05c3b47f2c4264eda" name = "github.com/go-openapi/spec" packages = ["."] + pruneopts = "UT" revision = "8557d72e4f077c2dbe1e48df09e596b6fb9b7991" version = "v0.19.4" [[projects]] + digest = "1:43d0f99f53acce97119181dcd592321084690c2d462c57680ccb4472ae084949" name = "github.com/go-openapi/swag" packages = ["."] + pruneopts = "UT" revision = "c3d0f7896d589f3babb99eea24bbc7de98108e72" version = "v0.19.5" [[projects]] + digest = "1:e5e45557e1871c967a6ccaa5b95d1233a2c01ab00615621825d1aca7383dc022" name = "github.com/gobwas/glob" packages = [ ".", @@ -228,11 +279,13 @@ "syntax/ast", "syntax/lexer", "util/runes", - "util/strings" + "util/strings", ] + pruneopts = "UT" revision = "e7a84e9525fe90abcda167b604e483cc959ad4aa" [[projects]] + digest = "1:b51c4d0071cbb46efd912e9411ce2ec8755cb67bfa1e2a467b704c4a0469924d" name = "github.com/gogo/protobuf" packages = [ "gogoproto", @@ -262,19 +315,23 @@ "protoc-gen-gogofast", "sortkeys", "vanity", - "vanity/command" + "vanity/command", ] + pruneopts = "UT" revision = "5628607bb4c51c3157aacc3a50f0ab707582b805" version = "v1.3.1" [[projects]] branch = "master" + digest = "1:b7cb6054d3dff43b38ad2e92492f220f57ae6087ee797dca298139776749ace8" name = "github.com/golang/groupcache" packages = ["lru"] + pruneopts = "UT" revision = "611e8accdfc92c4187d399e95ce826046d4c8d73" [[projects]] branch = "master" + digest = "1:3c60e8d6869358bc8f4e9095cedd83abb34a47b81fe32a263389aba1c0e8f09f" name = "github.com/golang/protobuf" packages = [ "proto", @@ -288,183 +345,241 @@ "ptypes/any", "ptypes/duration", "ptypes/empty", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "UT" revision = "ed6926b37a637426117ccab59282c3839528a700" [[projects]] + digest = "1:e4f5819333ac698d294fe04dbf640f84719658d5c7ce195b10060cc37292ce79" name = "github.com/golang/snappy" packages = ["."] + pruneopts = "UT" revision = "2a8bb927dd31d8daada140a5d09578521ce5c36a" version = "v0.0.1" [[projects]] + digest = "1:1d1cbf539d9ac35eb3148129f96be5537f1a1330cadcc7e3a83b4e72a59672a3" name = "github.com/google/go-cmp" packages = [ "cmp", "cmp/internal/diff", "cmp/internal/flags", "cmp/internal/function", - "cmp/internal/value" + "cmp/internal/value", ] + pruneopts = "UT" revision = "2d0692c2e9617365a95b295612ac0d4415ba4627" version = "v0.3.1" [[projects]] + digest = "1:a848ff8a9a04616f385520da14d031468ad24e4a9a38f84241d92bd045593251" name = "github.com/google/go-github" packages = ["github"] + pruneopts = "UT" revision = "50be09d24ee31a2b0868265e76c24b9545a6eb7a" [[projects]] + digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690" name = "github.com/google/go-querystring" packages = ["query"] + pruneopts = "UT" revision = "44c6ddd0a2342c386950e880b658017258da92fc" version = "v1.0.0" [[projects]] + digest = "1:a6181aca1fd5e27103f9a920876f29ac72854df7345a39f3b01e61c8c94cc8af" name = "github.com/google/gofuzz" packages = ["."] + pruneopts = "UT" revision = "f140a6486e521aad38f5917de355cbf147cc0496" version = "v1.0.0" [[projects]] + digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b" name = "github.com/google/uuid" packages = ["."] + pruneopts = "UT" revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4" version = "v1.1.1" [[projects]] + digest = "1:766102087520f9d54f2acc72bd6637045900ac735b4a419b128d216f0c5c4876" name = "github.com/googleapis/gax-go" packages = ["v2"] + pruneopts = "UT" revision = "bd5b16380fd03dc758d11cef74ba2e3bc8b0e8c2" version = "v2.0.5" [[projects]] + digest = "1:ca4524b4855ded427c7003ec903a5c854f37e7b1e8e2a93277243462c5b753a8" name = "github.com/googleapis/gnostic" packages = [ "OpenAPIv2", "compiler", - "extensions" + "extensions", ] + pruneopts = "UT" revision = "ab0dd09aa10e2952b28e12ecd35681b20463ebab" version = "v0.3.1" [[projects]] branch = "master" + digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795" name = "github.com/gopherjs/gopherjs" packages = ["js"] - revision = "ce3c9ade29deed38a85f259f40e823cc17213830" + pruneopts = "UT" + revision = "d3ddacdb130fcd23f77a827e3b599804730be6b5" [[projects]] + digest = "1:cbec35fe4d5a4fba369a656a8cd65e244ea2c743007d8f6c1ccb132acf9d1296" + name = "github.com/gorilla/mux" + packages = ["."] + pruneopts = "UT" + revision = "00bdffe0f3c77e27d2cf6f5c70232a2d3e4d9c15" + version = "v1.7.3" + +[[projects]] + digest = "1:e62657cca9badaa308d86e7716083e4c5933bb78e30a17743fc67f50be26f6f4" name = "github.com/gorilla/websocket" packages = ["."] + pruneopts = "UT" revision = "c3e18be99d19e6b3e8f1559eea2c161a665c4b6b" version = "v1.4.1" [[projects]] + digest = "1:f14364057165381ea296e49f8870a9ffce2b8a95e34d6ae06c759106aaef428c" name = "github.com/hashicorp/go-uuid" packages = ["."] + pruneopts = "UT" revision = "4f571afc59f3043a65f8fe6bf46d887b10a01d43" version = "v1.0.1" [[projects]] + digest = "1:c77361e611524ec8f2ad37c408c3c916111a70b6acf806a1200855696bf8fa4d" name = "github.com/hashicorp/golang-lru" packages = [ ".", - "simplelru" + "simplelru", ] + pruneopts = "UT" revision = "7f827b33c0f158ec5dfbba01bb0b14a4541fd81d" version = "v0.5.3" [[projects]] + digest = "1:78d28d5b84a26159c67ea51996a230da4bc07cac648adaae1dfb5fc0ec8e40d3" name = "github.com/imdario/mergo" packages = ["."] + pruneopts = "UT" revision = "1afb36080aec31e0d1528973ebe6721b191b0369" version = "v0.3.8" [[projects]] branch = "master" + digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d" name = "github.com/jbenet/go-context" packages = ["io"] + pruneopts = "UT" revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4" [[projects]] + digest = "1:ae221758bdddd57f5c76f4ee5e4110af32ee62583c46299094697f8f127e63da" name = "github.com/jcmturner/gofork" packages = [ "encoding/asn1", - "x/crypto/pbkdf2" + "x/crypto/pbkdf2", ] + pruneopts = "UT" revision = "dc7c13fece037a4a36e2b3c69db4991498d30692" version = "v1.0.0" [[projects]] + digest = "1:bb81097a5b62634f3e9fec1014657855610c82d19b9a40c17612e32651e35dca" name = "github.com/jmespath/go-jmespath" packages = ["."] + pruneopts = "UT" revision = "c2b33e84" [[projects]] branch = "master" + digest = "1:3daa28dd53624e04229a3499b6bb547b4c467d488e8293b1fc9d67a922713896" name = "github.com/joncalhoun/qson" packages = ["."] + pruneopts = "UT" revision = "8a9cab3a62b1b693e7dfa590a215dc6217552803" [[projects]] + digest = "1:beb5b4f42a25056f0aa291b5eadd21e2f2903a05d15dfe7caf7eaee7e12fa972" name = "github.com/json-iterator/go" packages = ["."] + pruneopts = "UT" revision = "03217c3e97663914aec3faafde50d081f197a0a2" version = "v1.1.8" [[projects]] + digest = "1:076c531484852c227471112d49465873aaad47e5ad6e1aec3a5b092a436117ef" name = "github.com/jstemmer/go-junit-report" packages = [ ".", "formatter", - "parser" + "parser", ] + pruneopts = "UT" revision = "cc1f095d5cc5eca2844f5c5ea7bb37f6b9bf6cac" version = "v0.9.1" [[projects]] + digest = "1:4b63210654b1f2b664f74ec434a1bb1cb442b3d75742cc064a10808d1cca6361" name = "github.com/jtolds/gls" packages = ["."] + pruneopts = "UT" revision = "b4936e06046bbecbb94cae9c18127ebe510a2cb9" version = "v4.20" [[projects]] + digest = "1:fd7f169f32c221b096c74e756bda16fe22d3bb448bbf74042fd0700407a1f92f" name = "github.com/kevinburke/ssh_config" packages = ["."] + pruneopts = "UT" revision = "6cfae18c12b8934b1afba3ce8159476fdef666ba" version = "1.0" [[projects]] + digest = "1:69ababe7369aa29063b83d163bdc1b939c1480a6c0d2b44e042d016f35c6e4ad" name = "github.com/klauspost/compress" packages = [ "fse", "huff0", "snappy", "zstd", - "zstd/internal/xxhash" + "zstd/internal/xxhash", ] + pruneopts = "UT" revision = "16a4d3d7137cdefd94d420f22b5c20260674b95c" version = "v1.9.1" [[projects]] + digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de" name = "github.com/konsorten/go-windows-terminal-sequences" packages = ["."] + pruneopts = "UT" revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e" version = "v1.0.2" [[projects]] + digest = "1:927762c6729b4e72957ba3310e485ed09cf8451c5a637a52fd016a9fe09e7936" name = "github.com/mailru/easyjson" packages = [ "buffer", "jlexer", - "jwriter" + "jwriter", ] + pruneopts = "UT" revision = "1b2b06f5f209fea48ff5922d8bfb2b9ed5d8f00b" version = "v0.7.0" [[projects]] + digest = "1:88e7456f46448df99fd934c3b90621afbaa14d29172923e36c47f42de386be56" name = "github.com/minio/minio-go" packages = [ ".", @@ -472,219 +587,279 @@ "pkg/encrypt", "pkg/s3signer", "pkg/s3utils", - "pkg/set" + "pkg/set", ] + pruneopts = "UT" revision = "c6c2912aa5522e5f5a505e6cba30e95f0d8456fa" version = "v6.0.25" [[projects]] + digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79" name = "github.com/mitchellh/go-homedir" packages = ["."] + pruneopts = "UT" revision = "af06845cf3004701891bf4fdb884bfe4920b3727" version = "v1.1.0" [[projects]] + digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" name = "github.com/mitchellh/mapstructure" packages = ["."] + pruneopts = "UT" revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" version = "v1.1.2" [[projects]] + digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563" name = "github.com/modern-go/concurrent" packages = ["."] + pruneopts = "UT" revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" version = "1.0.3" [[projects]] + digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855" name = "github.com/modern-go/reflect2" packages = ["."] + pruneopts = "UT" revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" version = "1.0.1" [[projects]] + digest = "1:2ca73053216eb11c8eea2855c8099ad82773638522f91cc0542ec9759163ff3c" name = "github.com/nats-io/go-nats" packages = [ ".", "encoders/builtin", - "util" + "util", ] + pruneopts = "UT" revision = "70fe06cee50d4b6f98248d9675fb55f2a3aa7228" version = "v1.7.2" [[projects]] branch = "master" + digest = "1:7594f474ecd4b8b2e58f572fffb6db29cbfe9b000260fefff5371865a5a43a83" name = "github.com/nats-io/go-nats-streaming" packages = [ ".", - "pb" + "pb", ] + pruneopts = "UT" revision = "c4c6d40b2ba9166e155cda77274d615aed57f314" [[projects]] + digest = "1:7cd07cefacf6d1e85399a47a68b32f4b8bef8ca12705bc46efb7d45c0dccb4af" name = "github.com/nats-io/nkeys" packages = ["."] + pruneopts = "UT" revision = "abe9e4e0a640435d624e757a9110b0e59f0b6b2c" version = "v0.1.2" [[projects]] + digest = "1:599f3202ce0a754144ddc4be4c6df9c6ab27b1d722a63ede6b2e0c3a2cc338a8" name = "github.com/nats-io/nuid" packages = ["."] + pruneopts = "UT" revision = "4b96681fa6d28dd0ab5fe79bac63b3a493d9ee94" version = "v1.0.1" [[projects]] branch = "master" + digest = "1:9135761761efd675ec1bdc76b3449b1ce6865d1852a2fbf25bde19e255acac31" name = "github.com/nlopes/slack" packages = [ ".", "internal/errorsx", "internal/timex", "slackevents", - "slackutilsx" + "slackutilsx", ] - revision = "d06c2a2b3249b44a9c5dee8485f5a87497beb9ea" + pruneopts = "UT" + revision = "d20eeb27bf8f755e0c9aafa933eced3b574b2219" [[projects]] + digest = "1:cef870622e603ac1305922eb5d380455cad27e354355ae7a855d8633ffa66197" name = "github.com/pierrec/lz4" packages = [ ".", - "internal/xxh32" + "internal/xxh32", ] + pruneopts = "UT" revision = "645f9b948eee34cbcc335c70999f79c29c420fbf" version = "v2.3.0" [[projects]] + digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" name = "github.com/pkg/errors" packages = ["."] + pruneopts = "UT" revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" version = "v0.8.1" [[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "UT" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:5bbebe8ac19ecb6c87790a89faa20566e38ed0d6494a1d14c4f5b05d9ce2436c" name = "github.com/rcrowley/go-metrics" packages = ["."] + pruneopts = "UT" revision = "cac0b30c2563378d434b5af411844adff8e32960" [[projects]] + digest = "1:ed615c5430ecabbb0fb7629a182da65ecee6523900ac1ac932520860878ffcad" name = "github.com/robfig/cron" packages = ["."] + pruneopts = "UT" revision = "b41be1df696709bb6395fe435af20370037c0b4c" version = "v1.2.0" [[projects]] + digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04" name = "github.com/sergi/go-diff" packages = ["diffmatchpatch"] + pruneopts = "UT" revision = "1744e2970ca51c86172c8190fadad617561ed6e7" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:e21aee53138ca0b77d49f81f15c349b36863beb2f58d7f788136b1c75364e509" name = "github.com/sirupsen/logrus" packages = ["."] + pruneopts = "UT" revision = "67a7fdcf741f4d5cee82cb9800994ccfd4393ad0" [[projects]] + digest = "1:237af0cf68bac89e21af72e6cd6b64f388854895e75f82ad08c6c011e1a8286c" name = "github.com/smartystreets/assertions" packages = [ ".", "internal/go-diff/diffmatchpatch", "internal/go-render/render", - "internal/oglematchers" + "internal/oglematchers", ] + pruneopts = "UT" revision = "f487f9de1cd36ebab28235b9373028812fb47cbd" version = "1.10.1" [[projects]] + digest = "1:483aa658b8b58e357a07ebdfcbcb0202b46f0c0f91e9b63c8807f7d4f5cd30f9" name = "github.com/smartystreets/goconvey" packages = [ "convey", "convey/gotest", - "convey/reporting" + "convey/reporting", ] + pruneopts = "UT" revision = "505e419363375c0dc132d3ac02632a4ee32199ca" version = "v1.6.4" [[projects]] + digest = "1:524b71991fc7d9246cc7dc2d9e0886ccb97648091c63e30eef619e6862c955dd" name = "github.com/spf13/pflag" packages = ["."] + pruneopts = "UT" revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab" version = "v1.0.5" [[projects]] + digest = "1:e4ed0afd67bf7be353921665cdac50834c867ff1bba153efc0745b755a7f5905" name = "github.com/src-d/gcfg" packages = [ ".", "scanner", "token", - "types" + "types", ] + pruneopts = "UT" revision = "1ac3a1ac202429a54835fe8408a92880156b489d" version = "v1.4.0" [[projects]] branch = "master" + digest = "1:3df46e572883257c46c470dc1796f9bc609d0d0d7339dd10358030649b9beb93" name = "github.com/streadway/amqp" packages = ["."] + pruneopts = "UT" revision = "edfb9018d2714e4ec54dbaba37dbfef2bdadf0e4" [[projects]] + digest = "1:ac83cf90d08b63ad5f7e020ef480d319ae890c208f8524622a2f3136e2686b02" name = "github.com/stretchr/objx" packages = ["."] + pruneopts = "UT" revision = "477a77ecc69700c7cdeb1fa9e129548e1c1c393c" version = "v0.1.1" [[projects]] + digest = "1:ad527ce5c6b2426790449db7663fe53f8bb647f9387295406794c8be001238da" name = "github.com/stretchr/testify" packages = [ "assert", - "mock" + "mock", ] + pruneopts = "UT" revision = "221dbe5ed46703ee255b1da0dec05086f5035f62" version = "v1.4.0" [[projects]] + digest = "1:5a68167017eaa32aa408397806b9d69815244238ed774439a8863ef4bc329eeb" name = "github.com/tidwall/gjson" packages = ["."] + pruneopts = "UT" revision = "c34bf81952c067718854115564f8e55978be5e1d" version = "v1.3.4" [[projects]] + digest = "1:8453ddbed197809ee8ca28b06bd04e127bec9912deb4ba451fea7a1eca578328" name = "github.com/tidwall/match" packages = ["."] + pruneopts = "UT" revision = "33827db735fff6510490d69a8622612558a557ed" version = "v1.0.1" [[projects]] + digest = "1:ddfe0a54e5f9b29536a6d7b2defa376f2cb2b6e4234d676d7ff214d5b097cb50" name = "github.com/tidwall/pretty" packages = ["."] + pruneopts = "UT" revision = "1166b9ac2b65e46a43d8618d30d1554f4652d49b" version = "v1.0.0" [[projects]] + digest = "1:b70c951ba6fdeecfbd50dabe95aa5e1b973866ae9abbece46ad60348112214f2" name = "github.com/tidwall/sjson" packages = ["."] + pruneopts = "UT" revision = "25fb082a20e29e83fb7b7ef5f5919166aad1f084" version = "v1.0.4" [[projects]] + digest = "1:766db8705204fd893db77ff5fde228362fbceac616b87ccb9976518095aac8ce" name = "github.com/xanzy/go-gitlab" packages = ["."] - revision = "457d4d018eaa1fad8e6c63502cebcd11ba60164e" - version = "v0.22.0" + pruneopts = "UT" + revision = "87a6b9db49fa4bd6efeaeec450b0c5661f94fcb5" + version = "v0.21.0" [[projects]] + digest = "1:172f94a6b3644a8f9e6b5e5b7fc9fe1e42d424f52a0300b2e7ab1e57db73f85d" name = "github.com/xanzy/ssh-agent" packages = ["."] + pruneopts = "UT" revision = "6a3e2ff9e7c564f36873c2e36413f634534f1c44" version = "v0.2.1" [[projects]] + digest = "1:07ca513e3b295b9aeb1f0f6fbefb8102139a2764ce0f28d02040c0eb2dc276dd" name = "go.opencensus.io" packages = [ ".", @@ -703,13 +878,15 @@ "trace", "trace/internal", "trace/propagation", - "trace/tracestate" + "trace/tracestate", ] + pruneopts = "UT" revision = "59d1ce35d30f3c25ba762169da2a37eab6ffa041" version = "v0.22.1" [[projects]] branch = "master" + digest = "1:35041d2389057ddabc32b374768b133c795fa7f6eb9bcd80ac9bd820d86ea8a4" name = "golang.org/x/crypto" packages = [ "argon2", @@ -732,30 +909,36 @@ "ssh", "ssh/agent", "ssh/knownhosts", - "ssh/terminal" + "ssh/terminal", ] - revision = "f4817d981bb690635456c5c1c6aa0585e5d45891" + pruneopts = "UT" + revision = "c7e5f84aec591254278750bee18f39e5dd19cdb5" [[projects]] branch = "master" + digest = "1:b1444bc98b5838c3116ed23e231fee4fa8509f975abd96e5d9e67e572dd01604" name = "golang.org/x/exp" packages = [ "apidiff", - "cmd/apidiff" + "cmd/apidiff", ] + pruneopts = "UT" revision = "a1ab85dbe136a36c66fbea07de5e3d62a0ce60ad" [[projects]] branch = "master" + digest = "1:21d7bad9b7da270fd2d50aba8971a041bd691165c95096a2a4c68db823cbc86a" name = "golang.org/x/lint" packages = [ ".", - "golint" + "golint", ] + pruneopts = "UT" revision = "16217165b5de779cb6a5e4fc81fa9c1166fda457" [[projects]] branch = "master" + digest = "1:c01950158c9535178651a56e3a82b2088560555e9f69b1c9a0f79afbc8619c43" name = "golang.org/x/net" packages = [ "context", @@ -769,42 +952,50 @@ "proxy", "publicsuffix", "trace", - "websocket" + "websocket", ] - revision = "7e6e90b9ea8824b29cbeee76d03ef838c9187418" + pruneopts = "UT" + revision = "a882066a44e04a21d46e51451c71e131344e830e" [[projects]] branch = "master" + digest = "1:31e33f76456ccf54819ab4a646cf01271d1a99d7712ab84bf1a9e7b61cd2031b" name = "golang.org/x/oauth2" packages = [ ".", "google", "internal", "jws", - "jwt" + "jwt", ] + pruneopts = "UT" revision = "0f29369cfe4552d0e4bcddc57cc75f4d7e672a33" [[projects]] branch = "master" + digest = "1:a2fc247e64b5dafd3251f12d396ec85f163d5bb38763c4997856addddf6e78d8" name = "golang.org/x/sync" packages = [ "errgroup", - "semaphore" + "semaphore", ] + pruneopts = "UT" revision = "cd5d95a43a6e21273425c7ae415d3df9ea832eeb" [[projects]] branch = "master" + digest = "1:92c957c2713e817f2c5e25735152837312e3e0458e784a5e168d725474ac8995" name = "golang.org/x/sys" packages = [ "cpu", "unix", - "windows" + "windows", ] + pruneopts = "UT" revision = "c1f44814a5cd81a6d1cb589ef1e528bc5d305e07" [[projects]] + digest = "1:66a2f252a58b4fbbad0e4e180e1d85a83c222b6bce09c3dcdef3dc87c72eda7c" name = "golang.org/x/text" packages = [ "collate", @@ -823,19 +1014,23 @@ "unicode/cldr", "unicode/norm", "unicode/rangetable", - "width" + "width", ] + pruneopts = "UT" revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475" version = "v0.3.2" [[projects]] branch = "master" + digest = "1:a2f668c709f9078828e99cb1768cb02e876cb81030545046a32b54b2ac2a9ea8" name = "golang.org/x/time" packages = ["rate"] + pruneopts = "UT" revision = "555d28b269f0569763d25dbe1a237ae74c6bcc82" [[projects]] branch = "master" + digest = "1:403cd4290937cc1ce67cd607806ee217720305519109b6c49d88a44144c1e100" name = "golang.org/x/tools" packages = [ "cmd/goimports", @@ -856,11 +1051,13 @@ "internal/imports", "internal/module", "internal/semver", - "internal/span" + "internal/span", ] - revision = "f7ea15e60b12ba031eaab7e3247e845e5c7eba73" + pruneopts = "UT" + revision = "689d0f08e67ae0c77c260e137ac6a3729498c92f" [[projects]] + digest = "1:526726f1b56fe59206fd9ade203359f20d0958c2feaa6c263f677cf655944c92" name = "google.golang.org/api" packages = [ "googleapi/transport", @@ -871,12 +1068,14 @@ "transport", "transport/grpc", "transport/http", - "transport/http/internal/propagation" + "transport/http/internal/propagation", ] + pruneopts = "UT" revision = "4f42dad4690a01d7f6fa461106c63889ff1be027" version = "v0.13.0" [[projects]] + digest = "1:c98e9b93e6d178378530b920fe6e1aa4b3dd4972872111e83827746aa1f33ded" name = "google.golang.org/appengine" packages = [ ".", @@ -890,13 +1089,15 @@ "internal/socket", "internal/urlfetch", "socket", - "urlfetch" + "urlfetch", ] + pruneopts = "UT" revision = "971852bfffca25b069c31162ae8f247a3dba083b" version = "v1.6.5" [[projects]] branch = "master" + digest = "1:fb32dd440d7b296fd516c15af931e13cab6b0ce746f1b077c5d79825bbde0037" name = "google.golang.org/genproto" packages = [ "googleapis/api/annotations", @@ -904,11 +1105,13 @@ "googleapis/pubsub/v1", "googleapis/rpc/status", "googleapis/type/expr", - "protobuf/field_mask" + "protobuf/field_mask", ] + pruneopts = "UT" revision = "919d9bdd9fe6f1a5dd95ce5d5e4cdb8fd3c516d0" [[projects]] + digest = "1:56380851a4d733663a711bd5ad5917ea5cb825661364aa13397c6af2061be8bc" name = "google.golang.org/grpc" packages = [ ".", @@ -956,36 +1159,46 @@ "serviceconfig", "stats", "status", - "tap" + "tap", ] + pruneopts = "UT" revision = "9d331e2b02dd47daeecae02790f61cc88dc75a64" version = "v1.25.0" [[projects]] + digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a" name = "gopkg.in/inf.v0" packages = ["."] + pruneopts = "UT" revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" version = "v0.9.1" [[projects]] + digest = "1:2a2e303bb32696f9250d4ba3d8c25c7849a6bff66a13a9f9caf4f34774b5ee51" name = "gopkg.in/ini.v1" packages = ["."] - revision = "8ee0b789944d2b0a7297c671009b3e7bc977376f" - version = "v1.50.0" + pruneopts = "UT" + revision = "1eb383f13cde0e7be091181a93b58574638129f0" + version = "v1.49.0" [[projects]] + digest = "1:c902038ee2d6f964d3b9f2c718126571410c5d81251cbab9fe58abd37803513c" name = "gopkg.in/jcmturner/aescts.v1" packages = ["."] + pruneopts = "UT" revision = "f6abebb3171c4c1b1fea279cb7c7325020a26290" version = "v1.0.1" [[projects]] + digest = "1:a1a3e185c03d79a7452d5d5b4c91be4cc433f55e6ed3a35233d852c966e39013" name = "gopkg.in/jcmturner/dnsutils.v1" packages = ["."] + pruneopts = "UT" revision = "13eeb8d49ffb74d7a75784c35e4d900607a3943c" version = "v1.0.1" [[projects]] + digest = "1:653c1ef9be253f28c38612cc0fb0571dd440a3d61a97f82e6205d53942a7b4a9" name = "gopkg.in/jcmturner/gokrb5.v5" packages = [ "asn1tools", @@ -1018,12 +1231,14 @@ "messages", "mstypes", "pac", - "types" + "types", ] + pruneopts = "UT" revision = "32ba44ca5b42f17a4a9f33ff4305e70665a1bc0f" version = "v5.3.0" [[projects]] + digest = "1:dc01a587d07be012625ba63df6d4224ae6d7a83e79bfebde6d987c10538d66dd" name = "gopkg.in/jcmturner/gokrb5.v7" packages = [ "asn1tools", @@ -1055,39 +1270,47 @@ "krberror", "messages", "pac", - "types" + "types", ] + pruneopts = "UT" revision = "363118e62befa8a14ff01031c025026077fe5d6d" version = "v7.3.0" [[projects]] + digest = "1:917e312d1c83bac01db5771433a141f7e4754df0ebe83d2e8edc821320aff849" name = "gopkg.in/jcmturner/rpc.v0" packages = ["ndr"] + pruneopts = "UT" revision = "4480c480c9cd343b54b0acb5b62261cbd33d7adf" version = "v0.0.2" [[projects]] + digest = "1:0f16d9c577198e3b8d3209f5a89aabe679525b2aba2a7548714e973035c0e232" name = "gopkg.in/jcmturner/rpc.v1" packages = [ "mstypes", - "ndr" + "ndr", ] + pruneopts = "UT" revision = "99a8ce2fbf8b8087b6ed12a37c61b10f04070043" version = "v1.1.0" [[projects]] + digest = "1:eb27cfcaf8d7e4155224dd0a209f1d0ab19784fef01be142638b78b7b6becd6b" name = "gopkg.in/src-d/go-billy.v4" packages = [ ".", "helper/chroot", "helper/polyfill", "osfs", - "util" + "util", ] + pruneopts = "UT" revision = "780403cfc1bc95ff4d07e7b26db40a6186c5326e" version = "v4.3.2" [[projects]] + digest = "1:b2ad0a18676cd4d5b4b180709c1ea34dbabd74b3d7db0cc01e6d287d5f1e3a99" name = "gopkg.in/src-d/go-git.v4" packages = [ ".", @@ -1130,24 +1353,30 @@ "utils/merkletrie/filesystem", "utils/merkletrie/index", "utils/merkletrie/internal/frame", - "utils/merkletrie/noder" + "utils/merkletrie/noder", ] + pruneopts = "UT" revision = "0d1a009cbb604db18be960db5f1525b99a55d727" version = "v4.13.1" [[projects]] + digest = "1:78d374b493e747afa9fbb2119687e3740a7fb8d0ebabddfef0a012593aaecbb3" name = "gopkg.in/warnings.v0" packages = ["."] + pruneopts = "UT" revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b" version = "v0.1.2" [[projects]] + digest = "1:f26a5d382387e03a40d1471dddfba85dfff9bf05352d7e42d37612677c4d3c5c" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "UT" revision = "f90ceb4f409096b60e2e9076b38b304b8246e5fa" version = "v2.2.5" [[projects]] + digest = "1:131158a88aad1f94854d0aa21a64af2802d0a470fb0f01cb33c04fafd2047111" name = "honnef.co/go/tools" packages = [ "arg", @@ -1174,13 +1403,15 @@ "staticcheck/vrp", "stylecheck", "unused", - "version" + "version", ] + pruneopts = "UT" revision = "afd67930eec2a9ed3e9b19f684d17a062285f16a" version = "2019.2.3" [[projects]] branch = "release-1.15" + digest = "1:d89afbf3588e87d2c9e6efdd5528d249b32d23a12fbd7ec324f3cb373c6fb76c" name = "k8s.io/api" packages = [ "admissionregistration/v1beta1", @@ -1218,12 +1449,14 @@ "settings/v1alpha1", "storage/v1", "storage/v1alpha1", - "storage/v1beta1" + "storage/v1beta1", ] + pruneopts = "UT" revision = "3a12735a829ac2f7817379647da6d47c39327512" [[projects]] branch = "release-1.15" + digest = "1:b2ba899970541943ec0a2f1bdef707b029d8591236fb7dab27b222f84037d8ef" name = "k8s.io/apimachinery" packages = [ "pkg/api/errors", @@ -1270,12 +1503,13 @@ "pkg/watch", "third_party/forked/golang/json", "third_party/forked/golang/netutil", - "third_party/forked/golang/reflect" + "third_party/forked/golang/reflect", ] + pruneopts = "UT" revision = "31ade1b30762be61c32b2e8db2a11aa8b0b8960e" [[projects]] - branch = "release-12.0" + digest = "1:25e5278fc841f46e9b465a8c67f3304b32ac85ced3b7057438318db79f71c1ed" name = "k8s.io/client-go" packages = [ "discovery", @@ -1468,12 +1702,15 @@ "util/homedir", "util/keyutil", "util/retry", - "util/workqueue" + "util/workqueue", ] - revision = "5f2132fc4383659da452dba21a1c6c9890b0e062" + pruneopts = "UT" + revision = "78d2af792babf2dd937ba2e2a8d99c753a5eda89" + version = "v12.0.0" [[projects]] branch = "release-1.15" + digest = "1:f41480fd8c5f54b13326ee0f2ee398d5734789b790dbc4db57f9b08a0d63139a" name = "k8s.io/code-generator" packages = [ "cmd/client-gen", @@ -1485,11 +1722,13 @@ "cmd/client-gen/path", "cmd/client-gen/types", "pkg/namer", - "pkg/util" + "pkg/util", ] + pruneopts = "T" revision = "18da4a14b22b17d2fa761e50037fabfbacec225b" [[projects]] + digest = "1:a9f99f1c11620be972d49d2e4e296031a5fbc168ada49a9e618d9b35f751f119" name = "k8s.io/gengo" packages = [ "args", @@ -1499,18 +1738,22 @@ "generator", "namer", "parser", - "types" + "types", ] + pruneopts = "T" revision = "b90029ef6cd877cb3f422d75b3a07707e3aac6b7" [[projects]] + digest = "1:93e82f25d75aba18436ad1ac042cb49493f096011f2541075721ed6f9e05c044" name = "k8s.io/klog" packages = ["."] + pruneopts = "UT" revision = "2ca9ad30301bf30a8a6e0fa2110db6b8df699a91" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:94ad85c6f3cfad58b9f602b9a2d4af530e8db8f861bca708c4dd236f771ea1a4" name = "k8s.io/kube-openapi" packages = [ "cmd/openapi-gen", @@ -1519,29 +1762,128 @@ "pkg/generators", "pkg/generators/rules", "pkg/util/proto", - "pkg/util/sets" + "pkg/util/sets", ] - revision = "30be4d16710ac61bce31eb28a01054596fe6a9f1" + pruneopts = "UT" + revision = "0270cf2f1c1d995d34b36019a6f65d58e6e33ad4" [[projects]] branch = "master" + digest = "1:2d3f59daa4b479ff4e100a2e1d8fea6780040fdadc177869531fe4cc29407f55" name = "k8s.io/utils" packages = [ "buffer", "integer", - "trace" + "trace", ] + pruneopts = "UT" revision = "2b95a09bc58df43d4032504619706b6a38293a47" [[projects]] + digest = "1:7719608fe0b52a4ece56c2dde37bedd95b938677d1ab0f84b8a7852e4c59f849" name = "sigs.k8s.io/yaml" packages = ["."] + pruneopts = "UT" revision = "fd68e9863619f6ec2fdd8625fe1f02e7c877e480" version = "v1.1.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "df67feb7bfc29e71c524d42fa1f6878e4b4ffbe085ece6cf09c30a8875269e0f" + input-imports = [ + "cloud.google.com/go/pubsub", + "github.com/Knetic/govaluate", + "github.com/Shopify/sarama", + "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1", + "github.com/aws/aws-sdk-go/aws", + "github.com/aws/aws-sdk-go/aws/credentials", + "github.com/aws/aws-sdk-go/aws/session", + "github.com/aws/aws-sdk-go/service/sns", + "github.com/aws/aws-sdk-go/service/sqs", + "github.com/colinmarc/hdfs", + "github.com/eclipse/paho.mqtt.golang", + "github.com/fsnotify/fsnotify", + "github.com/ghodss/yaml", + "github.com/go-openapi/spec", + "github.com/gobwas/glob", + "github.com/gogo/protobuf/protoc-gen-gofast", + "github.com/gogo/protobuf/protoc-gen-gogofast", + "github.com/golang/protobuf/proto", + "github.com/golang/protobuf/protoc-gen-go", + "github.com/google/go-github/github", + "github.com/google/uuid", + "github.com/gorilla/mux", + "github.com/joncalhoun/qson", + "github.com/minio/minio-go", + "github.com/mitchellh/mapstructure", + "github.com/nats-io/go-nats", + "github.com/nats-io/go-nats-streaming", + "github.com/nlopes/slack", + "github.com/nlopes/slack/slackevents", + "github.com/pkg/errors", + "github.com/robfig/cron", + "github.com/sirupsen/logrus", + "github.com/smartystreets/goconvey/convey", + "github.com/streadway/amqp", + "github.com/stretchr/testify/assert", + "github.com/stretchr/testify/mock", + "github.com/tidwall/gjson", + "github.com/tidwall/sjson", + "github.com/xanzy/go-gitlab", + "golang.org/x/crypto/ssh", + "google.golang.org/api/option", + "google.golang.org/grpc", + "google.golang.org/grpc/codes", + "google.golang.org/grpc/connectivity", + "google.golang.org/grpc/metadata", + "google.golang.org/grpc/status", + "gopkg.in/jcmturner/gokrb5.v5/client", + "gopkg.in/jcmturner/gokrb5.v5/config", + "gopkg.in/jcmturner/gokrb5.v5/credentials", + "gopkg.in/jcmturner/gokrb5.v5/keytab", + "gopkg.in/src-d/go-git.v4", + "gopkg.in/src-d/go-git.v4/config", + "gopkg.in/src-d/go-git.v4/plumbing", + "gopkg.in/src-d/go-git.v4/plumbing/transport", + "gopkg.in/src-d/go-git.v4/plumbing/transport/http", + "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh", + "k8s.io/api/apps/v1", + "k8s.io/api/core/v1", + "k8s.io/apimachinery/pkg/api/errors", + "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured", + "k8s.io/apimachinery/pkg/fields", + "k8s.io/apimachinery/pkg/labels", + "k8s.io/apimachinery/pkg/runtime", + "k8s.io/apimachinery/pkg/runtime/schema", + "k8s.io/apimachinery/pkg/runtime/serializer", + "k8s.io/apimachinery/pkg/selection", + "k8s.io/apimachinery/pkg/types", + "k8s.io/apimachinery/pkg/util/intstr", + "k8s.io/apimachinery/pkg/util/runtime", + "k8s.io/apimachinery/pkg/util/wait", + "k8s.io/apimachinery/pkg/watch", + "k8s.io/client-go/discovery", + "k8s.io/client-go/discovery/fake", + "k8s.io/client-go/dynamic", + "k8s.io/client-go/dynamic/dynamicinformer", + "k8s.io/client-go/dynamic/fake", + "k8s.io/client-go/informers/core/v1", + "k8s.io/client-go/kubernetes", + "k8s.io/client-go/kubernetes/fake", + "k8s.io/client-go/kubernetes/scheme", + "k8s.io/client-go/rest", + "k8s.io/client-go/testing", + "k8s.io/client-go/tools/cache", + "k8s.io/client-go/tools/clientcmd", + "k8s.io/client-go/tools/portforward", + "k8s.io/client-go/transport/spdy", + "k8s.io/client-go/util/flowcontrol", + "k8s.io/client-go/util/workqueue", + "k8s.io/code-generator/cmd/client-gen", + "k8s.io/gengo/examples/deepcopy-gen", + "k8s.io/kube-openapi/cmd/openapi-gen", + "k8s.io/kube-openapi/pkg/common", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 3b77506fe9..5a5f52b008 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -92,6 +92,10 @@ required = [ name = "github.com/Knetic/govaluate" branch = "master" +[[constraint]] + name = "github.com/gorilla/mux" + version = "v1.7.3" + [[constraint]] name = "github.com/colinmarc/hdfs" revision = "48eb8d6c34a97ffc73b406356f0f2e1c569b42a5" @@ -109,7 +113,7 @@ required = [ name = "k8s.io/apimachinery" [[override]] - branch = "release-12.0" + version = "v12.0.0" name = "k8s.io/client-go" [prune] diff --git a/Makefile b/Makefile index d272d50b30..02583d9876 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ override LDFLAGS += \ # docker image publishing options DOCKER_PUSH?=true IMAGE_NAMESPACE?=argoproj -IMAGE_TAG?=v0.11 +IMAGE_TAG?=v0.12-rc ifeq (${DOCKER_PUSH},true) ifndef IMAGE_NAMESPACE @@ -35,13 +35,13 @@ endif # Build the project images .DELETE_ON_ERROR: -all: sensor-linux sensor-controller-linux gateway-controller-linux gateway-client-linux webhook-linux calendar-linux resource-linux artifact-linux file-linux nats-linux kafka-linux amqp-linux mqtt-linux storage-grid-linux github-linux hdfs-linux gitlab-linux sns-linux sqs-linux pubsub-linux slack-linux +all: sensor-linux sensor-controller-linux gateway-controller-linux gateway-client-linux webhook-linux calendar-linux resource-linux minio-linux file-linux nats-linux kafka-linux amqp-linux mqtt-linux storage-grid-linux github-linux hdfs-linux gitlab-linux sns-linux sqs-linux pubsub-linux slack-linux -all-images: sensor-image sensor-controller-image gateway-controller-image gateway-client-image webhook-image calendar-image resource-image artifact-image file-image nats-image kafka-image amqp-image mqtt-image storage-grid-image github-image gitlab-image sns-image pubsub-image hdfs-image sqs-image slack-image +all-images: sensor-image sensor-controller-image gateway-controller-image gateway-client-image webhook-image calendar-image resource-image minio-image file-image nats-image kafka-image amqp-image mqtt-image storage-grid-image github-image gitlab-image sns-image pubsub-image hdfs-image sqs-image slack-image all-controller-images: sensor-controller-image gateway-controller-image -all-core-gateway-images: webhook-image calendar-image artifact-image file-image nats-image kafka-image amqp-image mqtt-image resource-image +all-core-gateway-images: webhook-image calendar-image minio-image file-image nats-image kafka-image amqp-image mqtt-image resource-image .PHONY: all clean test @@ -58,7 +58,7 @@ sensor-image: sensor-linux # Sensor controller sensor-controller: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/sensor-controller ./cmd/controllers/sensor + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/sensor-controller ./controllers/sensor/cmd sensor-controller-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make sensor-controller @@ -69,7 +69,7 @@ sensor-controller-image: sensor-controller-linux # Gateway controller gateway-controller: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gateway-controller ./cmd/controllers/gateway/main.go + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gateway-controller ./controllers/gateway/cmd gateway-controller-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make gateway-controller @@ -81,196 +81,196 @@ gateway-controller-image: gateway-controller-linux # Gateway client binary gateway-client: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gateway-client ./gateways/cmd/main.go + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gateway-client ./gateways/client gateway-client-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make gateway-client gateway-client-image: gateway-client-linux - docker build -t $(IMAGE_PREFIX)gateway-client:$(IMAGE_TAG) -f ./gateways/Dockerfile . + docker build -t $(IMAGE_PREFIX)gateway-client:$(IMAGE_TAG) -f ./gateways/client/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)gateway-client:$(IMAGE_TAG) ; fi # gateway binaries webhook: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/webhook-gateway ./gateways/core/webhook/cmd/ + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/webhook-gateway ./gateways/server/webhook/cmd/ webhook-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make webhook webhook-image: webhook-linux - docker build -t $(IMAGE_PREFIX)webhook-gateway:$(IMAGE_TAG) -f ./gateways/core/webhook/Dockerfile . + docker build -t $(IMAGE_PREFIX)webhook-gateway:$(IMAGE_TAG) -f ./gateways/server/webhook/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)webhook-gateway:$(IMAGE_TAG) ; fi calendar: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/calendar-gateway ./gateways/core/calendar/cmd/ + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/calendar-gateway ./gateways/server/calendar/cmd/ calendar-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make calendar calendar-image: calendar-linux - docker build -t $(IMAGE_PREFIX)calendar-gateway:$(IMAGE_TAG) -f ./gateways/core/calendar/Dockerfile . + docker build -t $(IMAGE_PREFIX)calendar-gateway:$(IMAGE_TAG) -f ./gateways/server/calendar/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)calendar-gateway:$(IMAGE_TAG) ; fi resource: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/resource-gateway ./gateways/core/resource/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/resource-gateway ./gateways/server/resource/cmd resource-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make resource resource-image: resource-linux - docker build -t $(IMAGE_PREFIX)resource-gateway:$(IMAGE_TAG) -f ./gateways/core/resource/Dockerfile . + docker build -t $(IMAGE_PREFIX)resource-gateway:$(IMAGE_TAG) -f ./gateways/server/resource/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)resource-gateway:$(IMAGE_TAG) ; fi -artifact: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/artifact-gateway ./gateways/core/artifact/cmd +minio: + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/minio-gateway ./gateways/server/minio/cmd -artifact-linux: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make artifact +minio-linux: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make minio -artifact-image: artifact-linux - docker build -t $(IMAGE_PREFIX)artifact-gateway:$(IMAGE_TAG) -f ./gateways/core/artifact/Dockerfile . - @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)artifact-gateway:$(IMAGE_TAG) ; fi +minio-image: minio-linux + docker build -t $(IMAGE_PREFIX)minio-gateway:$(IMAGE_TAG) -f ./gateways/server/minio/Dockerfile . + @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)minio-gateway:$(IMAGE_TAG) ; fi file: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/file-gateway ./gateways/core/file/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/file-gateway ./gateways/server/file/cmd file-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make file file-image: file-linux - docker build -t $(IMAGE_PREFIX)file-gateway:$(IMAGE_TAG) -f ./gateways/core/file/Dockerfile . + docker build -t $(IMAGE_PREFIX)file-gateway:$(IMAGE_TAG) -f ./gateways/server/file/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)file-gateway:$(IMAGE_TAG) ; fi #Stream gateways nats: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/nats-gateway ./gateways/core/stream/nats/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/nats-gateway ./gateways/server/nats/cmd nats-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make nats nats-image: nats-linux - docker build -t $(IMAGE_PREFIX)nats-gateway:$(IMAGE_TAG) -f ./gateways/core/stream/nats/Dockerfile . + docker build -t $(IMAGE_PREFIX)nats-gateway:$(IMAGE_TAG) -f ./gateways/server/nats/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)nats-gateway:$(IMAGE_TAG) ; fi kafka: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/kafka-gateway ./gateways/core/stream/kafka/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/kafka-gateway ./gateways/server/kafka/cmd kafka-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make kafka kafka-image: kafka-linux - docker build -t $(IMAGE_PREFIX)kafka-gateway:$(IMAGE_TAG) -f ./gateways/core/stream/kafka/Dockerfile . + docker build -t $(IMAGE_PREFIX)kafka-gateway:$(IMAGE_TAG) -f ./gateways/server/kafka/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)kafka-gateway:$(IMAGE_TAG) ; fi amqp: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/amqp-gateway ./gateways/core/stream/amqp/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/amqp-gateway ./gateways/server/amqp/cmd amqp-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make amqp amqp-image: amqp-linux - docker build -t $(IMAGE_PREFIX)amqp-gateway:$(IMAGE_TAG) -f ./gateways/core/stream/amqp/Dockerfile . + docker build -t $(IMAGE_PREFIX)amqp-gateway:$(IMAGE_TAG) -f ./gateways/server/amqp/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)amqp-gateway:$(IMAGE_TAG) ; fi mqtt: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/mqtt-gateway ./gateways/core/stream/mqtt/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/mqtt-gateway ./gateways/server/mqtt/cmd mqtt-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make mqtt mqtt-image: mqtt-linux - docker build -t $(IMAGE_PREFIX)mqtt-gateway:$(IMAGE_TAG) -f ./gateways/core/stream/mqtt/Dockerfile . + docker build -t $(IMAGE_PREFIX)mqtt-gateway:$(IMAGE_TAG) -f ./gateways/server/mqtt/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)mqtt-gateway:$(IMAGE_TAG) ; fi # Custom gateways storage-grid: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/storagegrid-gateway ./gateways/community/storagegrid/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/storagegrid-gateway ./gateways/server/storagegrid/cmd storage-grid-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make storage-grid storage-grid-image: storage-grid-linux - docker build -t $(IMAGE_PREFIX)storage-grid-gateway:$(IMAGE_TAG) -f ./gateways/community/storagegrid/Dockerfile . + docker build -t $(IMAGE_PREFIX)storage-grid-gateway:$(IMAGE_TAG) -f ./gateways/server/storagegrid/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)storage-grid-gateway:$(IMAGE_TAG) ; fi gitlab: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gitlab-gateway ./gateways/community/gitlab/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gitlab-gateway ./gateways/server/gitlab/cmd gitlab-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make gitlab gitlab-image: gitlab-linux - docker build -t $(IMAGE_PREFIX)gitlab-gateway:$(IMAGE_TAG) -f ./gateways/community/gitlab/Dockerfile . + docker build -t $(IMAGE_PREFIX)gitlab-gateway:$(IMAGE_TAG) -f ./gateways/server/gitlab/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)gitlab-gateway:$(IMAGE_TAG) ; fi github: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/github-gateway ./gateways/community/github/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/github-gateway ./gateways/server/github/cmd github-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make github github-image: github-linux - docker build -t $(IMAGE_PREFIX)github-gateway:$(IMAGE_TAG) -f ./gateways/community/github/Dockerfile . + docker build -t $(IMAGE_PREFIX)github-gateway:$(IMAGE_TAG) -f ./gateways/server/github/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)github-gateway:$(IMAGE_TAG) ; fi sns: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/aws-sns-gateway ./gateways/community/aws-sns/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/aws-sns-gateway ./gateways/server/aws-sns/cmd sns-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make sns sns-image: - docker build -t $(IMAGE_PREFIX)aws-sns-gateway:$(IMAGE_TAG) -f ./gateways/community/aws-sns/Dockerfile . + docker build -t $(IMAGE_PREFIX)aws-sns-gateway:$(IMAGE_TAG) -f ./gateways/server/aws-sns/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)aws-sns-gateway:$(IMAGE_TAG) ; fi pubsub: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gcp-pubsub-gateway ./gateways/community/gcp-pubsub/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/gcp-pubsub-gateway ./gateways/server/gcp-pubsub/cmd pubsub-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make pubsub pubsub-image: pubsub-linux - docker build -t $(IMAGE_PREFIX)gcp-pubsub-gateway:$(IMAGE_TAG) -f ./gateways/community/gcp-pubsub/Dockerfile . + docker build -t $(IMAGE_PREFIX)gcp-pubsub-gateway:$(IMAGE_TAG) -f ./gateways/server/gcp-pubsub/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)gcp-pubsub-gateway:$(IMAGE_TAG) ; fi hdfs: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/hdfs-gateway ./gateways/community/hdfs/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/hdfs-gateway ./gateways/server/hdfs/cmd hdfs-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make hdfs hdfs-image: hdfs-linux - docker build -t $(IMAGE_PREFIX)hdfs-gateway:$(IMAGE_TAG) -f ./gateways/community/hdfs/Dockerfile . + docker build -t $(IMAGE_PREFIX)hdfs-gateway:$(IMAGE_TAG) -f ./gateways/server/hdfs/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)hdfs-gateway:$(IMAGE_TAG) ; fi sqs: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/aws-sqs-gateway ./gateways/community/aws-sqs/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/aws-sqs-gateway ./gateways/server/aws-sqs/cmd sqs-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make sqs sqs-image: sqs-linux - docker build -t $(IMAGE_PREFIX)aws-sqs-gateway:$(IMAGE_TAG) -f ./gateways/community/aws-sqs/Dockerfile . + docker build -t $(IMAGE_PREFIX)aws-sqs-gateway:$(IMAGE_TAG) -f ./gateways/server/aws-sqs/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)aws-sqs-gateway:$(IMAGE_TAG) ; fi slack: - go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/slack-gateway ./gateways/community/slack/cmd + go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/slack-gateway ./gateways/server/slack/cmd slack-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make slack slack-image: slack-linux - docker build -t $(IMAGE_PREFIX)slack-gateway:$(IMAGE_TAG) -f ./gateways/community/slack/Dockerfile . + docker build -t $(IMAGE_PREFIX)slack-gateway:$(IMAGE_TAG) -f ./gateways/server/slack/Dockerfile . @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)slack-gateway:$(IMAGE_TAG) ; fi test: diff --git a/README.md b/README.md index 6a1dacdc19..12e0b386b4 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,40 @@ and trigger Kubernetes objects after successful event dependencies resolution. * Supports [CloudEvents](https://cloudevents.io/) for describing event data and transmission. * Ability to manage event sources at runtime. -## Documentation -To learn more about Argo Events, [go to complete documentation](https://argoproj.github.io/argo-events/) +## Getting Started +Follow [setup](https://argoproj.github.io/argo-events/installation/) instructions for installation. To see the Argo-Events in action, follow the +[getting started](https://argoproj.github.io/argo-events/getting_started/) guide. +Complete documentation is available at https://argoproj.github.io/argo-events/ + +[![asciicast](https://asciinema.org/a/AKkYwzEakSUsLqH8mMORA4kza.png)](https://asciinema.org/a/AKkYwzEakSUsLqH8mMORA4kza) + +## Available Event Listeners +1. AMQP +2. AWS SNS +3. AWS SQS +4. Cron Schedules +5. GCP PubSub +6. GitHub +7. GitLab +8. HDFS +9. File Based Events +10. Kafka +11. Minio +12. NATS +13. MQTT +14. K8s Resources +15. Slack +16. NetApp StorageGrid +17. Webhooks ## Who uses Argo Events? Organizations below are **officially** using Argo Events. Please send a PR with your organization name if you are using Argo Events. +* [BioBox Analytics](https://biobox.io) * [BlackRock](https://www.blackrock.com/) * [Canva](https://www.canva.com/) * [Fairwinds](https://fairwinds.com/) * [Intuit](https://www.intuit.com/) * [Viaduct](https://www.viaduct.ai/) -* [BioBox Analytics](https://biobox.io) ## Contribute Read and abide by the [Argo Events Code of Conduct](https://github.com/argoproj/argo-events/blob/master/CODE_OF_CONDUCT.md) diff --git a/VERSION b/VERSION index 0eb41820ee..5473372f79 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.11 \ No newline at end of file +0.12-rc \ No newline at end of file diff --git a/api/event-source.html b/api/event-source.html new file mode 100644 index 0000000000..0ffead5d76 --- /dev/null +++ b/api/event-source.html @@ -0,0 +1,1755 @@ +

Packages:

+ +

argoproj.io/v1alpha1

+

+

Package v1alpha1 is the v1alpha1 version of the API.

+

+Resource Types: + +

AMQPEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

AMQPEventSource refers to an event-source for AMQP stream events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+url
+ +string + +
+

URL for rabbitmq service

+
+exchangeName
+ +string + +
+

ExchangeName is the exchange name +For more information, visit https://www.rabbitmq.com/tutorials/amqp-concepts.html

+
+exchangeType
+ +string + +
+

ExchangeType is rabbitmq exchange type

+
+routingKey
+ +string + +
+

Routing key for bindings

+
+connectionBackoff
+ +github.com/argoproj/argo-events/common.Backoff + +
+(Optional) +

Backoff holds parameters applied to connection.

+
+

CalendarEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

CalendarEventSource describes a time based dependency. One of the fields (schedule, interval, or recurrence) must be passed. +Schedule takes precedence over interval; interval takes precedence over recurrence

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+schedule
+ +string + +
+

Schedule is a cron-like expression. For reference, see: https://en.wikipedia.org/wiki/Cron

+
+interval
+ +string + +
+

Interval is a string that describes an interval duration, e.g. 1s, 30m, 2h…

+
+exclusionDates
+ +[]string + +
+

ExclusionDates defines the list of DATE-TIME exceptions for recurring events.

+
+timezone
+ +string + +
+(Optional) +

Timezone in which to run the schedule

+
+userPayload
+ +encoding/json.RawMessage + +
+(Optional) +

UserPayload will be sent to sensor as extra data once the event is triggered

+
+

EventSource +

+

+

EventSource is the definition of a eventsource resource

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+status
+ + +EventSourceStatus + + +
+
+spec
+ + +EventSourceSpec + + +
+
+
+ +
+
+

EventSourceSpec +

+

+(Appears on: +EventSource) +

+

+

EventSourceSpec refers to specification of event-source resource

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+minio
+ +map[string]github.com/argoproj/argo-events/pkg/apis/common.S3Artifact + +
+

Minio event sources

+
+calendar
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.CalendarEventSource + + +
+

Calendar event sources

+
+file
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.FileEventSource + + +
+

File event sources

+
+resource
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceEventSource + + +
+

Resource event sources

+
+webhook
+ +map[string]github.com/argoproj/argo-events/gateways/server/common/webhook.Context + +
+

Webhook event sources

+
+amqp
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.AMQPEventSource + + +
+

AMQP event sources

+
+kafka
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.KafkaEventSource + + +
+

Kafka event sources

+
+mqtt
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.MQTTEventSource + + +
+

MQTT event sources

+
+nats
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.NATSEventsSource + + +
+

NATS event sources

+
+sns
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SNSEventSource + + +
+

SNS event sources

+
+sqs
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SQSEventSource + + +
+

SQS event sources

+
+pubSub
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.PubSubEventSource + + +
+

PubSub eevnt sources

+
+github
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GithubEventSource + + +
+

Github event sources

+
+gitlab
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GitlabEventSource + + +
+

Gitlab event sources

+
+hdfs
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.HDFSEventSource + + +
+

HDFS event sources

+
+slack
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SlackEventSource + + +
+

Slack event sources

+
+storageGrid
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridEventSource + + +
+

StorageGrid event sources

+
+type
+ +Argo Events common.EventSourceType + +
+

Type of the event source

+
+

EventSourceStatus +

+

+(Appears on: +EventSource) +

+

+

EventSourceStatus holds the status of the event-source resource

+

+ + + + + + + + + + + + + +
FieldDescription
+createdAt
+ + +Kubernetes meta/v1.Time + + +
+
+

FileEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

FileEventSource describes an event-source for file related events.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+eventType
+ +string + +
+

Type of file operations to watch +Refer https://github.com/fsnotify/fsnotify/blob/master/fsnotify.go for more information

+
+watchPathConfig
+ +github.com/argoproj/argo-events/gateways/server/common/fsevent.WatchPathConfig + +
+

WatchPathConfig contains configuration about the file path to watch

+
+

GithubEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

GithubEventSource refers to event-source for github related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+id
+ +int64 + +
+

Id is the webhook’s id

+
+webhook
+ +github.com/argoproj/argo-events/gateways/server/common/webhook.Context + +
+

Webhook refers to the configuration required to run a http server

+
+owner
+ +string + +
+

Owner refers to GitHub owner name i.e. argoproj

+
+repository
+ +string + +
+

Repository refers to GitHub repo name i.e. argo-events

+
+events
+ +[]string + +
+

Events refer to Github events to subscribe to which the gateway will subscribe

+
+apiToken
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

APIToken refers to a K8s secret containing github api token

+
+webhookSecret
+ + +Kubernetes core/v1.SecretKeySelector + + +
+(Optional) +

WebhookSecret refers to K8s secret containing GitHub webhook secret +https://developer.github.com/webhooks/securing/

+
+insecure
+ +bool + +
+

Insecure tls verification

+
+active
+ +bool + +
+(Optional) +

Active refers to status of the webhook for event deliveries. +https://developer.github.com/webhooks/creating/#active

+
+contentType
+ +string + +
+

ContentType of the event delivery

+
+githubBaseURL
+ +string + +
+(Optional) +

GitHub base URL (for GitHub Enterprise)

+
+githubUploadURL
+ +string + +
+(Optional) +

GitHub upload URL (for GitHub Enterprise)

+
+namespace
+ +string + +
+

Namespace refers to Kubernetes namespace which is used to retrieve webhook secret and api token from.

+
+deleteHookOnFinish
+ +bool + +
+(Optional) +

DeleteHookOnFinish determines whether to delete the GitHub hook for the repository once the event source is stopped.

+
+

GitlabEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

GitlabEventSource refers to event-source related to Gitlab events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+webhook
+ +github.com/argoproj/argo-events/gateways/server/common/webhook.Context + +
+

Webhook holds configuration to run a http server

+
+projectId
+ +string + +
+

ProjectId is the id of project for which integration needs to setup

+
+event
+ +string + +
+

Event is a gitlab event to listen to. +Refer https://github.com/xanzy/go-gitlab/blob/bf34eca5d13a9f4c3f501d8a97b8ac226d55e4d9/projects.go#L794.

+
+accessToken
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

AccessToken is reference to k8 secret which holds the gitlab api access information

+
+enableSSLVerification
+ +bool + +
+(Optional) +

EnableSSLVerification to enable ssl verification

+
+gitlabBaseURL
+ +string + +
+

GitlabBaseURL is the base URL for API requests to a custom endpoint

+
+namespace
+ +string + +
+

Namespace refers to Kubernetes namespace which is used to retrieve access token from.

+
+deleteHookOnFinish
+ +bool + +
+(Optional) +

DeleteHookOnFinish determines whether to delete the GitLab hook for the project once the event source is stopped.

+
+

HDFSEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

HDFSEventSource refers to event-source for HDFS related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+WatchPathConfig
+ +github.com/argoproj/argo-events/gateways/server/common/fsevent.WatchPathConfig + +
+

+(Members of WatchPathConfig are embedded into this type.) +

+
+type
+ +string + +
+

Type of file operations to watch

+
+checkInterval
+ +string + +
+

CheckInterval is a string that describes an interval duration to check the directory state, e.g. 1s, 30m, 2h… (defaults to 1m)

+
+addresses
+ +[]string + +
+

Addresses is accessible addresses of HDFS name nodes

+
+hdfsUser
+ +string + +
+

HDFSUser is the user to access HDFS file system. +It is ignored if either ccache or keytab is used.

+
+krbCCacheSecret
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

KrbCCacheSecret is the secret selector for Kerberos ccache +Either ccache or keytab can be set to use Kerberos.

+
+krbKeytabSecret
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

KrbKeytabSecret is the secret selector for Kerberos keytab +Either ccache or keytab can be set to use Kerberos.

+
+krbUsername
+ +string + +
+

KrbUsername is the Kerberos username used with Kerberos keytab +It must be set if keytab is used.

+
+krbRealm
+ +string + +
+

KrbRealm is the Kerberos realm used with Kerberos keytab +It must be set if keytab is used.

+
+krbConfigConfigMap
+ + +Kubernetes core/v1.ConfigMapKeySelector + + +
+

KrbConfig is the configmap selector for Kerberos config as string +It must be set if either ccache or keytab is used.

+
+krbServicePrincipalName
+ +string + +
+

KrbServicePrincipalName is the principal name of Kerberos service +It must be set if either ccache or keytab is used.

+
+namespace
+ +string + +
+

Namespace refers to Kubernetes namespace which is used to retrieve cache secret and ket tab secret from.

+
+

KafkaEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

KafkaEventSource refers to event-source for Kafka related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+url
+ +string + +
+

URL to kafka cluster

+
+partition
+ +string + +
+

Partition name

+
+topic
+ +string + +
+

Topic name

+
+connectionBackoff
+ +github.com/argoproj/argo-events/common.Backoff + +
+

Backoff holds parameters applied to connection.

+
+

MQTTEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

MQTTEventSource refers to event-source for MQTT related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+url
+ +string + +
+

URL to connect to broker

+
+topic
+ +string + +
+

Topic name

+
+clientId
+ +string + +
+

ClientID is the id of the client

+
+connectionBackoff
+ +github.com/argoproj/argo-events/common.Backoff + +
+

ConnectionBackoff holds backoff applied to connection.

+
+

NATSEventsSource +

+

+(Appears on: +EventSourceSpec) +

+

+

NATSEventSource refers to event-source for NATS related events

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+url
+ +string + +
+

URL to connect to NATS cluster

+
+subject
+ +string + +
+

Subject holds the name of the subject onto which messages are published

+
+connectionBackoff
+ +github.com/argoproj/argo-events/common.Backoff + +
+

ConnectionBackoff holds backoff applied to connection.

+
+

PubSubEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

PubSubEventSource refers to event-source for GCP PubSub related events.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+projectID
+ +string + +
+

ProjectID is the unique identifier for your project on GCP

+
+topicProjectID
+ +string + +
+

TopicProjectID identifies the project where the topic should exist or be created +(assumed to be the same as ProjectID by default)

+
+topic
+ +string + +
+

Topic on which a subscription will be created

+
+credentialsFile
+ +string + +
+

CredentialsFile is the file that contains credentials to authenticate for GCP

+
+deleteSubscriptionOnFinish
+ +bool + +
+(Optional) +

DeleteSubscriptionOnFinish determines whether to delete the GCP PubSub subscription once the event source is stopped.

+
+

ResourceEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

ResourceEventSource refers to a event-source for K8s resource related events.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+namespace
+ +string + +
+

Namespace where resource is deployed

+
+filter
+ + +ResourceFilter + + +
+(Optional) +

Filter is applied on the metadata of the resource

+
+GroupVersionResource
+ + +Kubernetes meta/v1.GroupVersionResource + + +
+

+(Members of GroupVersionResource are embedded into this type.) +

+

Group of the resource

+
+eventType
+ + +ResourceEventType + + +
+(Optional) +

Type is the event type. +If not provided, the gateway will watch all events for a resource.

+
+

ResourceEventType +(string alias)

+

+(Appears on: +ResourceEventSource) +

+

+

ResourceEventType is the type of event for the K8s resource mutation

+

+

ResourceFilter +

+

+(Appears on: +ResourceEventSource) +

+

+

ResourceFilter contains K8 ObjectMeta information to further filter resource event objects

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+prefix
+ +string + +
+(Optional) +
+labels
+ +map[string]string + +
+(Optional) +
+fields
+ +map[string]string + +
+(Optional) +
+createdBy
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +
+

SNSEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

SNSEventSource refers to event-source for AWS SNS related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+webhook
+ +github.com/argoproj/argo-events/gateways/server/common/webhook.Context + +
+

Webhook configuration for http server

+
+topicArn
+ +string + +
+

TopicArn

+
+accessKey
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

AccessKey refers K8 secret containing aws access key

+
+secretKey
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

SecretKey refers K8 secret containing aws secret key

+
+namespace
+ +string + +
+(Optional) +

Namespace refers to Kubernetes namespace to read access related secret from.

+
+region
+ +string + +
+

Region is AWS region

+
+

SQSEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

SQSEventSource refers to event-source for AWS SQS related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+accessKey
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

AccessKey refers K8 secret containing aws access key

+
+secretKey
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

SecretKey refers K8 secret containing aws secret key

+
+region
+ +string + +
+

Region is AWS region

+
+queue
+ +string + +
+

Queue is AWS SQS queue to listen to for messages

+
+waitTimeSeconds
+ +int64 + +
+

WaitTimeSeconds is The duration (in seconds) for which the call waits for a message to arrive +in the queue before returning.

+
+namespace
+ +string + +
+(Optional) +

Namespace refers to Kubernetes namespace to read access related secret from.

+
+

SlackEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

SlackEventSource refers to event-source for Slack related events

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+signingSecret
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

Slack App signing secret

+
+token
+ + +Kubernetes core/v1.SecretKeySelector + + +
+

Token for URL verification handshake

+
+webhook
+ +github.com/argoproj/argo-events/gateways/server/common/webhook.Context + +
+

Webhook holds configuration for a REST endpoint

+
+namespace
+ +string + +
+

Namespace refers to Kubernetes namespace which is used to retrieve token and signing secret from.

+
+

StorageGridEventSource +

+

+(Appears on: +EventSourceSpec) +

+

+

StorageGridEventSource refers to event-source for StorageGrid related events

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+webhook
+ +github.com/argoproj/argo-events/gateways/server/common/webhook.Context + +
+

Webhook holds configuration for a REST endpoint

+
+events
+ +[]string + +
+

Events are s3 bucket notification events. +For more information on s3 notifications, follow https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-event-types-and-destinations +Note that storage grid notifications do not contain s3:

+
+filter
+ + +StorageGridFilter + + +
+

Filter on object key which caused the notification.

+
+

StorageGridFilter +

+

+(Appears on: +StorageGridEventSource) +

+

+

Filter represents filters to apply to bucket notifications for specifying constraints on objects

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+prefix
+ +string + +
+
+suffix
+ +string + +
+
+
+

+Generated with gen-crd-api-reference-docs +on git commit 8d85191. +

diff --git a/api/event-source.md b/api/event-source.md new file mode 100644 index 0000000000..06589b34aa --- /dev/null +++ b/api/event-source.md @@ -0,0 +1,3489 @@ +

+ +Packages: + +

+ + + +

+ +argoproj.io/v1alpha1 + +

+ +

+ +

+ +Package v1alpha1 is the v1alpha1 version of the API. + +

+ +

+ +Resource Types: + + + +

+ +AMQPEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +AMQPEventSource refers to an event-source for AMQP stream events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +url
string + +
+ +

+ +URL for rabbitmq service + +

+ +
+ +exchangeName
string + +
+ +

+ +ExchangeName is the exchange name For more information, visit +https://www.rabbitmq.com/tutorials/amqp-concepts.html + +

+ +
+ +exchangeType
string + +
+ +

+ +ExchangeType is rabbitmq exchange type + +

+ +
+ +routingKey
string + +
+ +

+ +Routing key for bindings + +

+ +
+ +connectionBackoff
+github.com/argoproj/argo-events/common.Backoff + +
+ +(Optional) + +

+ +Backoff holds parameters applied to connection. + +

+ +
+ +

+ +CalendarEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +CalendarEventSource describes a time based dependency. One of the fields +(schedule, interval, or recurrence) must be passed. Schedule takes +precedence over interval; interval takes precedence over recurrence + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +schedule
string + +
+ +

+ +Schedule is a cron-like expression. For reference, see: +https://en.wikipedia.org/wiki/Cron + +

+ +
+ +interval
string + +
+ +

+ +Interval is a string that describes an interval duration, e.g. 1s, 30m, +2h… + +

+ +
+ +exclusionDates
\[\]string + +
+ +

+ +ExclusionDates defines the list of DATE-TIME exceptions for recurring +events. + +

+ +
+ +timezone
string + +
+ +(Optional) + +

+ +Timezone in which to run the schedule + +

+ +
+ +userPayload
encoding/json.RawMessage + +
+ +(Optional) + +

+ +UserPayload will be sent to sensor as extra data once the event is +triggered + +

+ +
+ +

+ +EventSource + +

+ +

+ +

+ +EventSource is the definition of a eventsource resource + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +metadata
+ +Kubernetes meta/v1.ObjectMeta + +
+ +Refer to the Kubernetes API documentation for the fields of the +metadata field. + +
+ +status
+ EventSourceStatus + + +
+ +
+ +spec
+ EventSourceSpec + + +
+ +

+ + + +
+ +
+ +

+ +EventSourceSpec + +

+ +

+ +(Appears on: +EventSource) + +

+ +

+ +

+ +EventSourceSpec refers to specification of event-source resource + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +minio
+map\[string\]github.com/argoproj/argo-events/pkg/apis/common.S3Artifact + + +
+ +

+ +Minio event sources + +

+ +
+ +calendar
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.CalendarEventSource + + +
+ +

+ +Calendar event sources + +

+ +
+ +file
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.FileEventSource + + +
+ +

+ +File event sources + +

+ +
+ +resource
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceEventSource + + +
+ +

+ +Resource event sources + +

+ +
+ +webhook
+map\[string\]github.com/argoproj/argo-events/gateways/server/common/webhook.Context + + +
+ +

+ +Webhook event sources + +

+ +
+ +amqp
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.AMQPEventSource + + +
+ +

+ +AMQP event sources + +

+ +
+ +kafka
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.KafkaEventSource + + +
+ +

+ +Kafka event sources + +

+ +
+ +mqtt
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.MQTTEventSource + + +
+ +

+ +MQTT event sources + +

+ +
+ +nats
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.NATSEventsSource + + +
+ +

+ +NATS event sources + +

+ +
+ +sns
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SNSEventSource + + +
+ +

+ +SNS event sources + +

+ +
+ +sqs
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SQSEventSource + + +
+ +

+ +SQS event sources + +

+ +
+ +pubSub
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.PubSubEventSource + + +
+ +

+ +PubSub eevnt sources + +

+ +
+ +github
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GithubEventSource + + +
+ +

+ +Github event sources + +

+ +
+ +gitlab
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GitlabEventSource + + +
+ +

+ +Gitlab event sources + +

+ +
+ +hdfs
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.HDFSEventSource + + +
+ +

+ +HDFS event sources + +

+ +
+ +slack
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SlackEventSource + + +
+ +

+ +Slack event sources + +

+ +
+ +storageGrid
+ +map\[string\]github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridEventSource + + +
+ +

+ +StorageGrid event sources + +

+ +
+ +type
Argo Events common.EventSourceType + +
+ +

+ +Type of the event source + +

+ +
+ +

+ +EventSourceStatus + +

+ +

+ +(Appears on: +EventSource) + +

+ +

+ +

+ +EventSourceStatus holds the status of the event-source resource + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +createdAt
+ +Kubernetes meta/v1.Time + +
+ +
+ +

+ +FileEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +FileEventSource describes an event-source for file related events. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +eventType
string + +
+ +

+ +Type of file operations to watch Refer +https://github.com/fsnotify/fsnotify/blob/master/fsnotify.go +for more information + +

+ +
+ +watchPathConfig
+github.com/argoproj/argo-events/gateways/server/common/fsevent.WatchPathConfig + + +
+ +

+ +WatchPathConfig contains configuration about the file path to watch + +

+ +
+ +

+ +GithubEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +GithubEventSource refers to event-source for github related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +id
int64 + +
+ +

+ +Id is the webhook’s id + +

+ +
+ +webhook
+github.com/argoproj/argo-events/gateways/server/common/webhook.Context + + +
+ +

+ +Webhook refers to the configuration required to run a http server + +

+ +
+ +owner
string + +
+ +

+ +Owner refers to GitHub owner name i.e. argoproj + +

+ +
+ +repository
string + +
+ +

+ +Repository refers to GitHub repo name i.e. argo-events + +

+ +
+ +events
\[\]string + +
+ +

+ +Events refer to Github events to subscribe to which the gateway will +subscribe + +

+ +
+ +apiToken
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +APIToken refers to a K8s secret containing github api token + +

+ +
+ +webhookSecret
+ +Kubernetes core/v1.SecretKeySelector + +
+ +(Optional) + +

+ +WebhookSecret refers to K8s secret containing GitHub webhook secret +https://developer.github.com/webhooks/securing/ + +

+ +
+ +insecure
bool + +
+ +

+ +Insecure tls verification + +

+ +
+ +active
bool + +
+ +(Optional) + +

+ +Active refers to status of the webhook for event deliveries. +https://developer.github.com/webhooks/creating/\#active + +

+ +
+ +contentType
string + +
+ +

+ +ContentType of the event delivery + +

+ +
+ +githubBaseURL
string + +
+ +(Optional) + +

+ +GitHub base URL (for GitHub Enterprise) + +

+ +
+ +githubUploadURL
string + +
+ +(Optional) + +

+ +GitHub upload URL (for GitHub Enterprise) + +

+ +
+ +namespace
string + +
+ +

+ +Namespace refers to Kubernetes namespace which is used to retrieve +webhook secret and api token from. + +

+ +
+ +deleteHookOnFinish
bool + +
+ +(Optional) + +

+ +DeleteHookOnFinish determines whether to delete the GitHub hook for the +repository once the event source is stopped. + +

+ +
+ +

+ +GitlabEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +GitlabEventSource refers to event-source related to Gitlab events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +webhook
+github.com/argoproj/argo-events/gateways/server/common/webhook.Context + + +
+ +

+ +Webhook holds configuration to run a http server + +

+ +
+ +projectId
string + +
+ +

+ +ProjectId is the id of project for which integration needs to setup + +

+ +
+ +event
string + +
+ +

+ +Event is a gitlab event to listen to. Refer +https://github.com/xanzy/go-gitlab/blob/bf34eca5d13a9f4c3f501d8a97b8ac226d55e4d9/projects.go\#L794. + +

+ +
+ +accessToken
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +AccessToken is reference to k8 secret which holds the gitlab api access +information + +

+ +
+ +enableSSLVerification
bool + +
+ +(Optional) + +

+ +EnableSSLVerification to enable ssl verification + +

+ +
+ +gitlabBaseURL
string + +
+ +

+ +GitlabBaseURL is the base URL for API requests to a custom endpoint + +

+ +
+ +namespace
string + +
+ +

+ +Namespace refers to Kubernetes namespace which is used to retrieve +access token from. + +

+ +
+ +deleteHookOnFinish
bool + +
+ +(Optional) + +

+ +DeleteHookOnFinish determines whether to delete the GitLab hook for the +project once the event source is stopped. + +

+ +
+ +

+ +HDFSEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +HDFSEventSource refers to event-source for HDFS related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +WatchPathConfig
+github.com/argoproj/argo-events/gateways/server/common/fsevent.WatchPathConfig + + +
+ +

+ +(Members of WatchPathConfig are embedded into this type.) + +

+ +
+ +type
string + +
+ +

+ +Type of file operations to watch + +

+ +
+ +checkInterval
string + +
+ +

+ +CheckInterval is a string that describes an interval duration to check +the directory state, e.g. 1s, 30m, 2h… (defaults to 1m) + +

+ +
+ +addresses
\[\]string + +
+ +

+ +Addresses is accessible addresses of HDFS name nodes + +

+ +
+ +hdfsUser
string + +
+ +

+ +HDFSUser is the user to access HDFS file system. It is ignored if either +ccache or keytab is used. + +

+ +
+ +krbCCacheSecret
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +KrbCCacheSecret is the secret selector for Kerberos ccache Either ccache +or keytab can be set to use Kerberos. + +

+ +
+ +krbKeytabSecret
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +KrbKeytabSecret is the secret selector for Kerberos keytab Either ccache +or keytab can be set to use Kerberos. + +

+ +
+ +krbUsername
string + +
+ +

+ +KrbUsername is the Kerberos username used with Kerberos keytab It must +be set if keytab is used. + +

+ +
+ +krbRealm
string + +
+ +

+ +KrbRealm is the Kerberos realm used with Kerberos keytab It must be set +if keytab is used. + +

+ +
+ +krbConfigConfigMap
+ +Kubernetes core/v1.ConfigMapKeySelector + +
+ +

+ +KrbConfig is the configmap selector for Kerberos config as string It +must be set if either ccache or keytab is used. + +

+ +
+ +krbServicePrincipalName
string + +
+ +

+ +KrbServicePrincipalName is the principal name of Kerberos service It +must be set if either ccache or keytab is used. + +

+ +
+ +namespace
string + +
+ +

+ +Namespace refers to Kubernetes namespace which is used to retrieve cache +secret and ket tab secret from. + +

+ +
+ +

+ +KafkaEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +KafkaEventSource refers to event-source for Kafka related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +url
string + +
+ +

+ +URL to kafka cluster + +

+ +
+ +partition
string + +
+ +

+ +Partition name + +

+ +
+ +topic
string + +
+ +

+ +Topic name + +

+ +
+ +connectionBackoff
+github.com/argoproj/argo-events/common.Backoff + +
+ +

+ +Backoff holds parameters applied to connection. + +

+ +
+ +

+ +MQTTEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +MQTTEventSource refers to event-source for MQTT related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +url
string + +
+ +

+ +URL to connect to broker + +

+ +
+ +topic
string + +
+ +

+ +Topic name + +

+ +
+ +clientId
string + +
+ +

+ +ClientID is the id of the client + +

+ +
+ +connectionBackoff
+github.com/argoproj/argo-events/common.Backoff + +
+ +

+ +ConnectionBackoff holds backoff applied to connection. + +

+ +
+ +

+ +NATSEventsSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +NATSEventSource refers to event-source for NATS related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +url
string + +
+ +

+ +URL to connect to NATS cluster + +

+ +
+ +subject
string + +
+ +

+ +Subject holds the name of the subject onto which messages are published + +

+ +
+ +connectionBackoff
+github.com/argoproj/argo-events/common.Backoff + +
+ +

+ +ConnectionBackoff holds backoff applied to connection. + +

+ +
+ +

+ +PubSubEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +PubSubEventSource refers to event-source for GCP PubSub related events. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +projectID
string + +
+ +

+ +ProjectID is the unique identifier for your project on GCP + +

+ +
+ +topicProjectID
string + +
+ +

+ +TopicProjectID identifies the project where the topic should exist or be +created (assumed to be the same as ProjectID by default) + +

+ +
+ +topic
string + +
+ +

+ +Topic on which a subscription will be created + +

+ +
+ +credentialsFile
string + +
+ +

+ +CredentialsFile is the file that contains credentials to authenticate +for GCP + +

+ +
+ +deleteSubscriptionOnFinish
bool + +
+ +(Optional) + +

+ +DeleteSubscriptionOnFinish determines whether to delete the GCP PubSub +subscription once the event source is stopped. + +

+ +
+ +

+ +ResourceEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +ResourceEventSource refers to a event-source for K8s resource related +events. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +namespace
string + +
+ +

+ +Namespace where resource is deployed + +

+ +
+ +filter
+ ResourceFilter + + +
+ +(Optional) + +

+ +Filter is applied on the metadata of the resource + +

+ +
+ +GroupVersionResource
+ +Kubernetes meta/v1.GroupVersionResource + +
+ +

+ +(Members of GroupVersionResource are embedded into this +type.) + +

+ +

+ +Group of the resource + +

+ +
+ +eventType
+ ResourceEventType + + +
+ +(Optional) + +

+ +Type is the event type. If not provided, the gateway will watch all +events for a resource. + +

+ +
+ +

+ +ResourceEventType (string alias) + +

+ +

+ +

+ +(Appears on: +ResourceEventSource) + +

+ +

+ +

+ +ResourceEventType is the type of event for the K8s resource mutation + +

+ +

+ +

+ +ResourceFilter + +

+ +

+ +(Appears on: +ResourceEventSource) + +

+ +

+ +

+ +ResourceFilter contains K8 ObjectMeta information to further filter +resource event objects + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +prefix
string + +
+ +(Optional) + +
+ +labels
map\[string\]string + +
+ +(Optional) + +
+ +fields
map\[string\]string + +
+ +(Optional) + +
+ +createdBy
+ +Kubernetes meta/v1.Time + +
+ +(Optional) + +
+ +

+ +SNSEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +SNSEventSource refers to event-source for AWS SNS related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +webhook
+github.com/argoproj/argo-events/gateways/server/common/webhook.Context + + +
+ +

+ +Webhook configuration for http server + +

+ +
+ +topicArn
string + +
+ +

+ +TopicArn + +

+ +
+ +accessKey
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +AccessKey refers K8 secret containing aws access key + +

+ +
+ +secretKey
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +SecretKey refers K8 secret containing aws secret key + +

+ +
+ +namespace
string + +
+ +(Optional) + +

+ +Namespace refers to Kubernetes namespace to read access related secret +from. + +

+ +
+ +region
string + +
+ +

+ +Region is AWS region + +

+ +
+ +

+ +SQSEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +SQSEventSource refers to event-source for AWS SQS related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +accessKey
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +AccessKey refers K8 secret containing aws access key + +

+ +
+ +secretKey
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +SecretKey refers K8 secret containing aws secret key + +

+ +
+ +region
string + +
+ +

+ +Region is AWS region + +

+ +
+ +queue
string + +
+ +

+ +Queue is AWS SQS queue to listen to for messages + +

+ +
+ +waitTimeSeconds
int64 + +
+ +

+ +WaitTimeSeconds is The duration (in seconds) for which the call waits +for a message to arrive in the queue before returning. + +

+ +
+ +namespace
string + +
+ +(Optional) + +

+ +Namespace refers to Kubernetes namespace to read access related secret +from. + +

+ +
+ +

+ +SlackEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +SlackEventSource refers to event-source for Slack related events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +signingSecret
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +Slack App signing secret + +

+ +
+ +token
+ +Kubernetes core/v1.SecretKeySelector + +
+ +

+ +Token for URL verification handshake + +

+ +
+ +webhook
+github.com/argoproj/argo-events/gateways/server/common/webhook.Context + + +
+ +

+ +Webhook holds configuration for a REST endpoint + +

+ +
+ +namespace
string + +
+ +

+ +Namespace refers to Kubernetes namespace which is used to retrieve token +and signing secret from. + +

+ +
+ +

+ +StorageGridEventSource + +

+ +

+ +(Appears on: +EventSourceSpec) + +

+ +

+ +

+ +StorageGridEventSource refers to event-source for StorageGrid related +events + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +webhook
+github.com/argoproj/argo-events/gateways/server/common/webhook.Context + + +
+ +

+ +Webhook holds configuration for a REST endpoint + +

+ +
+ +events
\[\]string + +
+ +

+ +Events are s3 bucket notification events. For more information on s3 +notifications, follow +https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html\#notification-how-to-event-types-and-destinations +Note that storage grid notifications do not contain s3: + +

+ +
+ +filter
+ StorageGridFilter + + +
+ +

+ +Filter on object key which caused the notification. + +

+ +
+ +

+ +StorageGridFilter + +

+ +

+ +(Appears on: +StorageGridEventSource) + +

+ +

+ +

+ +Filter represents filters to apply to bucket notifications for +specifying constraints on objects + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +prefix
string + +
+ +
+ +suffix
string + +
+ +
+ +
+ +

+ + Generated with gen-crd-api-reference-docs on git +commit 8d85191. + +

diff --git a/api/gateway.html b/api/gateway.html new file mode 100644 index 0000000000..965c9ad323 --- /dev/null +++ b/api/gateway.html @@ -0,0 +1,731 @@ +

Packages:

+ +

argoproj.io/v1alpha1

+

+

Package v1alpha1 is the v1alpha1 version of the API.

+

+Resource Types: + +

EventSourceRef +

+

+(Appears on: +GatewaySpec) +

+

+

EventSourceRef holds information about the EventSourceRef custom resource

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the event source

+
+namespace
+ +string + +
+(Optional) +

Namespace of the event source +Default value is the namespace where referencing gateway is deployed

+
+

Gateway +

+

+

Gateway is the definition of a gateway resource

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+status
+ + +GatewayStatus + + +
+
+spec
+ + +GatewaySpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+template
+ + +Kubernetes core/v1.PodTemplateSpec + + +
+

Template is the pod specification for the gateway +Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core

+
+eventSourceRef
+ + +EventSourceRef + + +
+

EventSourceRef refers to event-source that stores event source configurations for the gateway

+
+type
+ +Argo Events common.EventSourceType + +
+

Type is the type of gateway. Used as metadata.

+
+service
+ + +Kubernetes core/v1.Service + + +
+

Service is the specifications of the service to expose the gateway +Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#service-v1-core

+
+watchers
+ + +NotificationWatchers + + +
+

Watchers are components which are interested listening to notifications from this gateway +These only need to be specified when gateway dispatch mechanism is through HTTP POST notifications. +In future, support for NATS, KAFKA will be added as a means to dispatch notifications in which case +specifying watchers would be unnecessary.

+
+processorPort
+ +string + +
+

Port on which the gateway event source processor is running on.

+
+eventProtocol
+ +Argo Events common.EventProtocol + +
+

EventProtocol is the underlying protocol used to send events from gateway to watchers(components interested in listening to event from this gateway)

+
+replica
+ +int + +
+

Replica is the gateway deployment replicas

+
+
+

GatewayNotificationWatcher +

+

+(Appears on: +NotificationWatchers) +

+

+

GatewayNotificationWatcher is the gateway interested in listening to notifications from this gateway

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name is the gateway name

+
+port
+ +string + +
+

Port is http server port on which gateway is running

+
+endpoint
+ +string + +
+

Endpoint is REST API endpoint to post event to. +Events are sent using HTTP POST method to this endpoint.

+
+namespace
+ +string + +
+

Namespace of the gateway

+
+

GatewayResource +

+

+(Appears on: +GatewayStatus) +

+

+

GatewayResource holds the metadata about the gateway resources

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+deployment
+ + +Kubernetes meta/v1.ObjectMeta + + +
+

Metadata of the deployment for the gateway

+
+service
+ + +Kubernetes meta/v1.ObjectMeta + + +
+(Optional) +

Metadata of the service for the gateway

+
+

GatewaySpec +

+

+(Appears on: +Gateway) +

+

+

GatewaySpec represents gateway specifications

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+template
+ + +Kubernetes core/v1.PodTemplateSpec + + +
+

Template is the pod specification for the gateway +Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core

+
+eventSourceRef
+ + +EventSourceRef + + +
+

EventSourceRef refers to event-source that stores event source configurations for the gateway

+
+type
+ +Argo Events common.EventSourceType + +
+

Type is the type of gateway. Used as metadata.

+
+service
+ + +Kubernetes core/v1.Service + + +
+

Service is the specifications of the service to expose the gateway +Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#service-v1-core

+
+watchers
+ + +NotificationWatchers + + +
+

Watchers are components which are interested listening to notifications from this gateway +These only need to be specified when gateway dispatch mechanism is through HTTP POST notifications. +In future, support for NATS, KAFKA will be added as a means to dispatch notifications in which case +specifying watchers would be unnecessary.

+
+processorPort
+ +string + +
+

Port on which the gateway event source processor is running on.

+
+eventProtocol
+ +Argo Events common.EventProtocol + +
+

EventProtocol is the underlying protocol used to send events from gateway to watchers(components interested in listening to event from this gateway)

+
+replica
+ +int + +
+

Replica is the gateway deployment replicas

+
+

GatewayStatus +

+

+(Appears on: +Gateway) +

+

+

GatewayStatus contains information about the status of a gateway.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +NodePhase + + +
+

Phase is the high-level summary of the gateway

+
+startedAt
+ + +Kubernetes meta/v1.Time + + +
+

StartedAt is the time at which this gateway was initiated

+
+message
+ +string + +
+

Message is a human readable string indicating details about a gateway in its phase

+
+nodes
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NodeStatus + + +
+

Nodes is a mapping between a node ID and the node’s status +it records the states for the configurations of gateway.

+
+resources
+ + +GatewayResource + + +
+

Resources refers to the metadata about the gateway resources

+
+

NodePhase +(string alias)

+

+(Appears on: +GatewayStatus, +NodeStatus) +

+

+

NodePhase is the label for the condition of a node.

+

+

NodeStatus +

+

+(Appears on: +GatewayStatus) +

+

+

NodeStatus describes the status for an individual node in the gateway configurations. +A single node can represent one configuration.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+id
+ +string + +
+

ID is a unique identifier of a node within a sensor +It is a hash of the node name

+
+name
+ +string + +
+

Name is a unique name in the node tree used to generate the node ID

+
+displayName
+ +string + +
+

DisplayName is the human readable representation of the node

+
+phase
+ + +NodePhase + + +
+

Phase of the node

+
+startedAt
+ + +Kubernetes meta/v1.MicroTime + + +
+

StartedAt is the time at which this node started

+
+message
+ +string + +
+

Message store data or something to save for configuration

+
+updateTime
+ + +Kubernetes meta/v1.MicroTime + + +
+

UpdateTime is the time when node(gateway configuration) was updated

+
+

NotificationWatchers +

+

+(Appears on: +GatewaySpec) +

+

+

NotificationWatchers are components which are interested listening to notifications from this gateway

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+gateways
+ + +[]GatewayNotificationWatcher + + +
+

Gateways is the list of gateways interested in listening to notifications from this gateway

+
+sensors
+ + +[]SensorNotificationWatcher + + +
+

Sensors is the list of sensors interested in listening to notifications from this gateway

+
+

SensorNotificationWatcher +

+

+(Appears on: +NotificationWatchers) +

+

+

SensorNotificationWatcher is the sensor interested in listening to notifications from this gateway

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name is the name of the sensor

+
+namespace
+ +string + +
+

Namespace of the sensor

+
+
+

+Generated with gen-crd-api-reference-docs +on git commit 8d85191. +

diff --git a/api/gateway.md b/api/gateway.md new file mode 100644 index 0000000000..20df87387d --- /dev/null +++ b/api/gateway.md @@ -0,0 +1,1451 @@ +

+ +Packages: + +

+ + + +

+ +argoproj.io/v1alpha1 + +

+ +

+ +

+ +Package v1alpha1 is the v1alpha1 version of the API. + +

+ +

+ +Resource Types: + + + +

+ +EventSourceRef + +

+ +

+ +(Appears on: +GatewaySpec) + +

+ +

+ +

+ +EventSourceRef holds information about the EventSourceRef custom +resource + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name of the event source + +

+ +
+ +namespace
string + +
+ +(Optional) + +

+ +Namespace of the event source Default value is the namespace where +referencing gateway is deployed + +

+ +
+ +

+ +Gateway + +

+ +

+ +

+ +Gateway is the definition of a gateway resource + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +metadata
+ +Kubernetes meta/v1.ObjectMeta + +
+ +Refer to the Kubernetes API documentation for the fields of the +metadata field. + +
+ +status
+ GatewayStatus + +
+ +
+ +spec
+GatewaySpec + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +template
+ +Kubernetes core/v1.PodTemplateSpec + +
+ +

+ +Template is the pod specification for the gateway Refer +https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/\#pod-v1-core + +

+ +
+ +eventSourceRef
+ EventSourceRef + + +
+ +

+ +EventSourceRef refers to event-source that stores event source +configurations for the gateway + +

+ +
+ +type
Argo Events common.EventSourceType + +
+ +

+ +Type is the type of gateway. Used as metadata. + +

+ +
+ +service
+ +Kubernetes core/v1.Service + +
+ +

+ +Service is the specifications of the service to expose the gateway Refer +https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/\#service-v1-core + +

+ +
+ +watchers
+ +NotificationWatchers + +
+ +

+ +Watchers are components which are interested listening to notifications +from this gateway These only need to be specified when gateway dispatch +mechanism is through HTTP POST notifications. In future, support for +NATS, KAFKA will be added as a means to dispatch notifications in which +case specifying watchers would be unnecessary. + +

+ +
+ +processorPort
string + +
+ +

+ +Port on which the gateway event source processor is running on. + +

+ +
+ +eventProtocol
Argo Events common.EventProtocol + + +
+ +

+ +EventProtocol is the underlying protocol used to send events from +gateway to watchers(components interested in listening to event from +this gateway) + +

+ +
+ +replica
int + +
+ +

+ +Replica is the gateway deployment replicas + +

+ +
+ +
+ +

+ +GatewayNotificationWatcher + +

+ +

+ +(Appears on: +NotificationWatchers) + +

+ +

+ +

+ +GatewayNotificationWatcher is the gateway interested in listening to +notifications from this gateway + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name is the gateway name + +

+ +
+ +port
string + +
+ +

+ +Port is http server port on which gateway is running + +

+ +
+ +endpoint
string + +
+ +

+ +Endpoint is REST API endpoint to post event to. Events are sent using +HTTP POST method to this endpoint. + +

+ +
+ +namespace
string + +
+ +

+ +Namespace of the gateway + +

+ +
+ +

+ +GatewayResource + +

+ +

+ +(Appears on: +GatewayStatus) + +

+ +

+ +

+ +GatewayResource holds the metadata about the gateway resources + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +deployment
+ +Kubernetes meta/v1.ObjectMeta + +
+ +

+ +Metadata of the deployment for the gateway + +

+ +
+ +service
+ +Kubernetes meta/v1.ObjectMeta + +
+ +(Optional) + +

+ +Metadata of the service for the gateway + +

+ +
+ +

+ +GatewaySpec + +

+ +

+ +(Appears on: +Gateway) + +

+ +

+ +

+ +GatewaySpec represents gateway specifications + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +template
+ +Kubernetes core/v1.PodTemplateSpec + +
+ +

+ +Template is the pod specification for the gateway Refer +https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/\#pod-v1-core + +

+ +
+ +eventSourceRef
+ EventSourceRef + + +
+ +

+ +EventSourceRef refers to event-source that stores event source +configurations for the gateway + +

+ +
+ +type
Argo Events common.EventSourceType + +
+ +

+ +Type is the type of gateway. Used as metadata. + +

+ +
+ +service
+ +Kubernetes core/v1.Service + +
+ +

+ +Service is the specifications of the service to expose the gateway Refer +https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/\#service-v1-core + +

+ +
+ +watchers
+ +NotificationWatchers + +
+ +

+ +Watchers are components which are interested listening to notifications +from this gateway These only need to be specified when gateway dispatch +mechanism is through HTTP POST notifications. In future, support for +NATS, KAFKA will be added as a means to dispatch notifications in which +case specifying watchers would be unnecessary. + +

+ +
+ +processorPort
string + +
+ +

+ +Port on which the gateway event source processor is running on. + +

+ +
+ +eventProtocol
Argo Events common.EventProtocol + + +
+ +

+ +EventProtocol is the underlying protocol used to send events from +gateway to watchers(components interested in listening to event from +this gateway) + +

+ +
+ +replica
int + +
+ +

+ +Replica is the gateway deployment replicas + +

+ +
+ +

+ +GatewayStatus + +

+ +

+ +(Appears on: +Gateway) + +

+ +

+ +

+ +GatewayStatus contains information about the status of a gateway. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +phase
+NodePhase + +
+ +

+ +Phase is the high-level summary of the gateway + +

+ +
+ +startedAt
+ +Kubernetes meta/v1.Time + +
+ +

+ +StartedAt is the time at which this gateway was initiated + +

+ +
+ +message
string + +
+ +

+ +Message is a human readable string indicating details about a gateway in +its phase + +

+ +
+ +nodes
+map\[string\]github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NodeStatus + + +
+ +

+ +Nodes is a mapping between a node ID and the node’s status it records +the states for the configurations of gateway. + +

+ +
+ +resources
+ GatewayResource + + +
+ +

+ +Resources refers to the metadata about the gateway resources + +

+ +
+ +

+ +NodePhase (string alias) + +

+ +

+ +

+ +(Appears on: +GatewayStatus, +NodeStatus) + +

+ +

+ +

+ +NodePhase is the label for the condition of a node. + +

+ +

+ +

+ +NodeStatus + +

+ +

+ +(Appears on: +GatewayStatus) + +

+ +

+ +

+ +NodeStatus describes the status for an individual node in the gateway +configurations. A single node can represent one configuration. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +id
string + +
+ +

+ +ID is a unique identifier of a node within a sensor It is a hash of the +node name + +

+ +
+ +name
string + +
+ +

+ +Name is a unique name in the node tree used to generate the node ID + +

+ +
+ +displayName
string + +
+ +

+ +DisplayName is the human readable representation of the node + +

+ +
+ +phase
+NodePhase + +
+ +

+ +Phase of the node + +

+ +
+ +startedAt
+ +Kubernetes meta/v1.MicroTime + +
+ +

+ +StartedAt is the time at which this node started + +

+ +
+ +message
string + +
+ +

+ +Message store data or something to save for configuration + +

+ +
+ +updateTime
+ +Kubernetes meta/v1.MicroTime + +
+ +

+ +UpdateTime is the time when node(gateway configuration) was updated + +

+ +
+ +

+ +NotificationWatchers + +

+ +

+ +(Appears on: +GatewaySpec) + +

+ +

+ +

+ +NotificationWatchers are components which are interested listening to +notifications from this gateway + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +gateways
+ +\[\]GatewayNotificationWatcher + +
+ +

+ +Gateways is the list of gateways interested in listening to +notifications from this gateway + +

+ +
+ +sensors
+ +\[\]SensorNotificationWatcher + +
+ +

+ +Sensors is the list of sensors interested in listening to notifications +from this gateway + +

+ +
+ +

+ +SensorNotificationWatcher + +

+ +

+ +(Appears on: +NotificationWatchers) + +

+ +

+ +

+ +SensorNotificationWatcher is the sensor interested in listening to +notifications from this gateway + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name is the name of the sensor + +

+ +
+ +namespace
string + +
+ +

+ +Namespace of the sensor + +

+ +
+ +
+ +

+ + Generated with gen-crd-api-reference-docs on git +commit 8d85191. + +

diff --git a/api/generate.sh b/api/generate.sh new file mode 100644 index 0000000000..5c50365bfd --- /dev/null +++ b/api/generate.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +pandoc --from markdown --to gfm event-source.html > event-source.md +pandoc --from markdown --to gfm gateway.html > gateway.md +pandoc --from markdown --to gfm sensor.html > sensor.md diff --git a/api/sensor.html b/api/sensor.html new file mode 100644 index 0000000000..d82d21b636 --- /dev/null +++ b/api/sensor.html @@ -0,0 +1,1816 @@ +

Packages:

+ +

argoproj.io/v1alpha1

+

+

Package v1alpha1 is the v1alpha1 version of the API.

+

+Resource Types: + +

ArtifactLocation +

+

+(Appears on: +TriggerTemplate) +

+

+

ArtifactLocation describes the source location for an external minio

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+s3
+ +Argo Events common.S3Artifact + +
+

S3 compliant minio

+
+inline
+ +string + +
+

Inline minio is embedded in sensor spec as a string

+
+file
+ + +FileArtifact + + +
+

File minio is minio stored in a file

+
+url
+ + +URLArtifact + + +
+

URL to fetch the minio from

+
+configmap
+ + +ConfigmapArtifact + + +
+

Configmap that stores the minio

+
+git
+ + +GitArtifact + + +
+

Git repository hosting the minio

+
+resource
+ + +Kubernetes meta/v1/unstructured.Unstructured + + +
+

Resource is generic template for K8s resource

+
+

Backoff +

+

+(Appears on: +TriggerPolicy) +

+

+

Backoff for an operation

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+duration
+ +time.Duration + +
+

Duration is the duration in nanoseconds

+
+factor
+ +float64 + +
+

Duration is multiplied by factor each iteration

+
+jitter
+ +float64 + +
+

The amount of jitter applied each iteration

+
+steps
+ +int + +
+

Exit with error after this many steps

+
+

ConfigmapArtifact +

+

+(Appears on: +ArtifactLocation) +

+

+

ConfigmapArtifact contains information about minio in k8 configmap

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the configmap

+
+namespace
+ +string + +
+

Namespace where configmap is deployed

+
+key
+ +string + +
+

Key within configmap data which contains trigger resource definition

+
+

DataFilter +

+

+(Appears on: +EventDependencyFilter) +

+

+

DataFilter describes constraints and filters for event data +Regular Expressions are purposefully not a feature as they are overkill for our uses here +See Rob Pike’s Post: https://commandcenter.blogspot.com/2011/08/regular-expressions-in-lexing-and.html

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+path
+ +string + +
+

Path is the JSONPath of the event’s (JSON decoded) data key +Path is a series of keys separated by a dot. A key may contain wildcard characters ‘*’ and ‘?’. +To access an array value use the index as the key. The dot and wildcard characters can be escaped with ‘\’. +See https://github.com/tidwall/gjson#path-syntax for more information on how to use this.

+
+type
+ + +JSONType + + +
+

Type contains the JSON type of the data

+
+value
+ +[]string + +
+

Value is the allowed string values for this key +Booleans are passed using strconv.ParseBool() +Numbers are parsed using as float64 using strconv.ParseFloat() +Strings are taken as is +Nils this value is ignored

+
+

DependencyGroup +

+

+(Appears on: +SensorSpec) +

+

+

DependencyGroup is the group of dependencies

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the group

+
+dependencies
+ +[]string + +
+

Dependencies of events

+
+

EventDependency +

+

+(Appears on: +SensorSpec) +

+

+

EventDependency describes a dependency

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name is a unique name of this dependency

+
+filters
+ + +EventDependencyFilter + + +
+

Filters and rules governing tolerations of success and constraints on the context and data of an event

+
+connected
+ +bool + +
+

Connected tells if subscription is already setup in case of nats protocol.

+
+

EventDependencyFilter +

+

+(Appears on: +EventDependency) +

+

+

EventDependencyFilter defines filters and constraints for a event.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name is the name of event filter

+
+time
+ + +TimeFilter + + +
+

Time filter on the event with escalation

+
+context
+ +Argo Events common.EventContext + +
+

Context filter constraints with escalation

+
+data
+ + +[]DataFilter + + +
+

Data filter constraints with escalation

+
+

FileArtifact +

+

+(Appears on: +ArtifactLocation) +

+

+

FileArtifact contains information about an minio in a filesystem

+

+ + + + + + + + + + + + + +
FieldDescription
+path
+ +string + +
+
+

GitArtifact +

+

+(Appears on: +ArtifactLocation) +

+

+

GitArtifact contains information about an minio stored in git

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+url
+ +string + +
+

Git URL

+
+cloneDirectory
+ +string + +
+

Directory to clone the repository. We clone complete directory because GitArtifact is not limited to any specific Git service providers. +Hence we don’t use any specific git provider client.

+
+creds
+ + +GitCreds + + +
+(Optional) +

Creds contain reference to git username and password

+
+namespace
+ +string + +
+(Optional) +

Namespace where creds are stored.

+
+sshKeyPath
+ +string + +
+(Optional) +

SSHKeyPath is path to your ssh key path. Use this if you don’t want to provide username and password. +ssh key path must be mounted in sensor pod.

+
+filePath
+ +string + +
+

Path to file that contains trigger resource definition

+
+branch
+ +string + +
+(Optional) +

Branch to use to pull trigger resource

+
+tag
+ +string + +
+(Optional) +

Tag to use to pull trigger resource

+
+ref
+ +string + +
+(Optional) +

Ref to use to pull trigger resource. Will result in a shallow clone and +fetch.

+
+remote
+ + +GitRemoteConfig + + +
+(Optional) +

Remote to manage set of tracked repositories. Defaults to “origin”. +Refer https://git-scm.com/docs/git-remote

+
+

GitCreds +

+

+(Appears on: +GitArtifact) +

+

+

GitCreds contain reference to git username and password

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+username
+ + +Kubernetes core/v1.SecretKeySelector + + +
+
+password
+ + +Kubernetes core/v1.SecretKeySelector + + +
+
+

GitRemoteConfig +

+

+(Appears on: +GitArtifact) +

+

+

GitRemoteConfig contains the configuration of a Git remote

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the remote to fetch from.

+
+urls
+ +[]string + +
+

URLs the URLs of a remote repository. It must be non-empty. Fetch will +always use the first URL, while push will use all of them.

+
+

JSONType +(string alias)

+

+(Appears on: +DataFilter) +

+

+

JSONType contains the supported JSON types for data filtering

+

+

NodePhase +(string alias)

+

+(Appears on: +NodeStatus, +SensorStatus) +

+

+

NodePhase is the label for the condition of a node

+

+

NodeStatus +

+

+(Appears on: +SensorStatus) +

+

+

NodeStatus describes the status for an individual node in the sensor’s FSM. +A single node can represent the status for event or a trigger.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+id
+ +string + +
+

ID is a unique identifier of a node within a sensor +It is a hash of the node name

+
+name
+ +string + +
+

Name is a unique name in the node tree used to generate the node ID

+
+displayName
+ +string + +
+

DisplayName is the human readable representation of the node

+
+type
+ + +NodeType + + +
+

Type is the type of the node

+
+phase
+ + +NodePhase + + +
+

Phase of the node

+
+startedAt
+ + +Kubernetes meta/v1.MicroTime + + +
+

StartedAt is the time at which this node started

+
+completedAt
+ + +Kubernetes meta/v1.MicroTime + + +
+

CompletedAt is the time at which this node completed

+
+message
+ +string + +
+

store data or something to save for event notifications or trigger events

+
+event
+ +Argo Events common.Event + +
+

Event stores the last seen event for this node

+
+

NodeType +(string alias)

+

+(Appears on: +NodeStatus) +

+

+

NodeType is the type of a node

+

+

NotificationType +(string alias)

+

+

NotificationType represent a type of notifications that are handled by a sensor

+

+

Sensor +

+

+

Sensor is the definition of a sensor resource

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +SensorSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+dependencies
+ + +[]EventDependency + + +
+

Dependencies is a list of the events that this sensor is dependent on.

+
+triggers
+ + +[]Trigger + + +
+

Triggers is a list of the things that this sensor evokes. These are the outputs from this sensor.

+
+template
+ + +Kubernetes core/v1.PodTemplateSpec + + +
+

Template contains sensor pod specification. For more information, read https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core

+
+eventProtocol
+ +Argo Events common.EventProtocol + +
+

EventProtocol is the protocol through which sensor receives events from gateway

+
+circuit
+ +string + +
+

Circuit is a boolean expression of dependency groups

+
+dependencyGroups
+ + +[]DependencyGroup + + +
+

DependencyGroups is a list of the groups of events.

+
+errorOnFailedRound
+ +bool + +
+

ErrorOnFailedRound if set to true, marks sensor state as error if the previous trigger round fails. +Once sensor state is set to error, no further triggers will be processed.

+
+
+status
+ + +SensorStatus + + +
+
+

SensorResources +

+

+(Appears on: +SensorStatus) +

+

+

SensorResources holds the metadata of the resources created for the sensor

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+deployment
+ + +Kubernetes meta/v1.ObjectMeta + + +
+

Deployment holds the metadata of the deployment for the sensor

+
+service
+ + +Kubernetes meta/v1.ObjectMeta + + +
+(Optional) +

Service holds the metadata of the service for the sensor

+
+

SensorSpec +

+

+(Appears on: +Sensor) +

+

+

SensorSpec represents desired sensor state

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+dependencies
+ + +[]EventDependency + + +
+

Dependencies is a list of the events that this sensor is dependent on.

+
+triggers
+ + +[]Trigger + + +
+

Triggers is a list of the things that this sensor evokes. These are the outputs from this sensor.

+
+template
+ + +Kubernetes core/v1.PodTemplateSpec + + +
+

Template contains sensor pod specification. For more information, read https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core

+
+eventProtocol
+ +Argo Events common.EventProtocol + +
+

EventProtocol is the protocol through which sensor receives events from gateway

+
+circuit
+ +string + +
+

Circuit is a boolean expression of dependency groups

+
+dependencyGroups
+ + +[]DependencyGroup + + +
+

DependencyGroups is a list of the groups of events.

+
+errorOnFailedRound
+ +bool + +
+

ErrorOnFailedRound if set to true, marks sensor state as error if the previous trigger round fails. +Once sensor state is set to error, no further triggers will be processed.

+
+

SensorStatus +

+

+(Appears on: +Sensor) +

+

+

SensorStatus contains information about the status of a sensor.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +NodePhase + + +
+

Phase is the high-level summary of the sensor

+
+startedAt
+ + +Kubernetes meta/v1.Time + + +
+

StartedAt is the time at which this sensor was initiated

+
+completedAt
+ + +Kubernetes meta/v1.Time + + +
+

CompletedAt is the time at which this sensor was completed

+
+message
+ +string + +
+

Message is a human readable string indicating details about a sensor in its phase

+
+nodes
+ + +map[string]github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.NodeStatus + + +
+

Nodes is a mapping between a node ID and the node’s status +it records the states for the FSM of this sensor.

+
+triggerCycleCount
+ +int32 + +
+

TriggerCycleCount is the count of sensor’s trigger cycle runs.

+
+triggerCycleStatus
+ + +TriggerCycleState + + +
+

TriggerCycleState is the status from last cycle of triggers execution.

+
+lastCycleTime
+ + +Kubernetes meta/v1.Time + + +
+

LastCycleTime is the time when last trigger cycle completed

+
+resources
+ + +SensorResources + + +
+

Resources refers to metadata of the resources created for the sensor

+
+

TimeFilter +

+

+(Appears on: +EventDependencyFilter) +

+

+

TimeFilter describes a window in time. +DataFilters out event events that occur outside the time limits. +In other words, only events that occur after Start and before Stop +will pass this filter.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+start
+ +string + +
+

Start is the beginning of a time window. +Before this time, events for this event are ignored and +format is hh:mm:ss

+
+stop
+ +string + +
+

StopPattern is the end of a time window. +After this time, events for this event are ignored and +format is hh:mm:ss

+
+

Trigger +

+

+(Appears on: +SensorSpec) +

+

+

Trigger is an action taken, output produced, an event created, a message sent

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+template
+ + +TriggerTemplate + + +
+

Template describes the trigger specification.

+
+templateParameters
+ + +[]TriggerParameter + + +
+

TemplateParameters is the list of resource parameters to pass to the template object

+
+resourceParameters
+ + +[]TriggerParameter + + +
+

ResourceParameters is the list of resource parameters to pass to resolved resource object in template object

+
+policy
+ + +TriggerPolicy + + +
+

Policy to configure backoff and execution criteria for the trigger

+
+

TriggerCondition +

+

+(Appears on: +TriggerTemplate) +

+

+

TriggerCondition describes condition which must be satisfied in order to execute a trigger. +Depending upon condition type, status of dependency groups is used to evaluate the result.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+any
+ +[]string + +
+

Any acts as a OR operator between dependencies

+
+all
+ +[]string + +
+

All acts as a AND operator between dependencies

+
+

TriggerCycleState +(string alias)

+

+(Appears on: +SensorStatus) +

+

+

TriggerCycleState is the label for the state of the trigger cycle

+

+

TriggerParameter +

+

+(Appears on: +Trigger) +

+

+

TriggerParameter indicates a passed parameter to a service template

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+src
+ + +TriggerParameterSource + + +
+

Src contains a source reference to the value of the parameter from a event event

+
+dest
+ +string + +
+

Dest is the JSONPath of a resource key. +A path is a series of keys separated by a dot. The colon character can be escaped with ‘.’ +The -1 key can be used to append a value to an existing array. +See https://github.com/tidwall/sjson#path-syntax for more information about how this is used.

+
+operation
+ + +TriggerParameterOperation + + +
+

Operation is what to do with the existing value at Dest, whether to +‘prepend’, ‘overwrite’, or ‘append’ it.

+
+

TriggerParameterOperation +(string alias)

+

+(Appears on: +TriggerParameter) +

+

+

TriggerParameterOperation represents how to set a trigger destination +resource key

+

+

TriggerParameterSource +

+

+(Appears on: +TriggerParameter) +

+

+

TriggerParameterSource defines the source for a parameter from a event event

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+event
+ +string + +
+

Event is the name of the event for which to retrieve this event

+
+path
+ +string + +
+

Path is the JSONPath of the event’s (JSON decoded) data key +Path is a series of keys separated by a dot. A key may contain wildcard characters ‘*’ and ‘?’. +To access an array value use the index as the key. The dot and wildcard characters can be escaped with ‘\’. +See https://github.com/tidwall/gjson#path-syntax for more information on how to use this.

+
+value
+ +string + +
+

Value is the default literal value to use for this parameter source +This is only used if the path is invalid. +If the path is invalid and this is not defined, this param source will produce an error.

+
+

TriggerPolicy +

+

+(Appears on: +Trigger) +

+

+

TriggerPolicy dictates the policy for the trigger retries

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+backoff
+ + +Backoff + + +
+

Backoff before checking resource state

+
+state
+ + +TriggerStateLabels + + +
+

State refers to labels used to check the resource state

+
+errorOnBackoffTimeout
+ +bool + +
+

ErrorOnBackoffTimeout determines whether sensor should transition to error state if the backoff times out and yet the resource neither transitioned into success or failure.

+
+

TriggerStateLabels +

+

+(Appears on: +TriggerPolicy) +

+

+

TriggerStateLabels defines the labels used to decide if a resource is in success or failure state.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+success
+ +map[string]string + +
+

Success defines labels required to identify a resource in success state

+
+failure
+ +map[string]string + +
+

Failure defines labels required to identify a resource in failed state

+
+

TriggerTemplate +

+

+(Appears on: +Trigger) +

+

+

TriggerTemplate is the template that describes trigger specification.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name is a unique name of the action to take

+
+when
+ + +TriggerCondition + + +
+

When is the condition to execute the trigger

+
+GroupVersionResource
+ + +Kubernetes meta/v1.GroupVersionResource + + +
+

+(Members of GroupVersionResource are embedded into this type.) +

+

The unambiguous kind of this object - used in order to retrieve the appropriate kubernetes api client for this resource

+
+source
+ + +ArtifactLocation + + +
+

Source of the K8 resource file(s)

+
+

URLArtifact +

+

+(Appears on: +ArtifactLocation) +

+

+

URLArtifact contains information about an minio at an http endpoint.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+path
+ +string + +
+

Path is the complete URL

+
+verifyCert
+ +bool + +
+

VerifyCert decides whether the connection is secure or not

+
+
+

+Generated with gen-crd-api-reference-docs +on git commit 8d85191. +

diff --git a/api/sensor.md b/api/sensor.md new file mode 100644 index 0000000000..bef98f80db --- /dev/null +++ b/api/sensor.md @@ -0,0 +1,3661 @@ +

+ +Packages: + +

+ + + +

+ +argoproj.io/v1alpha1 + +

+ +

+ +

+ +Package v1alpha1 is the v1alpha1 version of the API. + +

+ +

+ +Resource Types: + + + +

+ +ArtifactLocation + +

+ +

+ +(Appears on: +TriggerTemplate) + +

+ +

+ +

+ +ArtifactLocation describes the source location for an external minio + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +s3
Argo Events common.S3Artifact + +
+ +

+ +S3 compliant minio + +

+ +
+ +inline
string + +
+ +

+ +Inline minio is embedded in sensor spec as a string + +

+ +
+ +file
+ FileArtifact + +
+ +

+ +File minio is minio stored in a file + +

+ +
+ +url
+URLArtifact + +
+ +

+ +URL to fetch the minio from + +

+ +
+ +configmap
+ ConfigmapArtifact + + +
+ +

+ +Configmap that stores the minio + +

+ +
+ +git
+GitArtifact + +
+ +

+ +Git repository hosting the minio + +

+ +
+ +resource
+ +Kubernetes meta/v1/unstructured.Unstructured + +
+ +

+ +Resource is generic template for K8s resource + +

+ +
+ +

+ +Backoff + +

+ +

+ +(Appears on: +TriggerPolicy) + +

+ +

+ +

+ +Backoff for an operation + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +duration
time.Duration + +
+ +

+ +Duration is the duration in nanoseconds + +

+ +
+ +factor
float64 + +
+ +

+ +Duration is multiplied by factor each iteration + +

+ +
+ +jitter
float64 + +
+ +

+ +The amount of jitter applied each iteration + +

+ +
+ +steps
int + +
+ +

+ +Exit with error after this many steps + +

+ +
+ +

+ +ConfigmapArtifact + +

+ +

+ +(Appears on: +ArtifactLocation) + +

+ +

+ +

+ +ConfigmapArtifact contains information about minio in k8 configmap + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name of the configmap + +

+ +
+ +namespace
string + +
+ +

+ +Namespace where configmap is deployed + +

+ +
+ +key
string + +
+ +

+ +Key within configmap data which contains trigger resource definition + +

+ +
+ +

+ +DataFilter + +

+ +

+ +(Appears on: +EventDependencyFilter) + +

+ +

+ +

+ +DataFilter describes constraints and filters for event data Regular +Expressions are purposefully not a feature as they are overkill for our +uses here See Rob Pike’s Post: +https://commandcenter.blogspot.com/2011/08/regular-expressions-in-lexing-and.html + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +path
string + +
+ +

+ +Path is the JSONPath of the event’s (JSON decoded) data key Path is a +series of keys separated by a dot. A key may contain wildcard characters +‘\*’ and ‘?’. To access an array value use the index as the key. The dot +and wildcard characters can be escaped with ‘\’. See +https://github.com/tidwall/gjson\#path-syntax +for more information on how to use this. + +

+ +
+ +type
+JSONType + +
+ +

+ +Type contains the JSON type of the data + +

+ +
+ +value
\[\]string + +
+ +

+ +Value is the allowed string values for this key Booleans are passed +using strconv.ParseBool() Numbers are parsed using as float64 using +strconv.ParseFloat() Strings are taken as is Nils this value is ignored + +

+ +
+ +

+ +DependencyGroup + +

+ +

+ +(Appears on: +SensorSpec) + +

+ +

+ +

+ +DependencyGroup is the group of dependencies + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name of the group + +

+ +
+ +dependencies
\[\]string + +
+ +

+ +Dependencies of events + +

+ +
+ +

+ +EventDependency + +

+ +

+ +(Appears on: +SensorSpec) + +

+ +

+ +

+ +EventDependency describes a dependency + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name is a unique name of this dependency + +

+ +
+ +filters
+ +EventDependencyFilter + +
+ +

+ +Filters and rules governing tolerations of success and constraints on +the context and data of an event + +

+ +
+ +connected
bool + +
+ +

+ +Connected tells if subscription is already setup in case of nats +protocol. + +

+ +
+ +

+ +EventDependencyFilter + +

+ +

+ +(Appears on: +EventDependency) + +

+ +

+ +

+ +EventDependencyFilter defines filters and constraints for a event. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name is the name of event filter + +

+ +
+ +time
+TimeFilter + +
+ +

+ +Time filter on the event with escalation + +

+ +
+ +context
Argo Events common.EventContext + +
+ +

+ +Context filter constraints with escalation + +

+ +
+ +data
+\[\]DataFilter + +
+ +

+ +Data filter constraints with escalation + +

+ +
+ +

+ +FileArtifact + +

+ +

+ +(Appears on: +ArtifactLocation) + +

+ +

+ +

+ +FileArtifact contains information about an minio in a filesystem + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +path
string + +
+ +
+ +

+ +GitArtifact + +

+ +

+ +(Appears on: +ArtifactLocation) + +

+ +

+ +

+ +GitArtifact contains information about an minio stored in git + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +url
string + +
+ +

+ +Git URL + +

+ +
+ +cloneDirectory
string + +
+ +

+ +Directory to clone the repository. We clone complete directory because +GitArtifact is not limited to any specific Git service providers. Hence +we don’t use any specific git provider client. + +

+ +
+ +creds
+GitCreds + +
+ +(Optional) + +

+ +Creds contain reference to git username and password + +

+ +
+ +namespace
string + +
+ +(Optional) + +

+ +Namespace where creds are stored. + +

+ +
+ +sshKeyPath
string + +
+ +(Optional) + +

+ +SSHKeyPath is path to your ssh key path. Use this if you don’t want to +provide username and password. ssh key path must be mounted in sensor +pod. + +

+ +
+ +filePath
string + +
+ +

+ +Path to file that contains trigger resource definition + +

+ +
+ +branch
string + +
+ +(Optional) + +

+ +Branch to use to pull trigger resource + +

+ +
+ +tag
string + +
+ +(Optional) + +

+ +Tag to use to pull trigger resource + +

+ +
+ +ref
string + +
+ +(Optional) + +

+ +Ref to use to pull trigger resource. Will result in a shallow clone and +fetch. + +

+ +
+ +remote
+ GitRemoteConfig + + +
+ +(Optional) + +

+ +Remote to manage set of tracked repositories. Defaults to “origin”. +Refer +https://git-scm.com/docs/git-remote + +

+ +
+ +

+ +GitCreds + +

+ +

+ +(Appears on: +GitArtifact) + +

+ +

+ +

+ +GitCreds contain reference to git username and password + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +username
+ +Kubernetes core/v1.SecretKeySelector + +
+ +
+ +password
+ +Kubernetes core/v1.SecretKeySelector + +
+ +
+ +

+ +GitRemoteConfig + +

+ +

+ +(Appears on: +GitArtifact) + +

+ +

+ +

+ +GitRemoteConfig contains the configuration of a Git remote + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name of the remote to fetch from. + +

+ +
+ +urls
\[\]string + +
+ +

+ +URLs the URLs of a remote repository. It must be non-empty. Fetch will +always use the first URL, while push will use all of them. + +

+ +
+ +

+ +JSONType (string alias) + +

+ +

+ +

+ +(Appears on: +DataFilter) + +

+ +

+ +

+ +JSONType contains the supported JSON types for data filtering + +

+ +

+ +

+ +NodePhase (string alias) + +

+ +

+ +

+ +(Appears on: +NodeStatus, +SensorStatus) + +

+ +

+ +

+ +NodePhase is the label for the condition of a node + +

+ +

+ +

+ +NodeStatus + +

+ +

+ +(Appears on: +SensorStatus) + +

+ +

+ +

+ +NodeStatus describes the status for an individual node in the sensor’s +FSM. A single node can represent the status for event or a trigger. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +id
string + +
+ +

+ +ID is a unique identifier of a node within a sensor It is a hash of the +node name + +

+ +
+ +name
string + +
+ +

+ +Name is a unique name in the node tree used to generate the node ID + +

+ +
+ +displayName
string + +
+ +

+ +DisplayName is the human readable representation of the node + +

+ +
+ +type
+NodeType + +
+ +

+ +Type is the type of the node + +

+ +
+ +phase
+NodePhase + +
+ +

+ +Phase of the node + +

+ +
+ +startedAt
+ +Kubernetes meta/v1.MicroTime + +
+ +

+ +StartedAt is the time at which this node started + +

+ +
+ +completedAt
+ +Kubernetes meta/v1.MicroTime + +
+ +

+ +CompletedAt is the time at which this node completed + +

+ +
+ +message
string + +
+ +

+ +store data or something to save for event notifications or trigger +events + +

+ +
+ +event
Argo Events common.Event + +
+ +

+ +Event stores the last seen event for this node + +

+ +
+ +

+ +NodeType (string alias) + +

+ +

+ +

+ +(Appears on: +NodeStatus) + +

+ +

+ +

+ +NodeType is the type of a node + +

+ +

+ +

+ +NotificationType (string alias) + +

+ +

+ +

+ +

+ +NotificationType represent a type of notifications that are handled by a +sensor + +

+ +

+ +

+ +Sensor + +

+ +

+ +

+ +Sensor is the definition of a sensor resource + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +metadata
+ +Kubernetes meta/v1.ObjectMeta + +
+ +Refer to the Kubernetes API documentation for the fields of the +metadata field. + +
+ +spec
+SensorSpec + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +dependencies
+ \[\]EventDependency + + +
+ +

+ +Dependencies is a list of the events that this sensor is dependent on. + +

+ +
+ +triggers
+\[\]Trigger + +
+ +

+ +Triggers is a list of the things that this sensor evokes. These are the +outputs from this sensor. + +

+ +
+ +template
+ +Kubernetes core/v1.PodTemplateSpec + +
+ +

+ +Template contains sensor pod specification. For more information, read +https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/\#pod-v1-core + +

+ +
+ +eventProtocol
Argo Events common.EventProtocol + + +
+ +

+ +EventProtocol is the protocol through which sensor receives events from +gateway + +

+ +
+ +circuit
string + +
+ +

+ +Circuit is a boolean expression of dependency groups + +

+ +
+ +dependencyGroups
+ \[\]DependencyGroup + + +
+ +

+ +DependencyGroups is a list of the groups of events. + +

+ +
+ +errorOnFailedRound
bool + +
+ +

+ +ErrorOnFailedRound if set to true, marks sensor state as +error if the previous trigger round fails. Once sensor +state is set to error, no further triggers will be +processed. + +

+ +
+ +
+ +status
+ SensorStatus + +
+ +
+ +

+ +SensorResources + +

+ +

+ +(Appears on: +SensorStatus) + +

+ +

+ +

+ +SensorResources holds the metadata of the resources created for the +sensor + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +deployment
+ +Kubernetes meta/v1.ObjectMeta + +
+ +

+ +Deployment holds the metadata of the deployment for the sensor + +

+ +
+ +service
+ +Kubernetes meta/v1.ObjectMeta + +
+ +(Optional) + +

+ +Service holds the metadata of the service for the sensor + +

+ +
+ +

+ +SensorSpec + +

+ +

+ +(Appears on: Sensor) + +

+ +

+ +

+ +SensorSpec represents desired sensor state + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +dependencies
+ \[\]EventDependency + + +
+ +

+ +Dependencies is a list of the events that this sensor is dependent on. + +

+ +
+ +triggers
+\[\]Trigger + +
+ +

+ +Triggers is a list of the things that this sensor evokes. These are the +outputs from this sensor. + +

+ +
+ +template
+ +Kubernetes core/v1.PodTemplateSpec + +
+ +

+ +Template contains sensor pod specification. For more information, read +https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/\#pod-v1-core + +

+ +
+ +eventProtocol
Argo Events common.EventProtocol + + +
+ +

+ +EventProtocol is the protocol through which sensor receives events from +gateway + +

+ +
+ +circuit
string + +
+ +

+ +Circuit is a boolean expression of dependency groups + +

+ +
+ +dependencyGroups
+ \[\]DependencyGroup + + +
+ +

+ +DependencyGroups is a list of the groups of events. + +

+ +
+ +errorOnFailedRound
bool + +
+ +

+ +ErrorOnFailedRound if set to true, marks sensor state as +error if the previous trigger round fails. Once sensor +state is set to error, no further triggers will be +processed. + +

+ +
+ +

+ +SensorStatus + +

+ +

+ +(Appears on: Sensor) + +

+ +

+ +

+ +SensorStatus contains information about the status of a sensor. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +phase
+NodePhase + +
+ +

+ +Phase is the high-level summary of the sensor + +

+ +
+ +startedAt
+ +Kubernetes meta/v1.Time + +
+ +

+ +StartedAt is the time at which this sensor was initiated + +

+ +
+ +completedAt
+ +Kubernetes meta/v1.Time + +
+ +

+ +CompletedAt is the time at which this sensor was completed + +

+ +
+ +message
string + +
+ +

+ +Message is a human readable string indicating details about a sensor in +its phase + +

+ +
+ +nodes
+map\[string\]github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.NodeStatus + + +
+ +

+ +Nodes is a mapping between a node ID and the node’s status it records +the states for the FSM of this sensor. + +

+ +
+ +triggerCycleCount
int32 + +
+ +

+ +TriggerCycleCount is the count of sensor’s trigger cycle runs. + +

+ +
+ +triggerCycleStatus
+ TriggerCycleState + + +
+ +

+ +TriggerCycleState is the status from last cycle of triggers execution. + +

+ +
+ +lastCycleTime
+ +Kubernetes meta/v1.Time + +
+ +

+ +LastCycleTime is the time when last trigger cycle completed + +

+ +
+ +resources
+ SensorResources + + +
+ +

+ +Resources refers to metadata of the resources created for the sensor + +

+ +
+ +

+ +TimeFilter + +

+ +

+ +(Appears on: +EventDependencyFilter) + +

+ +

+ +

+ +TimeFilter describes a window in time. DataFilters out event events that +occur outside the time limits. In other words, only events that occur +after Start and before Stop will pass this filter. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +start
string + +
+ +

+ +Start is the beginning of a time window. Before this time, events for +this event are ignored and format is hh:mm:ss + +

+ +
+ +stop
string + +
+ +

+ +StopPattern is the end of a time window. After this time, events for +this event are ignored and format is hh:mm:ss + +

+ +
+ +

+ +Trigger + +

+ +

+ +(Appears on: +SensorSpec) + +

+ +

+ +

+ +Trigger is an action taken, output produced, an event created, a message +sent + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +template
+ TriggerTemplate + + +
+ +

+ +Template describes the trigger specification. + +

+ +
+ +templateParameters
+ \[\]TriggerParameter + + +
+ +

+ +TemplateParameters is the list of resource parameters to pass to the +template object + +

+ +
+ +resourceParameters
+ \[\]TriggerParameter + + +
+ +

+ +ResourceParameters is the list of resource parameters to pass to +resolved resource object in template object + +

+ +
+ +policy
+ TriggerPolicy + +
+ +

+ +Policy to configure backoff and execution criteria for the trigger + +

+ +
+ +

+ +TriggerCondition + +

+ +

+ +(Appears on: +TriggerTemplate) + +

+ +

+ +

+ +TriggerCondition describes condition which must be satisfied in order to +execute a trigger. Depending upon condition type, status of dependency +groups is used to evaluate the result. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +any
\[\]string + +
+ +

+ +Any acts as a OR operator between dependencies + +

+ +
+ +all
\[\]string + +
+ +

+ +All acts as a AND operator between dependencies + +

+ +
+ +

+ +TriggerCycleState (string alias) + +

+ +

+ +

+ +(Appears on: +SensorStatus) + +

+ +

+ +

+ +TriggerCycleState is the label for the state of the trigger cycle + +

+ +

+ +

+ +TriggerParameter + +

+ +

+ +(Appears on: +Trigger) + +

+ +

+ +

+ +TriggerParameter indicates a passed parameter to a service template + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +src
+ +TriggerParameterSource + +
+ +

+ +Src contains a source reference to the value of the parameter from a +event event + +

+ +
+ +dest
string + +
+ +

+ +Dest is the JSONPath of a resource key. A path is a series of keys +separated by a dot. The colon character can be escaped with ‘.’ The -1 +key can be used to append a value to an existing array. See +https://github.com/tidwall/sjson\#path-syntax +for more information about how this is used. + +

+ +
+ +operation
+ +TriggerParameterOperation + +
+ +

+ +Operation is what to do with the existing value at Dest, whether to +‘prepend’, ‘overwrite’, or ‘append’ it. + +

+ +
+ +

+ +TriggerParameterOperation (string alias) + +

+ +

+ +

+ +(Appears on: +TriggerParameter) + +

+ +

+ +

+ +TriggerParameterOperation represents how to set a trigger destination +resource key + +

+ +

+ +

+ +TriggerParameterSource + +

+ +

+ +(Appears on: +TriggerParameter) + +

+ +

+ +

+ +TriggerParameterSource defines the source for a parameter from a event +event + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +event
string + +
+ +

+ +Event is the name of the event for which to retrieve this event + +

+ +
+ +path
string + +
+ +

+ +Path is the JSONPath of the event’s (JSON decoded) data key Path is a +series of keys separated by a dot. A key may contain wildcard characters +‘\*’ and ‘?’. To access an array value use the index as the key. The dot +and wildcard characters can be escaped with ‘\’. See +https://github.com/tidwall/gjson\#path-syntax +for more information on how to use this. + +

+ +
+ +value
string + +
+ +

+ +Value is the default literal value to use for this parameter source This +is only used if the path is invalid. If the path is invalid and this is +not defined, this param source will produce an error. + +

+ +
+ +

+ +TriggerPolicy + +

+ +

+ +(Appears on: +Trigger) + +

+ +

+ +

+ +TriggerPolicy dictates the policy for the trigger retries + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +backoff
+Backoff + +
+ +

+ +Backoff before checking resource state + +

+ +
+ +state
+ TriggerStateLabels + + +
+ +

+ +State refers to labels used to check the resource state + +

+ +
+ +errorOnBackoffTimeout
bool + +
+ +

+ +ErrorOnBackoffTimeout determines whether sensor should transition to +error state if the backoff times out and yet the resource neither +transitioned into success or failure. + +

+ +
+ +

+ +TriggerStateLabels + +

+ +

+ +(Appears on: +TriggerPolicy) + +

+ +

+ +

+ +TriggerStateLabels defines the labels used to decide if a resource is in +success or failure state. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +success
map\[string\]string + +
+ +

+ +Success defines labels required to identify a resource in success state + +

+ +
+ +failure
map\[string\]string + +
+ +

+ +Failure defines labels required to identify a resource in failed state + +

+ +
+ +

+ +TriggerTemplate + +

+ +

+ +(Appears on: +Trigger) + +

+ +

+ +

+ +TriggerTemplate is the template that describes trigger specification. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +name
string + +
+ +

+ +Name is a unique name of the action to take + +

+ +
+ +when
+ TriggerCondition + + +
+ +

+ +When is the condition to execute the trigger + +

+ +
+ +GroupVersionResource
+ +Kubernetes meta/v1.GroupVersionResource + +
+ +

+ +(Members of GroupVersionResource are embedded into this +type.) + +

+ +

+ +The unambiguous kind of this object - used in order to retrieve the +appropriate kubernetes api client for this resource + +

+ +
+ +source
+ ArtifactLocation + + +
+ +

+ +Source of the K8 resource file(s) + +

+ +
+ +

+ +URLArtifact + +

+ +

+ +(Appears on: +ArtifactLocation) + +

+ +

+ +

+ +URLArtifact contains information about an minio at an http endpoint. + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Field + + + +Description + +
+ +path
string + +
+ +

+ +Path is the complete URL + +

+ +
+ +verifyCert
bool + +
+ +

+ +VerifyCert decides whether the connection is secure or not + +

+ +
+ +
+ +

+ + Generated with gen-crd-api-reference-docs on git +commit 8d85191. + +

diff --git a/common/common.go b/common/common.go index 305ec1a5d9..06d166e88f 100644 --- a/common/common.go +++ b/common/common.go @@ -17,156 +17,106 @@ limitations under the License. package common import ( - "github.com/argoproj/argo-events/pkg/apis/gateway" - "github.com/argoproj/argo-events/pkg/apis/sensor" + "github.com/pkg/errors" ) +// Defaults const ( - // ErrorResponse for http request ErrorResponse = "Error" - // StandardTimeFormat is time format reference for golang StandardTimeFormat = "2006-01-02 15:04:05" - // StandardYYYYMMDDFormat formats date in yyyy-mm-dd format StandardYYYYMMDDFormat = "2006-01-02" - // DefaultControllerNamespace is the default namespace where the sensor and gateways controllers are installed DefaultControllerNamespace = "argo-events" ) -// ENV VARS +// Environment variables const ( // EnvVarKubeConfig is the path to the Kubernetes configuration EnvVarKubeConfig = "KUBE_CONFIG" - // EnvVarDebugLog is the env var to turn on the debug mode for logging EnvVarDebugLog = "DEBUG_LOG" ) -// LABELS +// Controller environment variables const ( - // LabelOperation is a label for an operation in framework - LabelOperation = "operation" - - // LabelEventSource is label for event name - LabelEventSource = "event-source" + // EnvVarControllerConfigMap contains name of the configmap to retrieve controller configuration from + EnvVarControllerConfigMap = "CONTROLLER_CONFIG_MAP" + // EnvVarControllerInstanceID is used to get controller instance id + EnvVarControllerInstanceID = "CONTROLLER_INSTANCE_ID" + // EnvVarControllerName is used to get name of the controller + EnvVarControllerName = "CONTROLLER_NAME" + // EnvVarResourceName refers env var for name of the resource + EnvVarResourceName = "NAME" + // EnvVarNamespace refers to a K8s namespace + EnvVarNamespace = "NAMESPACE" ) -// SENSOR CONTROLLER CONSTANTS +// Controller labels const ( - // env variables constants - //LabelKeySensorControllerInstanceID is the label which allows to separate application among multiple running sensor controllers. - LabelKeySensorControllerInstanceID = sensor.FullName + "/sensor-controller-instanceid" - - // LabelSensorKeyPhase is a label applied to sensors to indicate the current phase of the sensor (for filtering purposes) - LabelSensorKeyPhase = sensor.FullName + "/phase" - - // LabelSensorKeyComplete is the label to mark sensors as complete - LabelSensorKeyComplete = sensor.FullName + "/complete" - - // EnvVarSensorControllerConfigMap is the name of the configmap to use for the sensor-controller - EnvVarSensorControllerConfigMap = "SENSOR_CONFIG_MAP" - - // labels constants - // LabelSensorControllerName is the default deployment name of the sensor-controller - LabelSensorControllerName = "sensor-controller" - - LabelArgoEventsSensorVersion = "argo-events-sensor-version" + // LabelGatewayName is the label for the K8s resource name + LabelResourceName = "resource-name" + // LabelControllerName is th label for the controller name + LabelControllerName = "controller-name" +) - // SensorControllerConfigMapKey is the key in the configmap to retrieve sensor configuration from. +const ( + // GatewayControllerConfigMapKey is the key in the configmap to retrieve controller configuration from. // Content encoding is expected to be YAML. - SensorControllerConfigMapKey = "config" - - // miscellaneous constants - // AnnotationSensorResourceSpecHashName is the annotation of a sensor resource spec hash - AnnotationSensorResourceSpecHashName = sensor.FullName + "/resource-spec-hash" + ControllerConfigMapKey = "config" ) -// SENSOR CONSTANTS +// Sensor constants const ( // SensorServiceEndpoint is the endpoint to dispatch the event to SensorServiceEndpoint = "/" - // SensorName refers env var for name of sensor SensorName = "SENSOR_NAME" - // SensorNamespace is used to get namespace where sensors are deployed SensorNamespace = "SENSOR_NAMESPACE" - // LabelSensorName is label for sensor name LabelSensorName = "sensor-name" - - // EnvVarSensorControllerInstanceID is used to get sensor controller instance id - EnvVarSensorControllerInstanceID = "SENSOR_CONTROLLER_INSTANCE_ID" -) - -// GATEWAY CONTROLLER CONSTANTS -const ( - // env variables - // EnvVarGatewayControllerConfigMap contains name of the configmap to retrieve gateway-controller configuration from - EnvVarGatewayControllerConfigMap = "GATEWAY_CONTROLLER_CONFIG_MAP" - - // EnvVarGatewayControllerInstanceID is used to get gateway controller instance id - EnvVarGatewayControllerInstanceID = "GATEWAY_CONTROLLER_INSTANCE_ID" - - // EnvVarGatewayControllerName is used to get name of gateway controller - EnvVarGatewayControllerName = "GATEWAY_CONTROLLER_NAME" - - // EnvVarGatewayName refers env var for name of gateway - EnvVarGatewayName = "GATEWAY_NAME" - - // EnvVarGatewayNamespace is namespace where gateway controller is deployed - EnvVarGatewayNamespace = "GATEWAY_NAMESPACE" - - // labels - // LabelGatewayControllerName is the default deployment name of the gateway-controller-controller - LabelGatewayControllerName = "gateway-controller" - - //LabelKeyGatewayControllerInstanceID is the label which allows to separate application among multiple running gateway-controller controllers. - LabelKeyGatewayControllerInstanceID = gateway.FullName + "/gateway-controller-instanceid" - - // LabelGatewayKeyPhase is a label applied to gateways to indicate the current phase of the gateway-controller (for filtering purposes) - LabelGatewayKeyPhase = gateway.FullName + "/phase" - - // LabelGatewayName is the label for gateway name - LabelGatewayName = "gateway-name" - - // LabelArgoEventsGatewayVersion is the label for the gateway version - LabelArgoEventsGatewayVersion = "argo-events-gateway-version" - - // AnnotationGatewayResourceSpecHashName is the annotation of a gateway resource spec hash - AnnotationGatewayResourceSpecHashName = gateway.FullName + "/resource-spec-hash" - - // GatewayControllerConfigMapKey is the key in the configmap to retrieve gateway-controller configuration from. - // Content encoding is expected to be YAML. - GatewayControllerConfigMapKey = "config" ) -// GATEWAY CONSTANTS +// Gateway constants const ( - // LabelGatewayEventSourceName is the label for a event source in gateway - LabelGatewayEventSourceName = "event-source-name" - - // LabelGatewayEventSourceID is the label for gateway configuration ID - LabelGatewayEventSourceID = "event-source-id" - - // LabelArgoEventsEventSourceVersion is the label for event source version - LabelArgoEventsEventSourceVersion = "argo-events-event-source-version" - - // EnvVarGatewayEventSourceConfigMap is used to get map containing event sources to run in a gateway - EnvVarGatewayEventSourceConfigMap = "GATEWAY_EVENT_SOURCE_CONFIG_MAP" - + // LabelEventSourceName is the label for a event source in gateway + LabelEventSourceName = "event-source-name" + // LabelEventSourceID is the label for gateway configuration ID + LabelEventSourceID = "event-source-id" EnvVarGatewayServerPort = "GATEWAY_SERVER_PORT" - // Server Connection Timeout, 10 seconds ServerConnTimeout = 10 ) +const ( + // EnvVarEventSource refers to event source name + EnvVarEventSource = "EVENT_SOURCE" + // AnnotationResourceSpecHash is the annotation of a K8s resource spec hash + AnnotationResourceSpecHash = "resource-spec-hash" +) + // CloudEvents constants const ( // CloudEventsVersion is the version of the CloudEvents spec targeted+ // by this library. CloudEventsVersion = "0.1" ) + +var ( + ErrNilEventSource = errors.New("event source can't be nil") +) + +// Miscellaneous Labels +const ( + // LabelOperation is a label for an operation in framework + LabelOperation = "operation" + // LabelEventSource is label for event name + LabelEventSource = "event-source" + // LabelOwnerName is the label for resource owner name + LabelOwnerName = "owner-name" + // LabelObjectName is the label for object name + LabelObjectName = "object-name" +) diff --git a/common/logger.go b/common/logger.go index 3736e2314e..7666c8877e 100644 --- a/common/logger.go +++ b/common/logger.go @@ -23,21 +23,22 @@ import ( // Logger constants const ( - LabelNamespace = "namespace" - LabelPhase = "phase" - LabelInstanceID = "instance-id" - LabelPodName = "pod-name" - LabelServiceName = "svc-name" - LabelEndpoint = "endpoint" - LabelPort = "port" - LabelURL = "url" - LabelNodeName = "node-name" - LabelNodeType = "node-type" - LabelHTTPMethod = "http-method" - LabelClientID = "client-id" - LabelVersion = "version" - LabelTime = "time" - LabelTriggerName = "trigger-name" + LabelNamespace = "namespace" + LabelPhase = "phase" + LabelInstanceID = "instance-id" + LabelPodName = "pod-name" + LabelDeploymentName = "deployment-name" + LabelServiceName = "svc-name" + LabelEndpoint = "endpoint" + LabelPort = "port" + LabelURL = "url" + LabelNodeName = "node-name" + LabelNodeType = "node-type" + LabelHTTPMethod = "http-method" + LabelClientID = "client-id" + LabelVersion = "version" + LabelTime = "time" + LabelTriggerName = "trigger-name" ) // NewArgoEventsLogger returns a new ArgoEventsLogger diff --git a/common/util.go b/common/util.go index f39d292331..9a2c89b648 100644 --- a/common/util.go +++ b/common/util.go @@ -21,8 +21,8 @@ import ( "fmt" "hash/fnv" "net/http" + "strings" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" @@ -35,14 +35,9 @@ func DefaultConfigMapName(controllerName string) string { return fmt.Sprintf("%s-configmap", controllerName) } -// DefaultServiceName returns a formulated name for a service -func DefaultServiceName(serviceName string) string { - return fmt.Sprintf("%s-svc", serviceName) -} - // ServiceDNSName returns a formulated dns name for a service func ServiceDNSName(serviceName, namespace string) string { - return fmt.Sprintf("%s-svc.%s.svc.cluster.local", serviceName, namespace) + return fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace) } // DefaultEventSourceName returns a formulated name for a gateway configuration @@ -111,12 +106,19 @@ func GetObjectHash(obj metav1.Object) (string, error) { return Hasher(string(b)), nil } -func CheckEventSourceVersion(cm *corev1.ConfigMap) error { - if cm.Labels == nil { - return fmt.Errorf("labels can't be empty. event source must be specified in as %s label", LabelArgoEventsEventSourceVersion) +// FormatEndpoint returns a formatted api endpoint +func FormatEndpoint(endpoint string) string { + if !strings.HasPrefix(endpoint, "/") { + return fmt.Sprintf("/%s", endpoint) } - if _, ok := cm.Labels[LabelArgoEventsEventSourceVersion]; !ok { - return fmt.Errorf("event source must be specified in as %s label", LabelArgoEventsEventSourceVersion) - } - return nil + return endpoint +} + +// FormattedURL returns a formatted url +func FormattedURL(url, endpoint string) string { + return fmt.Sprintf("%s%s", url, FormatEndpoint(endpoint)) +} + +func ErrEventSourceTypeMismatch(eventSourceType string) string { + return fmt.Sprintf("event source is not type of %s", eventSourceType) } diff --git a/common/util_test.go b/common/util_test.go index 7d3592b875..aded244489 100644 --- a/common/util_test.go +++ b/common/util_test.go @@ -17,7 +17,6 @@ limitations under the License. package common import ( - "fmt" "github.com/smartystreets/goconvey/convey" "net/http" "testing" @@ -66,12 +65,6 @@ func TestDefaultConfigMapName(t *testing.T) { assert.Equal(t, "sensor-controller-configmap", res) } -func TestDefaultServiceName(t *testing.T) { - convey.Convey("Given a service, get the default name", t, func() { - convey.So(DefaultServiceName("default"), convey.ShouldEqual, fmt.Sprintf("%s-svc", "default")) - }) -} - func TestDefaultNatsQueueName(t *testing.T) { convey.Convey("Given a nats queue, get the default name", t, func() { convey.So(DefaultNatsQueueName("default"), convey.ShouldEqual, "default-queue") @@ -114,3 +107,15 @@ func TestServerResourceForGroupVersionKind(t *testing.T) { }) }) } + +func TestFormatWebhookEndpoint(t *testing.T) { + convey.Convey("Given a webhook endpoint, format it", t, func() { + convey.So(FormatEndpoint("hello"), convey.ShouldEqual, "/hello") + }) +} + +func TestGenerateFormattedURL(t *testing.T) { + convey.Convey("Given a webhook, generate formatted URL", t, func() { + convey.So(FormattedURL("test-url", "fake"), convey.ShouldEqual, "test-url/fake") + }) +} diff --git a/controllers/common/informer.go b/controllers/common/informer.go deleted file mode 100644 index 73d7de5a65..0000000000 --- a/controllers/common/informer.go +++ /dev/null @@ -1,86 +0,0 @@ -package common - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/informers" - informersv1 "k8s.io/client-go/informers/core/v1" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/workqueue" -) - -// ArgoEventInformerFactory holds values to create SharedInformerFactory of argo-events -type ArgoEventInformerFactory struct { - OwnerGroupVersionKind schema.GroupVersionKind - OwnerInformer cache.SharedIndexInformer - informers.SharedInformerFactory - Queue workqueue.RateLimitingInterface -} - -// NewPodInformer returns a PodInformer of argo-events -func (c *ArgoEventInformerFactory) NewPodInformer() informersv1.PodInformer { - podInformer := c.SharedInformerFactory.Core().V1().Pods() - podInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - DeleteFunc: func(obj interface{}) { - owner, err := c.getOwner(obj) - if err != nil { - return - } - key, err := cache.MetaNamespaceKeyFunc(owner) - if err == nil { - c.Queue.Add(key) - } - }, - }, - ) - return podInformer -} - -// NewServiceInformer returns a ServiceInformer of argo-events -func (c *ArgoEventInformerFactory) NewServiceInformer() informersv1.ServiceInformer { - svcInformer := c.SharedInformerFactory.Core().V1().Services() - svcInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - DeleteFunc: func(obj interface{}) { - owner, err := c.getOwner(obj) - if err != nil { - return - } - key, err := cache.MetaNamespaceKeyFunc(owner) - if err == nil { - c.Queue.Add(key) - } - }, - }, - ) - return svcInformer -} - -func (c *ArgoEventInformerFactory) getOwner(obj interface{}) (interface{}, error) { - m, err := meta.Accessor(obj) - if err != nil { - return nil, err - } - for _, owner := range m.GetOwnerReferences() { - - if owner.APIVersion == c.OwnerGroupVersionKind.GroupVersion().String() && - owner.Kind == c.OwnerGroupVersionKind.Kind { - key := owner.Name - if len(m.GetNamespace()) > 0 { - key = m.GetNamespace() + "/" + key - } - obj, exists, err := c.OwnerInformer.GetIndexer().GetByKey(key) - if err != nil { - return nil, err - } - if !exists { - return nil, fmt.Errorf("failed to get object from cache") - } - return obj, nil - } - } - return nil, fmt.Errorf("no owner found") -} diff --git a/controllers/common/informer_test.go b/controllers/common/informer_test.go deleted file mode 100644 index 7b7f177b02..0000000000 --- a/controllers/common/informer_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package common - -import ( - "fmt" - "math/rand" - "testing" - "time" - - "github.com/smartystreets/goconvey/convey" - 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" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/workqueue" -) - -func getFakePodSharedIndexInformer(clientset kubernetes.Interface) cache.SharedIndexInformer { - // NewListWatchFromClient doesn't work with fake client. - // ref: https://github.com/kubernetes/client-go/issues/352 - return cache.NewSharedIndexInformer(&cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - return clientset.CoreV1().Pods("").List(options) - }, - WatchFunc: clientset.CoreV1().Pods("").Watch, - }, &corev1.Pod{}, 1*time.Second, cache.Indexers{}) -} - -func getInformerFactory(clientset kubernetes.Interface, queue workqueue.RateLimitingInterface, done chan struct{}) *ArgoEventInformerFactory { - informerFactory := informers.NewSharedInformerFactory(clientset, 0) - ownerInformer := getFakePodSharedIndexInformer(clientset) - go ownerInformer.Run(done) - return &ArgoEventInformerFactory{ - OwnerGroupVersionKind: schema.GroupVersionKind{Version: "v1", Kind: "Pod"}, - OwnerInformer: ownerInformer, - SharedInformerFactory: informerFactory, - Queue: queue, - } -} - -func getCommonPodSpec() corev1.PodSpec { - return corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "whalesay", - Image: "docker/whalesay:latest", - }, - }, - } -} - -func getPod(owner *corev1.Pod) *corev1.Pod { - var ownerReferneces = []metav1.OwnerReference{} - if owner != nil { - ownerReferneces = append(ownerReferneces, *metav1.NewControllerRef(owner, owner.GroupVersionKind())) - } - return &corev1.Pod{ - TypeMeta: metav1.TypeMeta{ - Kind: "Pod", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("pod-%d", rand.Uint32()), - OwnerReferences: ownerReferneces, - }, - Spec: getCommonPodSpec(), - } -} - -func getService(owner *corev1.Pod) *corev1.Service { - var ownerReferneces = []metav1.OwnerReference{} - if owner != nil { - ownerReferneces = append(ownerReferneces, *metav1.NewControllerRef(owner, owner.GroupVersionKind())) - } - return &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("svc-%d", rand.Uint32()), - OwnerReferences: ownerReferneces, - }, - Spec: corev1.ServiceSpec{}, - } -} - -func TestNewPodInformer(t *testing.T) { - done := make(chan struct{}) - queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - defer queue.ShutDown() - namespace := "namespace" - clientset := fake.NewSimpleClientset() - factory := getInformerFactory(clientset, queue, done) - convey.Convey("Given an informer factory", t, func() { - convey.Convey("Get a new gateway pod informer and make sure its not nil", func() { - podInformer := factory.NewPodInformer() - convey.So(podInformer, convey.ShouldNotBeNil) - - convey.Convey("Handle event", func() { - go podInformer.Informer().Run(done) - ownerPod := getPod(nil) - ownerPod, err := clientset.CoreV1().Pods(namespace).Create(ownerPod) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, factory.OwnerInformer.HasSynced) - - convey.Convey("Not enqueue owner key on creation", func() { - pod := getPod(ownerPod) - pod, err := clientset.CoreV1().Pods(namespace).Create(pod) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, podInformer.Informer().HasSynced) - - convey.So(queue.Len(), convey.ShouldEqual, 0) - - convey.Convey("Not enqueue owner key on update", func() { - pod.Labels = map[string]string{"foo": "bar"} - _, err = clientset.CoreV1().Pods(namespace).Update(pod) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, podInformer.Informer().HasSynced) - - convey.So(queue.Len(), convey.ShouldEqual, 0) - }) - - convey.Convey("Enqueue owner key on deletion", func() { - err := clientset.CoreV1().Pods(namespace).Delete(pod.Name, &metav1.DeleteOptions{}) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, podInformer.Informer().HasSynced) - - convey.So(queue.Len(), convey.ShouldEqual, 1) - key, _ := queue.Get() - queue.Done(key) - convey.So(key, convey.ShouldEqual, fmt.Sprintf("%s/%s", namespace, ownerPod.Name)) - }) - }) - }) - }) - }) -} - -func TestNewServiceInformer(t *testing.T) { - done := make(chan struct{}) - queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - defer queue.ShutDown() - namespace := "namespace" - clientset := fake.NewSimpleClientset() - factory := getInformerFactory(clientset, queue, done) - convey.Convey("Given an informer factory", t, func() { - convey.Convey("Get a new gateway service informer and make sure its not nil", func() { - svcInformer := factory.NewServiceInformer() - convey.So(svcInformer, convey.ShouldNotBeNil) - - convey.Convey("Handle event", func() { - go svcInformer.Informer().Run(done) - ownerPod := getPod(nil) - ownerPod, err := clientset.CoreV1().Pods(namespace).Create(ownerPod) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, factory.OwnerInformer.HasSynced) - - convey.Convey("Not enqueue owner key on creation", func() { - service := getService(ownerPod) - service, err := clientset.CoreV1().Services(namespace).Create(service) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, svcInformer.Informer().HasSynced) - convey.So(queue.Len(), convey.ShouldEqual, 0) - - convey.Convey("Not enqueue owner key on update", func() { - service.Labels = map[string]string{"foo": "bar"} - service, err = clientset.CoreV1().Services(namespace).Update(service) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, svcInformer.Informer().HasSynced) - convey.So(queue.Len(), convey.ShouldEqual, 0) - }) - - convey.Convey("Enqueue owner key on deletion", func() { - err := clientset.CoreV1().Services(namespace).Delete(service.Name, &metav1.DeleteOptions{}) - convey.So(err, convey.ShouldBeNil) - cache.WaitForCacheSync(done, svcInformer.Informer().HasSynced) - - convey.So(queue.Len(), convey.ShouldEqual, 1) - key, _ := queue.Get() - queue.Done(key) - convey.So(key, convey.ShouldEqual, fmt.Sprintf("%s/%s", namespace, ownerPod.Name)) - }) - }) - }) - }) - }) -} diff --git a/controllers/common/util.go b/controllers/common/util.go index 112335890c..a7ad13462a 100644 --- a/controllers/common/util.go +++ b/controllers/common/util.go @@ -1,39 +1,50 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package common import ( "github.com/argoproj/argo-events/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" ) -// ChildResourceContext holds necessary information for child resource setup -type ChildResourceContext struct { - SchemaGroupVersionKind schema.GroupVersionKind - LabelOwnerName string - LabelKeyOwnerControllerInstanceID string - AnnotationOwnerResourceHashName string - InstanceID string -} - // SetObjectMeta sets ObjectMeta of child resource -func (ctx *ChildResourceContext) SetObjectMeta(owner, obj metav1.Object) error { +func SetObjectMeta(owner, obj metav1.Object, gvk schema.GroupVersionKind) error { references := obj.GetOwnerReferences() references = append(references, - *metav1.NewControllerRef(owner, ctx.SchemaGroupVersionKind), + *metav1.NewControllerRef(owner, gvk), ) obj.SetOwnerReferences(references) if obj.GetName() == "" && obj.GetGenerateName() == "" { - obj.SetGenerateName(owner.GetName()) + obj.SetName(owner.GetName()) + } + if obj.GetNamespace() == "" { + obj.SetNamespace(owner.GetNamespace()) } - labels := obj.GetLabels() - if labels == nil { - labels = make(map[string]string) + objLabels := obj.GetLabels() + if objLabels == nil { + objLabels = make(map[string]string) } - labels[ctx.LabelOwnerName] = owner.GetName() - labels[ctx.LabelKeyOwnerControllerInstanceID] = ctx.InstanceID - obj.SetLabels(labels) + objLabels[common.LabelOwnerName] = owner.GetName() + obj.SetLabels(objLabels) hash, err := common.GetObjectHash(obj) if err != nil { @@ -43,8 +54,17 @@ func (ctx *ChildResourceContext) SetObjectMeta(owner, obj metav1.Object) error { if annotations == nil { annotations = make(map[string]string) } - annotations[ctx.AnnotationOwnerResourceHashName] = hash + annotations[common.AnnotationResourceSpecHash] = hash obj.SetAnnotations(annotations) return nil } + +// OwnerLabelSelector returns label selector for a K8s resource by it's owner +func OwnerLabelSelector(ownerName string) (labels.Selector, error) { + req, err := labels.NewRequirement(common.LabelResourceName, selection.Equals, []string{ownerName}) + if err != nil { + return nil, err + } + return labels.NewSelector().Add(*req), nil +} diff --git a/controllers/common/util_test.go b/controllers/common/util_test.go index 012e07eb9c..1f7526c22e 100644 --- a/controllers/common/util_test.go +++ b/controllers/common/util_test.go @@ -1,39 +1,49 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package common import ( "testing" - "github.com/smartystreets/goconvey/convey" + "github.com/argoproj/argo-events/common" + "github.com/stretchr/testify/assert" + appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" ) func TestSetObjectMeta(t *testing.T) { - convey.Convey("Given an object, set meta", t, func() { - groupVersionKind := schema.GroupVersionKind{ - Group: "grp", - Version: "ver", - Kind: "kind", - } - ctx := ChildResourceContext{ - SchemaGroupVersionKind: groupVersionKind, - LabelOwnerName: "foo", - LabelKeyOwnerControllerInstanceID: "id", - AnnotationOwnerResourceHashName: "hash", - InstanceID: "ID", - } - owner := corev1.Pod{} - pod := corev1.Pod{} - ref := metav1.NewControllerRef(&owner, groupVersionKind) - - err := ctx.SetObjectMeta(&owner, &pod) - convey.So(err, convey.ShouldBeEmpty) - convey.So(pod.Labels["foo"], convey.ShouldEqual, "") - convey.So(pod.Labels["id"], convey.ShouldEqual, "ID") - convey.So(pod.Annotations, convey.ShouldContainKey, "hash") - convey.So(pod.Name, convey.ShouldEqual, "") - convey.So(pod.GenerateName, convey.ShouldEqual, "") - convey.So(pod.OwnerReferences, convey.ShouldContain, *ref) - }) + owner := appv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-deployment", + Namespace: "fake-namespace", + }, + } + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pod", + }, + } + + err := SetObjectMeta(&owner, &pod, owner.GroupVersionKind()) + assert.Nil(t, err) + assert.Equal(t, "fake-namespace", pod.Namespace) + assert.Equal(t, owner.GroupVersionKind().Kind, pod.OwnerReferences[0].Kind) + assert.NotEmpty(t, pod.Annotations[common.AnnotationResourceSpecHash]) + assert.NotEmpty(t, pod.Labels) + assert.Equal(t, owner.Name, pod.Labels[common.LabelOwnerName]) } diff --git a/cmd/controllers/gateway/main.go b/controllers/gateway/cmd/main.go similarity index 82% rename from cmd/controllers/gateway/main.go rename to controllers/gateway/cmd/main.go index c489d84054..be79cdd645 100644 --- a/cmd/controllers/gateway/main.go +++ b/controllers/gateway/cmd/main.go @@ -33,12 +33,12 @@ func main() { } // gateway-controller configuration - configMap, ok := os.LookupEnv(common.EnvVarGatewayControllerConfigMap) + configMap, ok := os.LookupEnv(common.EnvVarControllerConfigMap) if !ok { - configMap = common.DefaultConfigMapName(common.LabelGatewayControllerName) + panic("controller configmap is not provided") } - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) + namespace, ok := os.LookupEnv(common.EnvVarNamespace) if !ok { namespace = common.DefaultControllerNamespace } @@ -51,6 +51,6 @@ func main() { panic(err) } - go controller.Run(context.Background(), 1, 1) + go controller.Run(context.Background(), 1) select {} } diff --git a/gateways/community/gcp-pubsub/config_test.go b/controllers/gateway/common.go similarity index 54% rename from gateways/community/gcp-pubsub/config_test.go rename to controllers/gateway/common.go index f816c79722..a8d22c43ec 100644 --- a/gateways/community/gcp-pubsub/config_test.go +++ b/controllers/gateway/common.go @@ -14,24 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package pubsub +package gateway -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -projectID: "1234" -topic: "test" -` +import "github.com/argoproj/argo-events/pkg/apis/gateway" -func TestParseConfig(t *testing.T) { - convey.Convey("Given a gcp-pubsub event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*pubSubEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} +// Labels +const ( + //LabelKeyGatewayControllerInstanceID is the label which allows to separate application among multiple running controller controllers. + LabelControllerInstanceID = gateway.FullName + "/gateway-controller-instanceid" + // LabelGatewayKeyPhase is a label applied to gateways to indicate the current phase of the controller (for filtering purposes) + LabelPhase = gateway.FullName + "/phase" +) diff --git a/controllers/gateway/config.go b/controllers/gateway/config.go index 9dddaf0518..6d32e15398 100644 --- a/controllers/gateway/config.go +++ b/controllers/gateway/config.go @@ -22,6 +22,7 @@ import ( "github.com/argoproj/argo-events/common" "github.com/ghodss/yaml" + "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -31,8 +32,8 @@ import ( ) // watches configuration for gateway controller -func (c *GatewayController) watchControllerConfigMap(ctx context.Context) (cache.Controller, error) { - c.log.Info("watching gateway-controller config map updates") +func (c *Controller) watchControllerConfigMap(ctx context.Context) (cache.Controller, error) { + c.logger.Infoln("watching gateway-controller config map updates") source := c.newControllerConfigMapWatch() _, controller := cache.NewInformer( source, @@ -41,19 +42,19 @@ func (c *GatewayController) watchControllerConfigMap(ctx context.Context) (cache cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if cm, ok := obj.(*apiv1.ConfigMap); ok { - c.log.Info("detected EventSource update. updating the gateway-controller config.") + c.logger.Info("detected new gateway controller configmap") err := c.updateConfig(cm) if err != nil { - c.log.Error("update of config failed", "err", err) + c.logger.WithError(err).Errorln("update of gateway controller configuration failed") } } }, UpdateFunc: func(old, new interface{}) { if newCm, ok := new.(*apiv1.ConfigMap); ok { - c.log.Info("detected EventSource update. updating the gateway-controller config.") + c.logger.Infoln("detected gateway controller configmap update.") err := c.updateConfig(newCm) if err != nil { - c.log.WithError(err).Error("update of config failed") + c.logger.WithError(err).Errorln("update of gateway controller configuration failed") } } }, @@ -64,8 +65,8 @@ func (c *GatewayController) watchControllerConfigMap(ctx context.Context) (cache } // creates a new config map watcher -func (c *GatewayController) newControllerConfigMapWatch() *cache.ListWatch { - x := c.kubeClientset.CoreV1().RESTClient() +func (c *Controller) newControllerConfigMapWatch() *cache.ListWatch { + x := c.k8sClient.CoreV1().RESTClient() resource := "configmaps" name := c.ConfigMap fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", name)) @@ -91,8 +92,8 @@ func (c *GatewayController) newControllerConfigMapWatch() *cache.ListWatch { } // ResyncConfig reloads the gateway-controller config from the configmap -func (c *GatewayController) ResyncConfig(namespace string) error { - cmClient := c.kubeClientset.CoreV1().ConfigMaps(namespace) +func (c *Controller) ResyncConfig(namespace string) error { + cmClient := c.k8sClient.CoreV1().ConfigMaps(namespace) cm, err := cmClient.Get(c.ConfigMap, metav1.GetOptions{}) if err != nil { return err @@ -101,12 +102,12 @@ func (c *GatewayController) ResyncConfig(namespace string) error { } // updates the gateway controller configuration -func (c *GatewayController) updateConfig(cm *apiv1.ConfigMap) error { - configStr, ok := cm.Data[common.GatewayControllerConfigMapKey] +func (c *Controller) updateConfig(cm *apiv1.ConfigMap) error { + configStr, ok := cm.Data[common.ControllerConfigMapKey] if !ok { - return fmt.Errorf("configMap '%s' does not have key '%s'", c.ConfigMap, common.GatewayControllerConfigMapKey) + return errors.Errorf("configMap '%s' does not have key '%s'", c.ConfigMap, common.ControllerConfigMapKey) } - var config GatewayControllerConfig + var config ControllerConfig err := yaml.Unmarshal([]byte(configStr), &config) if err != nil { return err diff --git a/controllers/gateway/config_test.go b/controllers/gateway/config_test.go index 508e7d2e2c..5257e7bd74 100644 --- a/controllers/gateway/config_test.go +++ b/controllers/gateway/config_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/argoproj/argo-events/common" - "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -29,47 +29,24 @@ var ( configmapName = common.DefaultConfigMapName("gateway-controller") ) -func TestGatewayControllerConfigWatch(t *testing.T) { - gc := getGatewayController() - - convey.Convey("Given a gateway", t, func() { - convey.Convey("Create a new watch and make sure watcher is not nil", func() { - watcher := gc.newControllerConfigMapWatch() - convey.So(watcher, convey.ShouldNotBeNil) - }) - }) - - convey.Convey("Given a gateway, resync config", t, func() { - convey.Convey("Update a gateway configmap with new instance id and remove namespace", func() { - cmObj := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: common.DefaultControllerNamespace, - Name: gc.ConfigMap, - }, - Data: map[string]string{ - common.GatewayControllerConfigMapKey: `instanceID: fake-instance-id`, - }, - } - cm, err := gc.kubeClientset.CoreV1().ConfigMaps(gc.Namespace).Create(cmObj) - convey.Convey("Make sure no error occurs", func() { - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Updated gateway configmap must be non-nil", func() { - convey.So(cm, convey.ShouldNotBeNil) - - convey.Convey("Resync the gateway configuration", func() { - err := gc.ResyncConfig(cmObj.Namespace) - convey.Convey("No error should occur while resyncing gateway configuration", func() { - convey.So(err, convey.ShouldBeNil) - - convey.Convey("The updated instance id must be fake-instance-id", func() { - convey.So(gc.Config.InstanceID, convey.ShouldEqual, "fake-instance-id") - convey.So(gc.Config.Namespace, convey.ShouldBeEmpty) - }) - }) - }) - }) - }) - }) - }) +func TestController_ResyncConfig(t *testing.T) { + controller := newController() + + cmObj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: common.DefaultControllerNamespace, + Name: controller.ConfigMap, + }, + Data: map[string]string{ + common.ControllerConfigMapKey: `instanceID: fake-instance-id`, + }, + } + + cm, err := controller.k8sClient.CoreV1().ConfigMaps(controller.Namespace).Create(cmObj) + assert.Nil(t, err) + assert.NotNil(t, cm) + err = controller.ResyncConfig(common.DefaultControllerNamespace) + assert.Nil(t, err) + assert.Equal(t, controller.Config.Namespace, "") + assert.Equal(t, controller.Config.InstanceID, "fake-instance-id") } diff --git a/controllers/gateway/controller.go b/controllers/gateway/controller.go index 6177283158..c5f88662a4 100644 --- a/controllers/gateway/controller.go +++ b/controllers/gateway/controller.go @@ -20,18 +20,14 @@ import ( "context" "errors" "fmt" - "github.com/sirupsen/logrus" "time" base "github.com/argoproj/argo-events" "github.com/argoproj/argo-events/common" - ccommon "github.com/argoproj/argo-events/controllers/common" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" clientset "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/informers" informersv1 "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -40,37 +36,35 @@ import ( ) const ( - gatewayResyncPeriod = 20 * time.Minute - gatewayResourceResyncPeriod = 30 * time.Minute - rateLimiterBaseDelay = 5 * time.Second - rateLimiterMaxDelay = 1000 * time.Second + gatewayResyncPeriod = 20 * time.Minute + rateLimiterBaseDelay = 5 * time.Second + rateLimiterMaxDelay = 1000 * time.Second ) -// GatewayControllerConfig contain the configuration settings for the gateway-controller -type GatewayControllerConfig struct { - // InstanceID is a label selector to limit the gateway-controller's watch of gateway jobs to a specific instance. +// ControllerConfig contain the configuration settings for the controller +type ControllerConfig struct { + // InstanceID is a label selector to limit the controller's watch of gateway to a specific instance. InstanceID string - - // Namespace is a label selector filter to limit gateway-controller-controller's watch to specific namespace + // Namespace is a label selector filter to limit controller's watch to specific namespace Namespace string } -// GatewayController listens for new gateways and hands off handling of each gateway-controller on the queue to the operator -type GatewayController struct { - // EventSource is the name of the config map in which to derive configuration of the contoller +// Controller listens for new gateways and hands off handling of each gateway controller on the queue to the operator +type Controller struct { + // ConfigMap is the name of the config map in which to derive configuration of the controller ConfigMap string - // Namespace for gateway controller + // Namespace for controller Namespace string - // Config is the gateway-controller gateway-controller-controller's configuration - Config GatewayControllerConfig - // log is the logger for a gateway - log *logrus.Logger - - // kubernetes config and apis - kubeConfig *rest.Config - kubeClientset kubernetes.Interface - gatewayClientset clientset.Interface - + // Config is the controller's configuration + Config ControllerConfig + // logger to logger stuff + logger *logrus.Logger + // K8s rest config + kubeConfig *rest.Config + // k8sClient is Kubernetes client + k8sClient kubernetes.Interface + // gatewayClient is the Argo-Events gateway resource client + gatewayClient clientset.Interface // gateway-controller informer and queue podInformer informersv1.PodInformer svcInformer informersv1.ServiceInformer @@ -78,21 +72,22 @@ type GatewayController struct { queue workqueue.RateLimitingInterface } -// NewGatewayController creates a new Controller -func NewGatewayController(rest *rest.Config, configMap, namespace string) *GatewayController { +// NewGatewayController creates a new controller +func NewGatewayController(rest *rest.Config, configMap, namespace string) *Controller { rateLimiter := workqueue.NewItemExponentialFailureRateLimiter(rateLimiterBaseDelay, rateLimiterMaxDelay) - return &GatewayController{ - ConfigMap: configMap, - Namespace: namespace, - kubeConfig: rest, - log: common.NewArgoEventsLogger(), - kubeClientset: kubernetes.NewForConfigOrDie(rest), - gatewayClientset: clientset.NewForConfigOrDie(rest), - queue: workqueue.NewRateLimitingQueue(rateLimiter), + return &Controller{ + ConfigMap: configMap, + Namespace: namespace, + kubeConfig: rest, + logger: common.NewArgoEventsLogger(), + k8sClient: kubernetes.NewForConfigOrDie(rest), + gatewayClient: clientset.NewForConfigOrDie(rest), + queue: workqueue.NewRateLimitingQueue(rateLimiter), } } -func (c *GatewayController) processNextItem() bool { +// processNextItem processes a gateway resource on the controller's queue +func (c *Controller) processNextItem() bool { // Wait until there is a new item in the queue key, quit := c.queue.Get() if quit { @@ -102,26 +97,26 @@ func (c *GatewayController) processNextItem() bool { obj, exists, err := c.informer.GetIndexer().GetByKey(key.(string)) if err != nil { - c.log.WithField(common.LabelGatewayName, key.(string)).WithError(err).Warn("failed to get gateway from informer index") + c.logger.WithField(common.LabelResourceName, key.(string)).WithError(err).Warnln("failed to get gateway from informer index") return true } if !exists { - // this happens after gateway-controller was deleted, but work queue still had entry in it + // this happens after controller was deleted, but work queue still had entry in it return true } gw, ok := obj.(*v1alpha1.Gateway) if !ok { - c.log.WithField(common.LabelGatewayName, key.(string)).WithError(err).Warn("key in index is not a gateway") + c.logger.WithField(common.LabelResourceName, key.(string)).WithError(err).Warnln("key in index is not a gateway") return true } - ctx := newGatewayOperationCtx(gw, c) + ctx := newGatewayContext(gw, c) err = ctx.operate() if err != nil { - if err := common.GenerateK8sEvent(c.kubeClientset, + if err := common.GenerateK8sEvent(c.k8sClient, fmt.Sprintf("controller failed to operate on gateway %s", gw.Name), common.StateChangeEventType, "controller operation failed", @@ -130,25 +125,25 @@ func (c *GatewayController) processNextItem() bool { c.Config.InstanceID, gw.Kind, map[string]string{ - common.LabelGatewayName: gw.Name, - common.LabelEventType: string(common.EscalationEventType), + common.LabelResourceName: gw.Name, + common.LabelEventType: string(common.EscalationEventType), }, ); err != nil { - ctx.log.WithError(err).Error("failed to create K8s event to escalate controller operation failure") + ctx.logger.WithError(err).Errorln("failed to create K8s event to escalate controller operation failure") } } err = c.handleErr(err, key) // create k8 event to escalate the error if err != nil { - ctx.log.WithError(err).Error("gateway controller failed to handle error") + ctx.logger.WithError(err).Errorln("controller failed to handle error") } return true } // handleErr checks if an error happened and make sure we will retry later // returns an error if unable to handle the error -func (c *GatewayController) handleErr(err error, key interface{}) error { +func (c *Controller) handleErr(err error, key interface{}) error { if err == nil { // Forget about the #AddRateLimited history of key on every successful sync // Ensure future updates for this key are not delayed because of outdated error history @@ -160,7 +155,7 @@ func (c *GatewayController) handleErr(err error, key interface{}) error { // requeues will happen very quickly even after a gateway pod goes down // we want to give the event pod a chance to come back up so we give a generous number of retries if c.queue.NumRequeues(key) < 20 { - c.log.WithField(common.LabelGatewayName, key.(string)).WithError(err).Error("error syncing gateway") + c.logger.WithField(common.LabelResourceName, key.(string)).WithError(err).Errorln("error syncing gateway") // Re-enqueue the key rate limited. This key will be processed later again. c.queue.AddRateLimited(key) @@ -169,63 +164,41 @@ func (c *GatewayController) handleErr(err error, key interface{}) error { return errors.New("exceeded max requeues") } -// Run executes the gateway-controller -func (c *GatewayController) Run(ctx context.Context, gwThreads, eventThreads int) { +// Run processes the gateway resources on the controller's queue +func (c *Controller) Run(ctx context.Context, threads int) { defer c.queue.ShutDown() - c.log.WithFields( + c.logger.WithFields( map[string]interface{}{ common.LabelInstanceID: c.Config.InstanceID, common.LabelVersion: base.GetVersion().Version, - }).Info("starting gateway-controller") + }).Infoln("starting controller") + _, err := c.watchControllerConfigMap(ctx) if err != nil { - c.log.WithError(err).Error("failed to register watch for gateway-controller config map") - return - } - - c.informer = c.newGatewayInformer() - go c.informer.Run(ctx.Done()) - - if !cache.WaitForCacheSync(ctx.Done(), c.informer.HasSynced) { - c.log.Panicf("timed out waiting for the caches to sync for gateways") + c.logger.WithError(err).Errorln("failed to register watch for controller config map") return } - listOptionsFunc := func(options *metav1.ListOptions) { - labelSelector := labels.NewSelector().Add(c.instanceIDReq()) - options.LabelSelector = labelSelector.String() - } - factory := ccommon.ArgoEventInformerFactory{ - OwnerGroupVersionKind: v1alpha1.SchemaGroupVersionKind, - OwnerInformer: c.informer, - SharedInformerFactory: informers.NewFilteredSharedInformerFactory(c.kubeClientset, gatewayResourceResyncPeriod, c.Config.Namespace, listOptionsFunc), - Queue: c.queue, - } - - c.podInformer = factory.NewPodInformer() - go c.podInformer.Informer().Run(ctx.Done()) - - if !cache.WaitForCacheSync(ctx.Done(), c.podInformer.Informer().HasSynced) { - c.log.Panic("timed out waiting for the caches to sync for gateway pods") - return + c.informer, err = c.newGatewayInformer() + if err != nil { + panic(err) } - c.svcInformer = factory.NewServiceInformer() - go c.svcInformer.Informer().Run(ctx.Done()) + go c.informer.Run(ctx.Done()) - if !cache.WaitForCacheSync(ctx.Done(), c.svcInformer.Informer().HasSynced) { - c.log.Panic("timed out waiting for the caches to sync for gateway services") + if !cache.WaitForCacheSync(ctx.Done(), c.informer.HasSynced) { + c.logger.Errorln("timed out waiting for the caches to sync for gateways") return } - for i := 0; i < gwThreads; i++ { + for i := 0; i < threads; i++ { go wait.Until(c.runWorker, time.Second, ctx.Done()) } <-ctx.Done() } -func (c *GatewayController) runWorker() { +func (c *Controller) runWorker() { for c.processNextItem() { } } diff --git a/controllers/gateway/controller_test.go b/controllers/gateway/controller_test.go index ab9f4cb918..c4278e2a78 100644 --- a/controllers/gateway/controller_test.go +++ b/controllers/gateway/controller_test.go @@ -19,99 +19,67 @@ package gateway import ( "fmt" "testing" - "time" "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" fakegateway "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/fake" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" ) -func getFakePodSharedIndexInformer(clientset kubernetes.Interface) cache.SharedIndexInformer { - // NewListWatchFromClient doesn't work with fake client. - // ref: https://github.com/kubernetes/client-go/issues/352 - return cache.NewSharedIndexInformer(&cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - return clientset.CoreV1().Pods("").List(options) - }, - WatchFunc: clientset.CoreV1().Pods("").Watch, - }, &corev1.Pod{}, 1*time.Second, cache.Indexers{}) -} - -func getGatewayController() *GatewayController { - clientset := fake.NewSimpleClientset() - done := make(chan struct{}) - informer := getFakePodSharedIndexInformer(clientset) - go informer.Run(done) - factory := informers.NewSharedInformerFactory(clientset, 0) - podInformer := factory.Core().V1().Pods() - go podInformer.Informer().Run(done) - svcInformer := factory.Core().V1().Services() - go svcInformer.Informer().Run(done) - return &GatewayController{ +func newController() *Controller { + controller := &Controller{ ConfigMap: configmapName, Namespace: common.DefaultControllerNamespace, - Config: GatewayControllerConfig{ + Config: ControllerConfig{ Namespace: common.DefaultControllerNamespace, InstanceID: "argo-events", }, - kubeClientset: clientset, - gatewayClientset: fakegateway.NewSimpleClientset(), - podInformer: podInformer, - svcInformer: svcInformer, - informer: informer, - queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), - log: common.NewArgoEventsLogger(), + k8sClient: fake.NewSimpleClientset(), + gatewayClient: fakegateway.NewSimpleClientset(), + queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + logger: common.NewArgoEventsLogger(), } + informer, err := controller.newGatewayInformer() + if err != nil { + panic(err) + } + controller.informer = informer + return controller } -func TestGatewayController(t *testing.T) { - convey.Convey("Given a gateway controller, process queue items", t, func() { - controller := getGatewayController() - - convey.Convey("Create a resource queue, add new item and process it", func() { - controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - controller.informer = controller.newGatewayInformer() - controller.queue.Add("hi") - res := controller.processNextItem() +func TestGatewayController_ProcessNextItem(t *testing.T) { + controller := newController() + controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + gw := &v1alpha1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-gateway", + Namespace: common.DefaultControllerNamespace, + }, + Spec: v1alpha1.GatewaySpec{}, + } + err := controller.informer.GetIndexer().Add(gw) + assert.Nil(t, err) - convey.Convey("Item from queue must be successfully processed", func() { - convey.So(res, convey.ShouldBeTrue) - }) + controller.queue.Add("fake-gateway") + res := controller.processNextItem() + assert.Equal(t, res, true) - convey.Convey("Shutdown queue and make sure queue does not process next item", func() { - controller.queue.ShutDown() - res := controller.processNextItem() - convey.So(res, convey.ShouldBeFalse) - }) - }) - }) + controller.queue.ShutDown() + res = controller.processNextItem() + assert.Equal(t, res, false) +} - convey.Convey("Given a gateway controller, handle errors in queue", t, func() { - controller := getGatewayController() - convey.Convey("Create a resource queue and add an item", func() { - controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - controller.queue.Add("hi") - convey.Convey("Handle an nil error", func() { - err := controller.handleErr(nil, "hi") - convey.So(err, convey.ShouldBeNil) - }) - convey.Convey("Exceed max requeues", func() { - controller.queue.Add("bye") - var err error - for i := 0; i < 21; i++ { - err = controller.handleErr(fmt.Errorf("real error"), "bye") - } - convey.So(err, convey.ShouldNotBeNil) - convey.So(err.Error(), convey.ShouldEqual, "exceeded max requeues") - }) - }) - }) +func TestGatewayController_HandleErr(t *testing.T) { + controller := newController() + controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + controller.queue.Add("hi") + var err error + for i := 0; i < 21; i++ { + err = controller.handleErr(fmt.Errorf("real error"), "bye") + } + assert.NotNil(t, err) + assert.Equal(t, err.Error(), "exceeded max requeues") } diff --git a/controllers/gateway/informer.go b/controllers/gateway/informer.go index e555f87ed2..45fc5302c9 100644 --- a/controllers/gateway/informer.go +++ b/controllers/gateway/informer.go @@ -17,48 +17,41 @@ limitations under the License. package gateway import ( - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/tools/cache" - "github.com/argoproj/argo-events/common" gatewayinformers "github.com/argoproj/argo-events/pkg/client/gateway/informers/externalversions" ) -func (c *GatewayController) instanceIDReq() labels.Requirement { - // it makes sense to make instance id is mandatory. +func (c *Controller) instanceIDReq() (*labels.Requirement, error) { if c.Config.InstanceID == "" { panic("instance id is required") } - instanceIDReq, err := labels.NewRequirement(common.LabelKeyGatewayControllerInstanceID, selection.Equals, []string{c.Config.InstanceID}) + instanceIDReq, err := labels.NewRequirement(LabelControllerInstanceID, selection.Equals, []string{c.Config.InstanceID}) if err != nil { - panic(err) + return nil, err } - return *instanceIDReq + c.logger.WithField("instance-id", instanceIDReq.String()).Infoln("instance id requirement") + return instanceIDReq, nil } -func (c *GatewayController) versionReq() labels.Requirement { - versionReq, err := labels.NewRequirement(common.LabelArgoEventsGatewayVersion, selection.Equals, []string{v1alpha1.ArgoEventsGatewayVersion}) +// The controller informer adds new gateways to the controller's queue based on Add, Update, and Delete Event Handlers for the gateway Resources +func (c *Controller) newGatewayInformer() (cache.SharedIndexInformer, error) { + labelSelector, err := c.instanceIDReq() if err != nil { - panic(err) + return nil, err } - return *versionReq -} - -// The gateway-controller informer adds new Gateways to the gateway-controller-controller's queue based on Add, Update, and Delete Event Handlers for the Gateway Resources -func (c *GatewayController) newGatewayInformer() cache.SharedIndexInformer { - gatewayInformerFactory := gatewayinformers.NewFilteredSharedInformerFactory( - c.gatewayClientset, + gatewayInformerFactory := gatewayinformers.NewSharedInformerFactoryWithOptions( + c.gatewayClient, gatewayResyncPeriod, - c.Config.Namespace, - func(options *metav1.ListOptions) { + gatewayinformers.WithNamespace(c.Config.Namespace), + gatewayinformers.WithTweakListOptions(func(options *metav1.ListOptions) { options.FieldSelector = fields.Everything().String() - labelSelector := labels.NewSelector().Add(c.instanceIDReq(), c.versionReq()) options.LabelSelector = labelSelector.String() - }, + }), ) informer := gatewayInformerFactory.Argoproj().V1alpha1().Gateways().Informer() informer.AddEventHandler( @@ -83,5 +76,5 @@ func (c *GatewayController) newGatewayInformer() cache.SharedIndexInformer { }, }, ) - return informer + return informer, nil } diff --git a/controllers/gateway/informer_test.go b/controllers/gateway/informer_test.go index bc9e58fe47..17f18e8706 100644 --- a/controllers/gateway/informer_test.go +++ b/controllers/gateway/informer_test.go @@ -17,29 +17,29 @@ limitations under the License. package gateway import ( + "fmt" "testing" - "github.com/argoproj/argo-events/common" - "github.com/smartystreets/goconvey/convey" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/selection" ) -func TestInformer(t *testing.T) { - convey.Convey("Given a gateway controller", t, func() { - controller := getGatewayController() - convey.Convey("Instance ID required key must match", func() { - req := controller.instanceIDReq() - convey.So(req.Key(), convey.ShouldEqual, common.LabelKeyGatewayControllerInstanceID) - convey.So(req.Operator(), convey.ShouldEqual, selection.Equals) - convey.So(req.Values().Has("argo-events"), convey.ShouldBeTrue) - }) - }) - - convey.Convey("Given a gateway controller", t, func() { - controller := getGatewayController() - convey.Convey("Get a new gateway informer and make sure its not nil", func() { - i := controller.newGatewayInformer() - convey.So(i, convey.ShouldNotBeNil) - }) - }) +func TestInformer_InstanceIDReq(t *testing.T) { + controller := newController() + req, err := controller.instanceIDReq() + assert.Nil(t, err) + assert.Equal(t, req.Key(), LabelControllerInstanceID) + assert.Equal(t, req.Operator(), selection.Equals) + assert.Equal(t, req.Values().Has("argo-events"), true) + assert.Equal(t, req.String(), fmt.Sprintf("%s=%s", LabelControllerInstanceID, "argo-events")) +} + +func TestInformer_NewInformer(t *testing.T) { + controller := newController() + i, err := controller.newGatewayInformer() + assert.Nil(t, err) + assert.NotNil(t, i) + err = i.GetIndexer().Add(&v1alpha1.Gateway{}) + assert.Nil(t, err) } diff --git a/controllers/gateway/operator.go b/controllers/gateway/operator.go index da890be6f4..e4de8f960c 100644 --- a/controllers/gateway/operator.go +++ b/controllers/gateway/operator.go @@ -17,343 +17,207 @@ limitations under the License. package gateway import ( - "github.com/sirupsen/logrus" "time" - "github.com/pkg/errors" - "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/pkg/apis/gateway" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - corev1 "k8s.io/api/core/v1" + gwclient "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" ) -// the context of an operation on a gateway-controller. -// the gateway-controller-controller creates this context each time it picks a Gateway off its queue. -type gwOperationCtx struct { - // gw is the gateway-controller object - gw *v1alpha1.Gateway - // updated indicates whether the gateway-controller object was updated and needs to be persisted back to k8 +// the context of an operation in the controller. +// the controller creates this context each time it picks a Gateway off its queue. +type gatewayContext struct { + // gateway is the controller object + gateway *v1alpha1.Gateway + // updated indicates whether the controller object was updated and needs to be persisted back to k8 updated bool - // log is the logger for a gateway - log *logrus.Logger - // reference to the gateway-controller-controller - controller *GatewayController - // gwrctx is the context to handle child resource - gwrctx gwResourceCtx + // logger is the logger for a gateway + logger *logrus.Logger + // reference to the controller + controller *Controller } -// newGatewayOperationCtx creates and initializes a new gOperationCtx object -func newGatewayOperationCtx(gw *v1alpha1.Gateway, controller *GatewayController) *gwOperationCtx { - gw = gw.DeepCopy() - return &gwOperationCtx{ - gw: gw, +// newGatewayContext creates and initializes a new gatewayContext object +func newGatewayContext(gatewayObj *v1alpha1.Gateway, controller *Controller) *gatewayContext { + gatewayObj = gatewayObj.DeepCopy() + return &gatewayContext{ + gateway: gatewayObj, updated: false, - log: common.NewArgoEventsLogger().WithFields( + logger: common.NewArgoEventsLogger().WithFields( map[string]interface{}{ - common.LabelGatewayName: gw.Name, - common.LabelNamespace: gw.Namespace, + common.LabelResourceName: gatewayObj.Name, + common.LabelNamespace: gatewayObj.Namespace, }).Logger, controller: controller, - gwrctx: NewGatewayResourceContext(gw, controller), } } // operate checks the status of gateway resource and takes action based on it. -func (goc *gwOperationCtx) operate() error { - defer func() { - if goc.updated { - var err error - eventType := common.StateChangeEventType - labels := map[string]string{ - common.LabelGatewayName: goc.gw.Name, - common.LabelGatewayKeyPhase: string(goc.gw.Status.Phase), - common.LabelKeyGatewayControllerInstanceID: goc.controller.Config.InstanceID, - common.LabelOperation: "persist_gateway_state", - } - goc.gw, err = PersistUpdates(goc.controller.gatewayClientset, goc.gw, goc.log) - if err != nil { - goc.log.WithError(err).Error("failed to persist gateway update, escalating...") - // escalate - eventType = common.EscalationEventType - } +func (ctx *gatewayContext) operate() error { + defer ctx.updateGatewayState() - labels[common.LabelEventType] = string(eventType) - if err := common.GenerateK8sEvent(goc.controller.kubeClientset, - "persist update", - eventType, - "gateway state update", - goc.gw.Name, - goc.gw.Namespace, - goc.controller.Config.InstanceID, - gateway.Kind, - labels, - ); err != nil { - goc.log.WithError(err).Error("failed to create K8s event to log gateway state persist operation") - return - } - goc.log.Info("successfully persisted gateway resource update and created K8s event") - } - goc.updated = false - }() + ctx.logger.WithField(common.LabelPhase, string(ctx.gateway.Status.Phase)).Infoln("operating on the gateway...") - goc.log.WithField(common.LabelPhase, string(goc.gw.Status.Phase)).Info("operating on the gateway") + if err := Validate(ctx.gateway); err != nil { + ctx.logger.WithError(err).Infoln("invalid gateway object") + return err + } // check the state of a gateway and take actions accordingly - switch goc.gw.Status.Phase { + switch ctx.gateway.Status.Phase { case v1alpha1.NodePhaseNew: - err := goc.createGatewayResources() - if err != nil { + if err := ctx.createGatewayResources(); err != nil { + ctx.logger.WithError(err).Errorln("failed to create resources for the gateway") + ctx.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) return err } + ctx.logger.Infoln("marking gateway as active") + ctx.markGatewayPhase(v1alpha1.NodePhaseRunning, "gateway is active") - // Gateway is in error - case v1alpha1.NodePhaseError: - goc.log.Error("gateway is in error state. please check escalated K8 event for the error") - err := goc.updateGatewayResources() + // Gateway is already running + case v1alpha1.NodePhaseRunning: + ctx.logger.Infoln("gateway is running") + err := ctx.updateGatewayResources() if err != nil { + ctx.logger.WithError(err).Errorln("failed to update resources for the gateway") + ctx.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) return err } + ctx.updated = true - // Gateway is already running, do nothing - case v1alpha1.NodePhaseRunning: - goc.log.Info("gateway is running") - err := goc.updateGatewayResources() + // Gateway is in error + case v1alpha1.NodePhaseError: + ctx.logger.Errorln("gateway is in error state. checking updates for gateway object...") + err := ctx.updateGatewayResources() if err != nil { + ctx.logger.WithError(err).Errorln("failed to update resources for the gateway") return err } + ctx.markGatewayPhase(v1alpha1.NodePhaseRunning, "gateway is now active") default: - goc.log.WithField(common.LabelPhase, string(goc.gw.Status.Phase)).Panic("unknown gateway phase.") + ctx.logger.WithField(common.LabelPhase, string(ctx.gateway.Status.Phase)).Errorln("unknown gateway phase") } return nil } -func (goc *gwOperationCtx) createGatewayResources() error { - err := Validate(goc.gw) - if err != nil { - goc.log.WithError(err).Error("gateway validation failed") - err = errors.Wrap(err, "failed to validate gateway") - goc.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) - return err - } - // Gateway pod has two components, - // 1) Gateway Server - Listen events from event source and dispatches the event to gateway client - // 2) Gateway Client - Listens for events from gateway server, convert them into cloudevents specification - // compliant events and dispatch them to watchers. - pod, err := goc.createGatewayPod() - if err != nil { - err = errors.Wrap(err, "failed to create gateway pod") - goc.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) - return err - } - goc.log.WithField(common.LabelPodName, pod.Name).Info("gateway pod is created") +// updateGatewayState updates the gateway state +func (ctx *gatewayContext) updateGatewayState() { + if ctx.updated { + var err error + eventType := common.StateChangeEventType + labels := map[string]string{ + common.LabelResourceName: ctx.gateway.Name, + LabelPhase: string(ctx.gateway.Status.Phase), + LabelControllerInstanceID: ctx.controller.Config.InstanceID, + common.LabelOperation: "persist_gateway_state", + } - // expose gateway if service is configured - if goc.gw.Spec.Service != nil { - svc, err := goc.createGatewayService() + ctx.gateway, err = PersistUpdates(ctx.controller.gatewayClient, ctx.gateway, ctx.logger) if err != nil { - err = errors.Wrap(err, "failed to create gateway service") - goc.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) - return err + ctx.logger.WithError(err).Errorln("failed to persist gateway update, escalating...") + eventType = common.EscalationEventType } - goc.log.WithField(common.LabelServiceName, svc.Name).Info("gateway service is created") - } - goc.log.Info("marking gateway as active") - goc.markGatewayPhase(v1alpha1.NodePhaseRunning, "gateway is active") - return nil -} - -func (goc *gwOperationCtx) createGatewayPod() (*corev1.Pod, error) { - pod, err := goc.gwrctx.newGatewayPod() - if err != nil { - goc.log.WithError(err).Error("failed to initialize pod for gateway") - return nil, err - } - pod, err = goc.gwrctx.createGatewayPod(pod) - if err != nil { - goc.log.WithError(err).Error("failed to create pod for gateway") - return nil, err + labels[common.LabelEventType] = string(eventType) + if err := common.GenerateK8sEvent(ctx.controller.k8sClient, + "persist update", + eventType, + "gateway state update", + ctx.gateway.Name, + ctx.gateway.Namespace, + ctx.controller.Config.InstanceID, + gateway.Kind, + labels, + ); err != nil { + ctx.logger.WithError(err).Errorln("failed to create K8s event to logger gateway state persist operation") + return + } + ctx.logger.Infoln("successfully persisted gateway resource update and created K8s event") } - return pod, nil + ctx.updated = false } -func (goc *gwOperationCtx) createGatewayService() (*corev1.Service, error) { - svc, err := goc.gwrctx.newGatewayService() - if err != nil { - goc.log.WithError(err).Error("failed to initialize service for gateway") - return nil, err - } - svc, err = goc.gwrctx.createGatewayService(svc) - if err != nil { - goc.log.WithError(err).Error("failed to create service for gateway") - return nil, err - } - return svc, nil -} +// mark the gateway phase +func (ctx *gatewayContext) markGatewayPhase(phase v1alpha1.NodePhase, message string) { + justCompleted := ctx.gateway.Status.Phase != phase + if justCompleted { + ctx.logger.WithFields( + map[string]interface{}{ + "old": string(ctx.gateway.Status.Phase), + "new": string(phase), + }, + ).Infoln("phase changed") -func (goc *gwOperationCtx) updateGatewayResources() error { - err := Validate(goc.gw) - if err != nil { - goc.log.WithError(err).Error("gateway validation failed") - err = errors.Wrap(err, "failed to validate gateway") - if goc.gw.Status.Phase != v1alpha1.NodePhaseError { - goc.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) + ctx.gateway.Status.Phase = phase + if ctx.gateway.ObjectMeta.Labels == nil { + ctx.gateway.ObjectMeta.Labels = make(map[string]string) + } + if ctx.gateway.Annotations == nil { + ctx.gateway.Annotations = make(map[string]string) } - return err - } - _, podChanged, err := goc.updateGatewayPod() - if err != nil { - err = errors.Wrap(err, "failed to update gateway pod") - goc.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) - return err + ctx.gateway.ObjectMeta.Labels[LabelPhase] = string(phase) + // add annotations so a resource sensor can watch this gateway. + ctx.gateway.Annotations[LabelPhase] = string(phase) } - _, svcChanged, err := goc.updateGatewayService() - if err != nil { - err = errors.Wrap(err, "failed to update gateway service") - goc.markGatewayPhase(v1alpha1.NodePhaseError, err.Error()) - return err + if ctx.gateway.Status.StartedAt.IsZero() { + ctx.gateway.Status.StartedAt = metav1.Time{Time: time.Now().UTC()} } - if goc.gw.Status.Phase != v1alpha1.NodePhaseRunning && (podChanged || svcChanged) { - goc.markGatewayPhase(v1alpha1.NodePhaseRunning, "gateway is active") - } + ctx.logger.WithFields( + map[string]interface{}{ + "old": ctx.gateway.Status.Message, + "new": message, + }, + ).Infoln("phase change message") - return nil + ctx.gateway.Status.Message = message + ctx.updated = true } -func (goc *gwOperationCtx) updateGatewayPod() (*corev1.Pod, bool, error) { - // Check if gateway spec has changed for pod. - existingPod, err := goc.gwrctx.getGatewayPod() - if err != nil { - goc.log.WithError(err).Error("failed to get pod for gateway") - return nil, false, err - } +// PersistUpdates of the gateway resource +func PersistUpdates(client gwclient.Interface, gw *v1alpha1.Gateway, log *logrus.Logger) (*v1alpha1.Gateway, error) { + gatewayClient := client.ArgoprojV1alpha1().Gateways(gw.ObjectMeta.Namespace) - // create a new pod spec - newPod, err := goc.gwrctx.newGatewayPod() - if err != nil { - goc.log.WithError(err).Error("failed to initialize pod for gateway") - return nil, false, err - } + // in case persist update fails + oldgw := gw.DeepCopy() - // check if pod spec remained unchanged - if existingPod != nil { - if existingPod.Annotations != nil && existingPod.Annotations[common.AnnotationGatewayResourceSpecHashName] == newPod.Annotations[common.AnnotationGatewayResourceSpecHashName] { - goc.log.WithField(common.LabelPodName, existingPod.Name).Debug("gateway pod spec unchanged") - return nil, false, nil + gw, err := gatewayClient.Update(gw) + if err != nil { + log.WithError(err).Warn("error updating gateway") + if errors.IsConflict(err) { + return oldgw, err } - - // By now we are sure that the spec changed, so lets go ahead and delete the exisitng gateway pod. - goc.log.WithField(common.LabelPodName, existingPod.Name).Info("gateway pod spec changed") - - err := goc.gwrctx.deleteGatewayPod(existingPod) + log.Info("re-applying updates on latest version and retrying update") + err = ReapplyUpdates(client, gw) if err != nil { - goc.log.WithError(err).Error("failed to delete pod for gateway") - return nil, false, err + log.WithError(err).Error("failed to re-apply update") + return oldgw, err } - - goc.log.WithField(common.LabelPodName, existingPod.Name).Info("gateway pod is deleted") } - - // Create new pod for updated gateway spec. - createdPod, err := goc.gwrctx.createGatewayPod(newPod) - if err != nil { - goc.log.WithError(err).Error("failed to create pod for gateway") - return nil, false, err - } - goc.log.WithError(err).WithField(common.LabelPodName, newPod.Name).Info("gateway pod is created") - - return createdPod, true, nil + log.WithField(common.LabelPhase, string(gw.Status.Phase)).Info("gateway state updated successfully") + return gw, nil } -func (goc *gwOperationCtx) updateGatewayService() (*corev1.Service, bool, error) { - // Check if gateway spec has changed for service. - existingSvc, err := goc.gwrctx.getGatewayService() - if err != nil { - goc.log.WithError(err).Error("failed to get service for gateway") - return nil, false, err - } - - // create a new service spec - newSvc, err := goc.gwrctx.newGatewayService() - if err != nil { - goc.log.WithError(err).Error("failed to initialize service for gateway") - return nil, false, err - } - - if existingSvc != nil { - // updated spec doesn't have service defined, delete existing service. - if newSvc == nil { - if err := goc.gwrctx.deleteGatewayService(existingSvc); err != nil { - return nil, false, err +// ReapplyUpdates to gateway resource +func ReapplyUpdates(client gwclient.Interface, gw *v1alpha1.Gateway) error { + return wait.ExponentialBackoff(common.DefaultRetry, func() (bool, error) { + gatewayClient := client.ArgoprojV1alpha1().Gateways(gw.Namespace) + g, err := gatewayClient.Update(gw) + if err != nil { + if !common.IsRetryableKubeAPIError(err) { + return false, err } - return nil, true, nil + return false, nil } - - // check if service spec remained unchanged - if existingSvc.Annotations[common.AnnotationGatewayResourceSpecHashName] == newSvc.Annotations[common.AnnotationGatewayResourceSpecHashName] { - goc.log.WithField(common.LabelServiceName, existingSvc.Name).Debug("gateway service spec unchanged") - return nil, false, nil - } - - // service spec changed, delete existing service and create new one - goc.log.WithField(common.LabelServiceName, existingSvc.Name).Info("gateway service spec changed") - - if err := goc.gwrctx.deleteGatewayService(existingSvc); err != nil { - return nil, false, err - } - } else if newSvc == nil { - // gateway service doesn't exist originally - return nil, false, nil - } - - // change createGatewayService to take a service spec - createdSvc, err := goc.gwrctx.createGatewayService(newSvc) - if err != nil { - goc.log.WithError(err).Error("failed to create service for gateway") - return nil, false, err - } - goc.log.WithField(common.LabelServiceName, createdSvc.Name).Info("gateway service is created") - - return createdSvc, true, nil -} - -// mark the overall gateway phase -func (goc *gwOperationCtx) markGatewayPhase(phase v1alpha1.NodePhase, message string) { - justCompleted := goc.gw.Status.Phase != phase - if justCompleted { - goc.log.WithFields( - map[string]interface{}{ - "old": string(goc.gw.Status.Phase), - "new": string(phase), - }, - ).Info("phase changed") - - goc.gw.Status.Phase = phase - if goc.gw.ObjectMeta.Labels == nil { - goc.gw.ObjectMeta.Labels = make(map[string]string) - } - if goc.gw.Annotations == nil { - goc.gw.Annotations = make(map[string]string) - } - goc.gw.ObjectMeta.Labels[common.LabelGatewayKeyPhase] = string(phase) - // add annotations so a resource sensor can watch this gateway. - goc.gw.Annotations[common.LabelGatewayKeyPhase] = string(phase) - } - if goc.gw.Status.StartedAt.IsZero() { - goc.gw.Status.StartedAt = metav1.Time{Time: time.Now().UTC()} - } - goc.log.WithFields( - map[string]interface{}{ - "old": string(goc.gw.Status.Message), - "new": message, - }, - ).Info("message") - goc.gw.Status.Message = message - goc.updated = true + gw = g + return true, nil + }) } diff --git a/controllers/gateway/operator_test.go b/controllers/gateway/operator_test.go index 09528a9a54..419f30d5be 100644 --- a/controllers/gateway/operator_test.go +++ b/controllers/gateway/operator_test.go @@ -19,337 +19,147 @@ package gateway import ( "testing" + "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/cache" ) -var testGatewayStr = `apiVersion: argoproj.io/v1alpha1 -kind: Gateway -metadata: - name: webhook-gateway - namespace: argo-events - labels: - gateways.argoproj.io/gateway-controller-instanceid: argo-events - gateway-name: "webhook-gateway" - argo-events-gateway-version: v0.11 -spec: - eventSource: "webhook-gateway-configmap" - type: "webhook" - processorPort: "9330" - eventProtocol: - type: "NATS" - nats: - url: "nats://nats.argo-events:4222" - type: "Standard" - eventVersion: "1.0" - template: - metadata: - name: "webhook-gateway" - labels: - gateway-name: "webhook-gateway" - spec: - containers: - - name: "gateway-client" - image: "argoproj/gateway-client:v0.6.2" - imagePullPolicy: "Always" - command: ["/bin/gateway-client"] - - name: "webhook-events" - image: "argoproj/webhook-gateway:v0.6.2" - imagePullPolicy: "Always" - command: ["/bin/webhook-gateway"] - serviceAccountName: "argo-events-sa" - service: - metadata: - name: webhook-gateway-svc - spec: - selector: - gateway-name: "webhook-gateway" - ports: - - port: 12000 - targetPort: 12000 - type: LoadBalancer` - -var ( - gatewayPodName = "webhook-gateway" - gatewaySvcName = "webhook-gateway-svc" -) +func TestGatewayOperateLifecycle(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + gateway, err := controller.gatewayClient.ArgoprojV1alpha1().Gateways(gatewayObj.Namespace).Create(gatewayObj) + assert.Nil(t, err) + assert.NotNil(t, gateway) + + tests := []struct { + name string + updateStateFunc func() + testFunc func(oldMetadata *v1alpha1.GatewayResource) + }{ + { + name: "process a new gateway object", + updateStateFunc: func() {}, + testFunc: func(oldMetadata *v1alpha1.GatewayResource) { + assert.Nil(t, oldMetadata) + deployment, err := controller.k8sClient.AppsV1().Deployments(ctx.gateway.Status.Resources.Deployment.Namespace).Get(ctx.gateway.Status.Resources.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + service, err := controller.k8sClient.CoreV1().Services(ctx.gateway.Status.Resources.Service.Namespace).Get(ctx.gateway.Status.Resources.Service.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.NotNil(t, ctx.gateway.Status.Resources) + assert.Equal(t, ctx.gateway.Status.Resources.Deployment.Name, deployment.Name) + assert.Equal(t, ctx.gateway.Status.Resources.Service.Name, service.Name) + gateway, err := controller.gatewayClient.ArgoprojV1alpha1().Gateways(ctx.gateway.Namespace).Get(ctx.gateway.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, gateway) + assert.Equal(t, gateway.Status.Phase, v1alpha1.NodePhaseRunning) + ctx.gateway = gateway.DeepCopy() + }, + }, + { + name: "process a updated gateway object", + updateStateFunc: func() { + ctx.gateway.Spec.Template.Spec.Containers[0].Name = "new-name" + }, + testFunc: func(oldMetadata *v1alpha1.GatewayResource) { + currentMetadata := ctx.gateway.Status.Resources.DeepCopy() + assert.NotEqual(t, oldMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash], currentMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash]) + assert.Equal(t, oldMetadata.Service.Annotations[common.AnnotationResourceSpecHash], currentMetadata.Service.Annotations[common.AnnotationResourceSpecHash]) + }, + }, + { + name: "process a gateway object in error", + updateStateFunc: func() { + ctx.gateway.Status.Phase = v1alpha1.NodePhaseError + ctx.gateway.Spec.Template.Spec.Containers[0].Name = "fixed-name" + }, + testFunc: func(oldMetadata *v1alpha1.GatewayResource) { + currentMetadata := ctx.gateway.Status.Resources.DeepCopy() + assert.NotEqual(t, oldMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash], currentMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash]) + assert.Equal(t, oldMetadata.Service.Annotations[common.AnnotationResourceSpecHash], currentMetadata.Service.Annotations[common.AnnotationResourceSpecHash]) + gateway, err := controller.gatewayClient.ArgoprojV1alpha1().Gateways(ctx.gateway.Namespace).Get(ctx.gateway.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, gateway) + assert.Equal(t, gateway.Status.Phase, v1alpha1.NodePhaseRunning) + }, + }, + } -func getGateway() (*v1alpha1.Gateway, error) { - gwBytes := []byte(testGatewayStr) - var gateway v1alpha1.Gateway - err := yaml.Unmarshal(gwBytes, &gateway) - return &gateway, err + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + oldMetadata := ctx.gateway.Status.Resources.DeepCopy() + test.updateStateFunc() + err := ctx.operate() + assert.Nil(t, err) + test.testFunc(oldMetadata) + }) + } } -func waitForAllInformers(done chan struct{}, controller *GatewayController) { - cache.WaitForCacheSync(done, controller.informer.HasSynced) - cache.WaitForCacheSync(done, controller.podInformer.Informer().HasSynced) - cache.WaitForCacheSync(done, controller.svcInformer.Informer().HasSynced) +func TestPersistUpdates(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + gateway, err := controller.gatewayClient.ArgoprojV1alpha1().Gateways(gatewayObj.Namespace).Create(gatewayObj) + assert.Nil(t, err) + assert.NotNil(t, gateway) + + ctx.gateway = gateway + ctx.gateway.Spec.Template.Name = "updated-name" + gateway, err = PersistUpdates(controller.gatewayClient, ctx.gateway, controller.logger) + assert.Nil(t, err) + assert.Equal(t, gateway.Spec.Template.Name, "updated-name") } -func getPodAndService(controller *GatewayController, namespace string) (*corev1.Pod, *corev1.Service, error) { - pod, err := controller.kubeClientset.CoreV1().Pods(namespace).Get(gatewayPodName, metav1.GetOptions{}) - if err != nil { - return nil, nil, err - } - svc, err := controller.kubeClientset.CoreV1().Services(namespace).Get(gatewaySvcName, metav1.GetOptions{}) - if err != nil { - return nil, nil, err - } - return pod, svc, err +func TestReapplyUpdates(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + gateway, err := controller.gatewayClient.ArgoprojV1alpha1().Gateways(gatewayObj.Namespace).Create(gatewayObj) + assert.Nil(t, err) + assert.NotNil(t, gateway) + + ctx.gateway = gateway + ctx.gateway.Spec.Template.Name = "updated-name" + gateway, err = PersistUpdates(controller.gatewayClient, ctx.gateway, controller.logger) + assert.Nil(t, err) + assert.Equal(t, gateway.Spec.Template.Name, "updated-name") } -func deletePodAndService(controller *GatewayController, namespace string) error { - err := controller.kubeClientset.CoreV1().Pods(namespace).Delete(gatewayPodName, &metav1.DeleteOptions{}) - if err != nil { - return err - } - err = controller.kubeClientset.CoreV1().Services(namespace).Delete(gatewaySvcName, &metav1.DeleteOptions{}) - return err +func TestOperator_MarkPhase(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + assert.Equal(t, ctx.gateway.Status.Phase, v1alpha1.NodePhaseNew) + assert.Equal(t, ctx.gateway.Status.Message, "") + ctx.gateway.Status.Phase = v1alpha1.NodePhaseRunning + ctx.markGatewayPhase(v1alpha1.NodePhaseRunning, "node is active") + assert.Equal(t, ctx.gateway.Status.Phase, v1alpha1.NodePhaseRunning) + assert.Equal(t, ctx.gateway.Status.Message, "node is active") } -func TestGatewayOperateLifecycle(t *testing.T) { - done := make(chan struct{}) - convey.Convey("Given a gateway resource spec, parse it", t, func() { - fakeController := getGatewayController() - gateway, err := getGateway() - convey.Convey("Make sure no error occurs", func() { - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Create the gateway", func() { - gateway, err = fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(fakeController.Config.Namespace).Create(gateway) - - convey.Convey("No error should occur and gateway resource should not be empty", func() { - convey.So(err, convey.ShouldBeNil) - convey.So(gateway, convey.ShouldNotBeNil) - - convey.Convey("Create a new gateway operation context", func() { - goc := newGatewayOperationCtx(gateway, fakeController) - convey.So(goc, convey.ShouldNotBeNil) - - convey.Convey("Operate on new gateway", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseNew, "test") - err := goc.operate() - - convey.Convey("Operation must succeed", func() { - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("A gateway pod and service must be created", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Go to running state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) - }) - }) - }) - }) - - convey.Convey("Operate on gateway in running state", func() { - err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Delete(gateway.Name, &metav1.DeleteOptions{}) - convey.So(err, convey.ShouldBeNil) - gateway, err = fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Create(gateway) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway, convey.ShouldNotBeNil) - - goc.markGatewayPhase(v1alpha1.NodePhaseNew, "test") - - // Operate it once to create pod and service - waitForAllInformers(done, fakeController) - err = goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Operation must succeed", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseRunning, "test") - - waitForAllInformers(done, fakeController) - err := goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Untouch pod and service", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Stay in running state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) - }) - }) - }) - - convey.Convey("Delete pod and service", func() { - err := deletePodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Operation must succeed", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseRunning, "test") - - waitForAllInformers(done, fakeController) - err := goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Create pod and service", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Stay in running state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) - }) - }) - }) - }) - - convey.Convey("Change pod and service spec", func() { - goc.gwrctx.gw.Spec.Template.Spec.RestartPolicy = "Never" - goc.gwrctx.gw.Spec.Service.Spec.ClusterIP = "127.0.0.1" - - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Operation must succeed", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseRunning, "test") - - waitForAllInformers(done, fakeController) - err := goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Delete pod and service", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod.Spec.RestartPolicy, convey.ShouldEqual, "Never") - convey.So(gatewaySvc.Spec.ClusterIP, convey.ShouldEqual, "127.0.0.1") - - convey.Convey("Stay in running state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) - }) - }) - }) - }) - }) - - convey.Convey("Operate on gateway in error state", func() { - err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Delete(gateway.Name, &metav1.DeleteOptions{}) - convey.So(err, convey.ShouldBeNil) - gateway, err = fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Create(gateway) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway, convey.ShouldNotBeNil) - - goc.markGatewayPhase(v1alpha1.NodePhaseNew, "test") - - // Operate it once to create pod and service - waitForAllInformers(done, fakeController) - err = goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Operation must succeed", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseError, "test") - - waitForAllInformers(done, fakeController) - err := goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Untouch pod and service", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Stay in error state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseError) - }) - }) - }) - - convey.Convey("Delete pod and service", func() { - err := deletePodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Operation must succeed", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseError, "test") - - waitForAllInformers(done, fakeController) - err := goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Create pod and service", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Go to running state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) - }) - }) - }) - }) - - convey.Convey("Change pod and service spec", func() { - goc.gwrctx.gw.Spec.Template.Spec.RestartPolicy = "Never" - goc.gwrctx.gw.Spec.Service.Spec.ClusterIP = "127.0.0.1" - - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod, convey.ShouldNotBeNil) - convey.So(gatewaySvc, convey.ShouldNotBeNil) - - convey.Convey("Operation must succeed", func() { - goc.markGatewayPhase(v1alpha1.NodePhaseError, "test") - - waitForAllInformers(done, fakeController) - err := goc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, fakeController) - - convey.Convey("Delete pod and service", func() { - gatewayPod, gatewaySvc, err := getPodAndService(fakeController, gateway.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(gatewayPod.Spec.RestartPolicy, convey.ShouldEqual, "Never") - convey.So(gatewaySvc.Spec.ClusterIP, convey.ShouldEqual, "127.0.0.1") - - convey.Convey("Go to running state", func() { - gateway, err := fakeController.gatewayClientset.ArgoprojV1alpha1().Gateways(gateway.Namespace).Get(gateway.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(gateway.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) - }) - }) - }) - }) - }) - }) - }) - }) - }) - }) +func TestOperator_UpdateGatewayState(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + gateway, err := controller.gatewayClient.ArgoprojV1alpha1().Gateways(gatewayObj.Namespace).Create(gatewayObj) + assert.Nil(t, err) + assert.NotNil(t, gateway) + ctx.gateway = gateway.DeepCopy() + assert.Equal(t, ctx.gateway.Status.Phase, v1alpha1.NodePhaseNew) + ctx.gateway.Status.Phase = v1alpha1.NodePhaseRunning + ctx.updated = true + ctx.updateGatewayState() + gateway, err = controller.gatewayClient.ArgoprojV1alpha1().Gateways(ctx.gateway.Namespace).Get(ctx.gateway.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, gateway) + ctx.gateway = gateway.DeepCopy() + assert.Equal(t, ctx.gateway.Status.Phase, v1alpha1.NodePhaseRunning) + + ctx.gateway.Status.Phase = v1alpha1.NodePhaseError + ctx.updated = false + ctx.updateGatewayState() + gateway, err = controller.gatewayClient.ArgoprojV1alpha1().Gateways(ctx.gateway.Namespace).Get(ctx.gateway.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, gateway) + ctx.gateway = gateway.DeepCopy() + assert.Equal(t, ctx.gateway.Status.Phase, v1alpha1.NodePhaseRunning) } diff --git a/controllers/gateway/resource.go b/controllers/gateway/resource.go index 4f59d794c2..6603ef9678 100644 --- a/controllers/gateway/resource.go +++ b/controllers/gateway/resource.go @@ -1,169 +1,248 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package gateway import ( "github.com/argoproj/argo-events/common" controllerscommon "github.com/argoproj/argo-events/controllers/common" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + "github.com/pkg/errors" + appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" ) -type gwResourceCtx struct { - // gw is the gateway-controller object - gw *v1alpha1.Gateway - // reference to the gateway-controller-controller - controller *GatewayController - - controllerscommon.ChildResourceContext +// buildServiceResource builds a new service that exposes gateway. +func (ctx *gatewayContext) buildServiceResource() (*corev1.Service, error) { + if ctx.gateway.Spec.Service == nil { + return nil, nil + } + service := ctx.gateway.Spec.Service.DeepCopy() + if err := controllerscommon.SetObjectMeta(ctx.gateway, service, v1alpha1.SchemaGroupVersionKind); err != nil { + return nil, err + } + return service, nil } -// NewGatewayResourceContext returns new gwResourceCtx -func NewGatewayResourceContext(gw *v1alpha1.Gateway, controller *GatewayController) gwResourceCtx { - return gwResourceCtx{ - gw: gw, - controller: controller, - ChildResourceContext: controllerscommon.ChildResourceContext{ - SchemaGroupVersionKind: v1alpha1.SchemaGroupVersionKind, - LabelOwnerName: common.LabelGatewayName, - LabelKeyOwnerControllerInstanceID: common.LabelKeyGatewayControllerInstanceID, - AnnotationOwnerResourceHashName: common.AnnotationGatewayResourceSpecHashName, - InstanceID: controller.Config.InstanceID, +// buildDeploymentResource builds a deployment resource for the gateway +func (ctx *gatewayContext) buildDeploymentResource() (*appv1.Deployment, error) { + if ctx.gateway.Spec.Template == nil { + return nil, errors.New("gateway template can't be empty") + } + + podTemplate := ctx.gateway.Spec.Template.DeepCopy() + + replica := int32(ctx.gateway.Spec.Replica) + if replica == 0 { + replica = 1 + } + + deployment := &appv1.Deployment{ + ObjectMeta: podTemplate.ObjectMeta, + Spec: appv1.DeploymentSpec{ + Replicas: &replica, + Template: *podTemplate, }, } + + if deployment.Spec.Template.Labels == nil { + deployment.Spec.Template.Labels = map[string]string{} + } + deployment.Spec.Template.Labels[common.LabelObjectName] = ctx.gateway.Name + + if deployment.Spec.Selector == nil { + deployment.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{}, + } + } + deployment.Spec.Selector.MatchLabels[common.LabelObjectName] = ctx.gateway.Name + + envVars := []corev1.EnvVar{ + { + Name: common.EnvVarNamespace, + Value: ctx.gateway.Namespace, + }, + { + Name: common.EnvVarEventSource, + Value: ctx.gateway.Spec.EventSourceRef.Name, + }, + { + Name: common.EnvVarResourceName, + Value: ctx.gateway.Name, + }, + { + Name: common.EnvVarControllerInstanceID, + Value: ctx.controller.Config.InstanceID, + }, + { + Name: common.EnvVarGatewayServerPort, + Value: ctx.gateway.Spec.ProcessorPort, + }, + } + + for i, container := range deployment.Spec.Template.Spec.Containers { + container.Env = append(container.Env, envVars...) + deployment.Spec.Template.Spec.Containers[i] = container + } + + if err := controllerscommon.SetObjectMeta(ctx.gateway, deployment, v1alpha1.SchemaGroupVersionKind); err != nil { + return nil, errors.Wrap(err, "failed to set the object metadata on the deployment object") + } + + return deployment, nil } -// gatewayResourceLabelSelector returns label selector of the gateway of the context -func (grc *gwResourceCtx) gatewayResourceLabelSelector() (labels.Selector, error) { - req, err := labels.NewRequirement(common.LabelGatewayName, selection.Equals, []string{grc.gw.Name}) +// createGatewayResources creates gateway deployment and service +func (ctx *gatewayContext) createGatewayResources() error { + if ctx.gateway.Status.Resources == nil { + ctx.gateway.Status.Resources = &v1alpha1.GatewayResource{} + } + + deployment, err := ctx.createGatewayDeployment() if err != nil { - return nil, err + return err } - return labels.NewSelector().Add(*req), nil -} + ctx.gateway.Status.Resources.Deployment = &deployment.ObjectMeta + ctx.logger.WithField("name", deployment.Name).WithField("namespace", deployment.Namespace).Infoln("gateway deployment is created") -// createGatewayService creates a given service -func (grc *gwResourceCtx) createGatewayService(svc *corev1.Service) (*corev1.Service, error) { - return grc.controller.kubeClientset.CoreV1().Services(grc.gw.Namespace).Create(svc) -} + if ctx.gateway.Spec.Service != nil { + service, err := ctx.createGatewayService() + if err != nil { + return err + } + ctx.gateway.Status.Resources.Service = &service.ObjectMeta + ctx.logger.WithField("name", service.Name).WithField("namespace", service.Namespace).Infoln("gateway service is created") + } -// deleteGatewayService deletes a given service -func (grc *gwResourceCtx) deleteGatewayService(svc *corev1.Service) error { - return grc.controller.kubeClientset.CoreV1().Services(grc.gw.Namespace).Delete(svc.Name, &metav1.DeleteOptions{}) + return nil } -// getGatewayService returns the service of gateway -func (grc *gwResourceCtx) getGatewayService() (*corev1.Service, error) { - selector, err := grc.gatewayResourceLabelSelector() +// createGatewayDeployment creates a deployment for the gateway +func (ctx *gatewayContext) createGatewayDeployment() (*appv1.Deployment, error) { + deployment, err := ctx.buildDeploymentResource() if err != nil { return nil, err } - svcs, err := grc.controller.svcInformer.Lister().Services(grc.gw.Namespace).List(selector) + return ctx.controller.k8sClient.AppsV1().Deployments(deployment.Namespace).Create(deployment) +} + +// createGatewayService creates a service for the gateway +func (ctx *gatewayContext) createGatewayService() (*corev1.Service, error) { + svc, err := ctx.buildServiceResource() if err != nil { return nil, err } - if len(svcs) == 0 { - return nil, nil - } - return svcs[0], nil + return ctx.controller.k8sClient.CoreV1().Services(svc.Namespace).Create(svc) } -// newGatewayService returns a new service that exposes gateway. -func (grc *gwResourceCtx) newGatewayService() (*corev1.Service, error) { - servicTemplateSpec := grc.gw.Spec.Service.DeepCopy() - if servicTemplateSpec == nil { - return nil, nil +// updateGatewayResources updates gateway deployment and service +func (ctx *gatewayContext) updateGatewayResources() error { + deployment, err := ctx.updateGatewayDeployment() + if err != nil { + return err } - service := &corev1.Service{ - ObjectMeta: servicTemplateSpec.ObjectMeta, - Spec: servicTemplateSpec.Spec, + if deployment != nil { + ctx.gateway.Status.Resources.Deployment = &deployment.ObjectMeta + ctx.logger.WithField("name", deployment.Name).WithField("namespace", deployment.Namespace).Infoln("gateway deployment is updated") } - if service.Namespace == "" { - service.Namespace = grc.gw.Namespace + + service, err := ctx.updateGatewayService() + if err != nil { + return err } - if service.Name == "" { - service.Name = common.DefaultServiceName(grc.gw.Name) + if service != nil { + ctx.gateway.Status.Resources.Service = &service.ObjectMeta + ctx.logger.WithField("name", service.Name).WithField("namespace", service.Namespace).Infoln("gateway service is updated") + return nil } - err := grc.SetObjectMeta(grc.gw, service) - return service, err + ctx.gateway.Status.Resources.Service = nil + return nil } -// getGatewayPod returns the pod of gateway -func (grc *gwResourceCtx) getGatewayPod() (*corev1.Pod, error) { - selector, err := grc.gatewayResourceLabelSelector() +// updateGatewayDeployment updates the gateway deployment +func (ctx *gatewayContext) updateGatewayDeployment() (*appv1.Deployment, error) { + newDeployment, err := ctx.buildDeploymentResource() if err != nil { return nil, err } - pods, err := grc.controller.podInformer.Lister().Pods(grc.gw.Namespace).List(selector) + + currentMetadata := ctx.gateway.Status.Resources.Deployment + if currentMetadata == nil { + return nil, errors.New("deployment metadata is expected to be set in gateway object") + } + + currentDeployment, err := ctx.controller.k8sClient.AppsV1().Deployments(currentMetadata.Namespace).Get(currentMetadata.Name, metav1.GetOptions{}) if err != nil { + if apierr.IsNotFound(err) { + return ctx.controller.k8sClient.AppsV1().Deployments(newDeployment.Namespace).Create(newDeployment) + } return nil, err } - if len(pods) == 0 { - return nil, nil + + if currentDeployment.Annotations != nil && currentDeployment.Annotations[common.AnnotationResourceSpecHash] != newDeployment.Annotations[common.AnnotationResourceSpecHash] { + if err := ctx.controller.k8sClient.AppsV1().Deployments(currentDeployment.Namespace).Delete(currentDeployment.Name, &metav1.DeleteOptions{}); err != nil { + return nil, err + } + return ctx.controller.k8sClient.AppsV1().Deployments(newDeployment.Namespace).Create(newDeployment) } - return pods[0], nil -} -// createGatewayPod creates a given pod -func (grc *gwResourceCtx) createGatewayPod(pod *corev1.Pod) (*corev1.Pod, error) { - return grc.controller.kubeClientset.CoreV1().Pods(grc.gw.Namespace).Create(pod) + return nil, nil } -// deleteGatewayPod deletes a given pod -func (grc *gwResourceCtx) deleteGatewayPod(pod *corev1.Pod) error { - return grc.controller.kubeClientset.CoreV1().Pods(grc.gw.Namespace).Delete(pod.Name, &metav1.DeleteOptions{}) -} +// updateGatewayService updates the gateway service +func (ctx *gatewayContext) updateGatewayService() (*corev1.Service, error) { + newService, err := ctx.buildServiceResource() + if err != nil { + return nil, err + } + if newService == nil && ctx.gateway.Status.Resources.Service != nil { + if err := ctx.controller.k8sClient.CoreV1().Services(ctx.gateway.Status.Resources.Service.Namespace).Delete(ctx.gateway.Status.Resources.Service.Name, &metav1.DeleteOptions{}); err != nil { + return nil, err + } + return nil, nil + } -// newGatewayPod returns a new pod of gateway -func (grc *gwResourceCtx) newGatewayPod() (*corev1.Pod, error) { - podTemplateSpec := grc.gw.Spec.Template.DeepCopy() - pod := &corev1.Pod{ - ObjectMeta: podTemplateSpec.ObjectMeta, - Spec: podTemplateSpec.Spec, + if newService == nil { + return nil, nil } - if pod.Namespace == "" { - pod.Namespace = grc.gw.Namespace + + if ctx.gateway.Status.Resources.Service == nil { + return ctx.controller.k8sClient.CoreV1().Services(newService.Namespace).Create(newService) } - if pod.Name == "" { - pod.Name = grc.gw.Name + + currentMetadata := ctx.gateway.Status.Resources.Service + currentService, err := ctx.controller.k8sClient.CoreV1().Services(currentMetadata.Namespace).Get(currentMetadata.Name, metav1.GetOptions{}) + if err != nil { + return ctx.controller.k8sClient.CoreV1().Services(newService.Namespace).Create(newService) } - grc.setupContainersForGatewayPod(pod) - err := grc.SetObjectMeta(grc.gw, pod) - return pod, err -} -// containers required for gateway deployment -func (grc *gwResourceCtx) setupContainersForGatewayPod(pod *corev1.Pod) { - // env variables - envVars := []corev1.EnvVar{ - { - Name: common.EnvVarGatewayNamespace, - Value: grc.gw.Namespace, - }, - { - Name: common.EnvVarGatewayEventSourceConfigMap, - Value: grc.gw.Spec.EventSource, - }, - { - Name: common.EnvVarGatewayName, - Value: grc.gw.Name, - }, - { - Name: common.EnvVarGatewayControllerInstanceID, - Value: grc.controller.Config.InstanceID, - }, - { - Name: common.EnvVarGatewayControllerName, - Value: common.LabelGatewayControllerName, - }, - { - Name: common.EnvVarGatewayServerPort, - Value: grc.gw.Spec.ProcessorPort, - }, + if currentMetadata == nil { + return nil, errors.New("service metadata is expected to be set in gateway object") } - for i, container := range pod.Spec.Containers { - container.Env = append(container.Env, envVars...) - pod.Spec.Containers[i] = container + + if currentService.Annotations != nil && currentService.Annotations[common.AnnotationResourceSpecHash] != newService.Annotations[common.AnnotationResourceSpecHash] { + if err := ctx.controller.k8sClient.CoreV1().Services(currentMetadata.Namespace).Delete(currentMetadata.Name, &metav1.DeleteOptions{}); err != nil { + return nil, err + } + if ctx.gateway.Spec.Service != nil { + return ctx.controller.k8sClient.CoreV1().Services(newService.Namespace).Create(newService) + } } + + return currentService, nil } diff --git a/controllers/gateway/resource_test.go b/controllers/gateway/resource_test.go new file mode 100644 index 0000000000..553d6af09a --- /dev/null +++ b/controllers/gateway/resource_test.go @@ -0,0 +1,316 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gateway + +import ( + "testing" + + "github.com/argoproj/argo-events/common" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + apierror "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +var gatewayObj = &v1alpha1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-gateway", + Namespace: common.DefaultControllerNamespace, + }, + Spec: v1alpha1.GatewaySpec{ + EventSourceRef: &v1alpha1.EventSourceRef{ + Name: "fake-event-source", + }, + Replica: 1, + Type: apicommon.WebhookEvent, + ProcessorPort: "8080", + Template: &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-gateway", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "gateway-client", + Image: "argoproj/gateway-client", + ImagePullPolicy: corev1.PullAlways, + }, + { + Name: "gateway-server", + ImagePullPolicy: corev1.PullAlways, + Image: "argoproj/webhook-gateway", + }, + }, + }, + }, + Service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-gateway-svc", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + Selector: map[string]string{ + "gateway-name": "webhook-gateway", + }, + Ports: []corev1.ServicePort{ + { + Name: "server-port", + Port: 12000, + TargetPort: intstr.FromInt(12000), + }, + }, + }, + }, + EventProtocol: &apicommon.EventProtocol{ + Type: apicommon.HTTP, + Http: apicommon.Http{ + Port: "9330", + }, + }, + Watchers: &v1alpha1.NotificationWatchers{ + Sensors: []v1alpha1.SensorNotificationWatcher{ + { + Name: "fake-sensor", + Namespace: common.DefaultControllerNamespace, + }, + }, + }, + }, +} + +func TestResource_BuildServiceResource(t *testing.T) { + controller := newController() + opCtx := newGatewayContext(gatewayObj, controller) + svc := opCtx.gateway.Spec.Service.DeepCopy() + opCtx.gateway.Spec.Service = nil + + // If no service is defined + service, err := opCtx.buildServiceResource() + assert.Nil(t, err) + assert.Nil(t, service) + opCtx.gateway.Spec.Service = svc + + // If service is defined + service, err = opCtx.buildServiceResource() + assert.Nil(t, err) + assert.NotNil(t, service) + + opCtx.gateway.Spec.Service.Name = "" + opCtx.gateway.Spec.Service.Namespace = "" + + service, err = opCtx.buildServiceResource() + assert.Nil(t, err) + assert.NotNil(t, service) + assert.Equal(t, service.Name, opCtx.gateway.Name) + assert.Equal(t, service.Namespace, opCtx.gateway.Namespace) + + newSvc, err := controller.k8sClient.CoreV1().Services(service.Namespace).Create(service) + assert.Nil(t, err) + assert.NotNil(t, newSvc) + assert.Equal(t, newSvc.Name, opCtx.gateway.Name) + assert.Equal(t, len(newSvc.Spec.Ports), 1) + assert.Equal(t, newSvc.Spec.Type, corev1.ServiceTypeLoadBalancer) +} + +func TestResource_BuildDeploymentResource(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj, controller) + deployment, err := ctx.buildDeploymentResource() + assert.Nil(t, err) + assert.NotNil(t, deployment) + + for _, container := range deployment.Spec.Template.Spec.Containers { + assert.NotNil(t, container.Env) + assert.Equal(t, container.Env[0].Name, common.EnvVarNamespace) + assert.Equal(t, container.Env[0].Value, ctx.gateway.Namespace) + assert.Equal(t, container.Env[1].Name, common.EnvVarEventSource) + assert.Equal(t, container.Env[1].Value, ctx.gateway.Spec.EventSourceRef.Name) + assert.Equal(t, container.Env[2].Name, common.EnvVarResourceName) + assert.Equal(t, container.Env[2].Value, ctx.gateway.Name) + assert.Equal(t, container.Env[3].Name, common.EnvVarControllerInstanceID) + assert.Equal(t, container.Env[3].Value, ctx.controller.Config.InstanceID) + assert.Equal(t, container.Env[4].Name, common.EnvVarGatewayServerPort) + assert.Equal(t, container.Env[4].Value, ctx.gateway.Spec.ProcessorPort) + } + + newDeployment, err := controller.k8sClient.AppsV1().Deployments(deployment.Namespace).Create(deployment) + assert.Nil(t, err) + assert.NotNil(t, newDeployment) + assert.Equal(t, newDeployment.Labels[common.LabelOwnerName], ctx.gateway.Name) + assert.NotNil(t, newDeployment.Annotations[common.AnnotationResourceSpecHash]) +} + +func TestResource_CreateGatewayResource(t *testing.T) { + tests := []struct { + name string + updateFunc func(ctx *gatewayContext) + testFunc func(controller *Controller, ctx *gatewayContext, t *testing.T) + }{ + { + name: "gateway with deployment and service", + updateFunc: func(ctx *gatewayContext) {}, + testFunc: func(controller *Controller, ctx *gatewayContext, t *testing.T) { + deploymentMetadata := ctx.gateway.Status.Resources.Deployment + serviceMetadata := ctx.gateway.Status.Resources.Service + deployment, err := controller.k8sClient.AppsV1().Deployments(deploymentMetadata.Namespace).Get(deploymentMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + service, err := controller.k8sClient.CoreV1().Services(serviceMetadata.Namespace).Get(serviceMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + }, + }, + { + name: "gateway with zero deployment replica", + updateFunc: func(ctx *gatewayContext) { + ctx.gateway.Spec.Replica = 0 + }, + testFunc: func(controller *Controller, ctx *gatewayContext, t *testing.T) { + deploymentMetadata := ctx.gateway.Status.Resources.Deployment + deployment, err := controller.k8sClient.AppsV1().Deployments(deploymentMetadata.Namespace).Get(deploymentMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.Equal(t, *deployment.Spec.Replicas, int32(1)) + }, + }, + { + name: "gateway with empty service template", + updateFunc: func(ctx *gatewayContext) { + ctx.gateway.Spec.Service = nil + }, + testFunc: func(controller *Controller, ctx *gatewayContext, t *testing.T) { + deploymentMetadata := ctx.gateway.Status.Resources.Deployment + deployment, err := controller.k8sClient.AppsV1().Deployments(deploymentMetadata.Namespace).Get(deploymentMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.Nil(t, ctx.gateway.Status.Resources.Service) + }, + }, + { + name: "gateway with resources in different namespaces", + updateFunc: func(ctx *gatewayContext) { + ctx.gateway.Spec.Template.Namespace = "new-namespace" + ctx.gateway.Spec.Service.Namespace = "new-namespace" + }, + testFunc: func(controller *Controller, ctx *gatewayContext, t *testing.T) { + deploymentMetadata := ctx.gateway.Status.Resources.Deployment + serviceMetadata := ctx.gateway.Status.Resources.Service + deployment, err := controller.k8sClient.AppsV1().Deployments(deploymentMetadata.Namespace).Get(deploymentMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + service, err := controller.k8sClient.CoreV1().Services(serviceMetadata.Namespace).Get(serviceMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.NotEqual(t, ctx.gateway.Namespace, deployment.Namespace) + assert.NotEqual(t, ctx.gateway.Namespace, service.Namespace) + }, + }, + { + name: "gateway with resources with empty names and namespaces", + updateFunc: func(ctx *gatewayContext) { + ctx.gateway.Spec.Template.Name = "" + ctx.gateway.Spec.Service.Name = "" + }, + testFunc: func(controller *Controller, ctx *gatewayContext, t *testing.T) { + deploymentMetadata := ctx.gateway.Status.Resources.Deployment + serviceMetadata := ctx.gateway.Status.Resources.Service + deployment, err := controller.k8sClient.AppsV1().Deployments(deploymentMetadata.Namespace).Get(deploymentMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + service, err := controller.k8sClient.CoreV1().Services(serviceMetadata.Namespace).Get(serviceMetadata.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.Equal(t, ctx.gateway.Name, deployment.Name) + assert.Equal(t, ctx.gateway.Name, service.Name) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + test.updateFunc(ctx) + err := ctx.createGatewayResources() + assert.Nil(t, err) + test.testFunc(controller, ctx, t) + }) + } +} + +func TestResource_UpdateGatewayResource(t *testing.T) { + controller := newController() + ctx := newGatewayContext(gatewayObj.DeepCopy(), controller) + err := ctx.createGatewayResources() + assert.Nil(t, err) + + tests := []struct { + name string + updateFunc func() + testFunc func(t *testing.T, oldMetadata *v1alpha1.GatewayResource) + }{ + { + name: "update deployment resource on gateway template change", + updateFunc: func() { + ctx.gateway.Spec.Template.Spec.Containers[0].ImagePullPolicy = corev1.PullIfNotPresent + ctx.gateway.Spec.Service.Spec.Type = corev1.ServiceTypeNodePort + }, + testFunc: func(t *testing.T, oldMetadata *v1alpha1.GatewayResource) { + currentMetadata := ctx.gateway.Status.Resources + deployment, err := controller.k8sClient.AppsV1().Deployments(currentMetadata.Deployment.Namespace).Get(currentMetadata.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.NotEqual(t, deployment.Annotations[common.AnnotationResourceSpecHash], oldMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash]) + service, err := controller.k8sClient.CoreV1().Services(currentMetadata.Service.Namespace).Get(currentMetadata.Service.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.NotEqual(t, service.Annotations[common.AnnotationResourceSpecHash], oldMetadata.Service.Annotations[common.AnnotationResourceSpecHash]) + }, + }, + { + name: "delete service resource if gateway service spec is removed", + updateFunc: func() { + ctx.gateway.Spec.Service = nil + }, + testFunc: func(t *testing.T, oldMetadata *v1alpha1.GatewayResource) { + currentMetadata := ctx.gateway.Status.Resources + deployment, err := controller.k8sClient.AppsV1().Deployments(currentMetadata.Deployment.Namespace).Get(currentMetadata.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.Equal(t, deployment.Annotations[common.AnnotationResourceSpecHash], oldMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash]) + assert.Nil(t, ctx.gateway.Status.Resources.Service) + service, err := controller.k8sClient.CoreV1().Services(oldMetadata.Service.Namespace).Get(oldMetadata.Service.Name, metav1.GetOptions{}) + assert.NotNil(t, err) + assert.Equal(t, apierror.IsNotFound(err), true) + assert.Nil(t, service) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + metadata := ctx.gateway.Status.Resources.DeepCopy() + test.updateFunc() + err := ctx.updateGatewayResources() + assert.Nil(t, err) + test.testFunc(t, metadata) + }) + } +} diff --git a/controllers/gateway/state.go b/controllers/gateway/state.go deleted file mode 100644 index 7c61b47580..0000000000 --- a/controllers/gateway/state.go +++ /dev/null @@ -1,50 +0,0 @@ -package gateway - -import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - gwclient "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" - "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/wait" -) - -// PersistUpdates of the gateway resource -func PersistUpdates(client gwclient.Interface, gw *v1alpha1.Gateway, log *logrus.Logger) (*v1alpha1.Gateway, error) { - gatewayClient := client.ArgoprojV1alpha1().Gateways(gw.ObjectMeta.Namespace) - - // in case persist update fails - oldgw := gw.DeepCopy() - - gw, err := gatewayClient.Update(gw) - if err != nil { - log.WithError(err).Warn("error updating gateway") - if errors.IsConflict(err) { - return oldgw, err - } - log.Info("re-applying updates on latest version and retrying update") - err = ReapplyUpdates(client, gw) - if err != nil { - log.WithError(err).Error("failed to re-apply update") - return oldgw, err - } - } - log.WithField(common.LabelPhase, string(gw.Status.Phase)).Info("gateway state updated successfully") - return gw, nil -} - -// ReapplyUpdates to gateway resource -func ReapplyUpdates(client gwclient.Interface, gw *v1alpha1.Gateway) error { - return wait.ExponentialBackoff(common.DefaultRetry, func() (bool, error) { - gatewayClient := client.ArgoprojV1alpha1().Gateways(gw.Namespace) - g, err := gatewayClient.Update(gw) - if err != nil { - if !common.IsRetryableKubeAPIError(err) { - return false, err - } - return false, nil - } - gw = g - return true, nil - }) -} diff --git a/controllers/gateway/state_test.go b/controllers/gateway/state_test.go deleted file mode 100644 index 9923e0b525..0000000000 --- a/controllers/gateway/state_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package gateway - -import ( - "github.com/argoproj/argo-events/common" - "testing" - - "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/fake" - "github.com/smartystreets/goconvey/convey" -) - -func TestPersistUpdates(t *testing.T) { - convey.Convey("Given a gateway resource", t, func() { - namespace := "argo-events" - client := fake.NewSimpleClientset() - logger := common.NewArgoEventsLogger() - gw, err := getGateway() - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Create the gateway", func() { - gw, err = client.ArgoprojV1alpha1().Gateways(namespace).Create(gw) - convey.So(err, convey.ShouldBeNil) - convey.So(gw, convey.ShouldNotBeNil) - - gw.ObjectMeta.Labels = map[string]string{ - "default": "default", - } - - convey.Convey("Update the gateway", func() { - updatedGw, err := PersistUpdates(client, gw, logger) - convey.So(err, convey.ShouldBeNil) - convey.So(updatedGw, convey.ShouldNotEqual, gw) - convey.So(updatedGw.Labels, convey.ShouldResemble, gw.Labels) - - updatedGw.Labels["new"] = "new" - - convey.Convey("Reapply the gateway", func() { - err := ReapplyUpdates(client, updatedGw) - convey.So(err, convey.ShouldBeNil) - convey.So(len(updatedGw.Labels), convey.ShouldEqual, 2) - }) - }) - }) - }) -} diff --git a/controllers/gateway/validate.go b/controllers/gateway/validate.go index 73860ff8a2..b1dbb51a4c 100644 --- a/controllers/gateway/validate.go +++ b/controllers/gateway/validate.go @@ -17,50 +17,50 @@ limitations under the License. package gateway import ( - "fmt" apicommon "github.com/argoproj/argo-events/pkg/apis/common" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + "github.com/pkg/errors" ) // Validate validates the gateway resource. -// Exporting this function so that external APIs can use this to validate gateway resource. -func Validate(gw *v1alpha1.Gateway) error { - if gw.Spec.Template == nil { - return fmt.Errorf("gateway pod template is not specified") +func Validate(gatewayObj *v1alpha1.Gateway) error { + if gatewayObj.Spec.Template == nil { + return errors.New("gateway pod template is not specified") } - if gw.Spec.Type == "" { - return fmt.Errorf("gateway type is not specified") + if gatewayObj.Spec.Type == "" { + return errors.New("gateway type is not specified") } - if gw.Spec.EventSource == "" { - return fmt.Errorf("event source for the gateway is not specified") + if gatewayObj.Spec.EventSourceRef == nil { + return errors.New("event source for the gateway is not specified") } - if gw.Spec.ProcessorPort == "" { - return fmt.Errorf("gateway processor port is not specified") + + if gatewayObj.Spec.ProcessorPort == "" { + return errors.New("gateway processor port is not specified") } - switch gw.Spec.EventProtocol.Type { + switch gatewayObj.Spec.EventProtocol.Type { case apicommon.HTTP: - if gw.Spec.Watchers == nil || (gw.Spec.Watchers.Gateways == nil && gw.Spec.Watchers.Sensors == nil) { - return fmt.Errorf("no associated watchers with gateway") + if gatewayObj.Spec.Watchers == nil || (gatewayObj.Spec.Watchers.Gateways == nil && gatewayObj.Spec.Watchers.Sensors == nil) { + return errors.New("no associated watchers with gateway") } - if gw.Spec.EventProtocol.Http.Port == "" { - return fmt.Errorf("http server port is not defined") + if gatewayObj.Spec.EventProtocol.Http.Port == "" { + return errors.New("http server port is not defined") } case apicommon.NATS: - if gw.Spec.EventProtocol.Nats.URL == "" { - return fmt.Errorf("nats url is not defined") + if gatewayObj.Spec.EventProtocol.Nats.URL == "" { + return errors.New("nats url is not defined") } - if gw.Spec.EventProtocol.Nats.Type == "" { - return fmt.Errorf("nats service type is not defined") + if gatewayObj.Spec.EventProtocol.Nats.Type == "" { + return errors.New("nats service type is not defined") } - if gw.Spec.EventProtocol.Nats.Type == apicommon.Streaming && gw.Spec.EventProtocol.Nats.ClientId == "" { - return fmt.Errorf("client id must be specified when using nats streaming") + if gatewayObj.Spec.EventProtocol.Nats.Type == apicommon.Streaming && gatewayObj.Spec.EventProtocol.Nats.ClientId == "" { + return errors.New("client id must be specified when using nats streaming") } - if gw.Spec.EventProtocol.Nats.Type == apicommon.Streaming && gw.Spec.EventProtocol.Nats.ClusterId == "" { - return fmt.Errorf("cluster id must be specified when using nats streaming") + if gatewayObj.Spec.EventProtocol.Nats.Type == apicommon.Streaming && gatewayObj.Spec.EventProtocol.Nats.ClusterId == "" { + return errors.New("cluster id must be specified when using nats streaming") } default: - return fmt.Errorf("unknown gateway type") + return errors.New("unknown gateway type") } return nil } diff --git a/controllers/gateway/validate_test.go b/controllers/gateway/validate_test.go index 47631c1dcb..a537b87d3e 100644 --- a/controllers/gateway/validate_test.go +++ b/controllers/gateway/validate_test.go @@ -18,28 +18,25 @@ package gateway import ( "fmt" - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - "github.com/ghodss/yaml" "io/ioutil" "testing" - "github.com/smartystreets/goconvey/convey" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" ) func TestValidate(t *testing.T) { dir := "../../examples/gateways" - convey.Convey("Validate list of gateways", t, func() { - files, err := ioutil.ReadDir(dir) - convey.So(err, convey.ShouldBeNil) - for _, file := range files { - fmt.Println("filename: ", file.Name()) - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", dir, file.Name())) - convey.So(err, convey.ShouldBeNil) - var gateway *v1alpha1.Gateway - err = yaml.Unmarshal([]byte(content), &gateway) - convey.So(err, convey.ShouldBeNil) - err = Validate(gateway) - convey.So(err, convey.ShouldBeNil) - } - }) + files, err := ioutil.ReadDir(dir) + assert.Nil(t, err) + for _, file := range files { + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", dir, file.Name())) + assert.Nil(t, err) + var gateway *v1alpha1.Gateway + err = yaml.Unmarshal([]byte(content), &gateway) + assert.Nil(t, err) + err = Validate(gateway) + assert.Nil(t, err) + } } diff --git a/cmd/controllers/sensor/main.go b/controllers/sensor/cmd/main.go similarity index 78% rename from cmd/controllers/sensor/main.go rename to controllers/sensor/cmd/main.go index 663897c9a1..aaaa9ac867 100644 --- a/cmd/controllers/sensor/main.go +++ b/controllers/sensor/cmd/main.go @@ -33,24 +33,24 @@ func main() { } // sensor-controller configuration - configMap, ok := os.LookupEnv(common.EnvVarSensorControllerConfigMap) + configMap, ok := os.LookupEnv(common.EnvVarControllerConfigMap) if !ok { - configMap = common.DefaultConfigMapName(common.LabelSensorControllerName) + panic("controller configmap is not provided") } - namespace, ok := os.LookupEnv(common.SensorNamespace) + namespace, ok := os.LookupEnv(common.EnvVarNamespace) if !ok { namespace = common.DefaultControllerNamespace } // create a new sensor controller - controller := sensor.NewSensorController(restConfig, configMap, namespace) + controller := sensor.NewController(restConfig, configMap, namespace) // watch updates to sensor controller configuration err = controller.ResyncConfig(namespace) if err != nil { panic(err) } - go controller.Run(context.Background(), 1, 1) + go controller.Run(context.Background(), 1) select {} } diff --git a/gateways/community/slack/config_test.go b/controllers/sensor/common.go similarity index 50% rename from gateways/community/slack/config_test.go rename to controllers/sensor/common.go index b07e62fdd7..4867b4ef3c 100644 --- a/gateways/community/slack/config_test.go +++ b/controllers/sensor/common.go @@ -14,29 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package slack +package sensor -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -hook: - endpoint: "/" - port: "8080" - url: "testurl" -token: - name: fake-token - key: fake -` +import "github.com/argoproj/argo-events/pkg/apis/sensor" -func TestParseConfig(t *testing.T) { - convey.Convey("Given a slack event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*slackEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} +// Labels +const ( + //LabelControllerInstanceID is the label which allows to separate application among multiple running controllers. + LabelControllerInstanceID = sensor.FullName + "/sensor-controller-instanceid" + // LabelPhase is a label applied to sensors to indicate the current phase of the sensor (for filtering purposes) + LabelPhase = sensor.FullName + "/phase" + // LabelComplete is the label to mark sensors as complete + LabelComplete = sensor.FullName + "/complete" +) diff --git a/controllers/sensor/config.go b/controllers/sensor/config.go index 2515e6c838..d0f9f23732 100644 --- a/controllers/sensor/config.go +++ b/controllers/sensor/config.go @@ -19,9 +19,10 @@ package sensor import ( "context" "fmt" - "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/common" "github.com/ghodss/yaml" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,49 +33,49 @@ import ( ) // watchControllerConfigMap watches updates to sensor controller configmap -func (c *SensorController) watchControllerConfigMap(ctx context.Context) (cache.Controller, error) { - log.Info("watching sensor-controller config map updates") - source := c.newControllerConfigMapWatch() - _, controller := cache.NewInformer( +func (controller *Controller) watchControllerConfigMap(ctx context.Context) (cache.Controller, error) { + log.Info("watching controller config map updates") + source := controller.newControllerConfigMapWatch() + _, ctrl := cache.NewInformer( source, &corev1.ConfigMap{}, 0, cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if cm, ok := obj.(*corev1.ConfigMap); ok { - log.Info("detected EventSource update. updating the sensor-controller config.") - err := c.updateConfig(cm) + log.Info("detected configuration update. updating the controller configuration") + err := controller.updateConfig(cm) if err != nil { - log.Errorf("update of config failed due to: %v", err) + log.Errorf("update of controller configuration failed due to: %v", err) } } }, UpdateFunc: func(old, new interface{}) { if newCm, ok := new.(*corev1.ConfigMap); ok { - log.Info("detected EventSource update. updating the sensor-controller config.") - err := c.updateConfig(newCm) + log.Info("detected configuration update. updating the controller configuration") + err := controller.updateConfig(newCm) if err != nil { - log.Errorf("update of config failed due to: %v", err) + log.Errorf("update of controller configuration failed due to: %v", err) } } }, }) - go controller.Run(ctx.Done()) - return controller, nil + go ctrl.Run(ctx.Done()) + return ctrl, nil } // newControllerConfigMapWatch returns a configmap watcher -func (c *SensorController) newControllerConfigMapWatch() *cache.ListWatch { - x := c.kubeClientset.CoreV1().RESTClient() +func (controller *Controller) newControllerConfigMapWatch() *cache.ListWatch { + x := controller.k8sClient.CoreV1().RESTClient() resource := "configmaps" - name := c.ConfigMap + name := controller.ConfigMap fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", name)) listFunc := func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector.String() req := x.Get(). - Namespace(c.Namespace). + Namespace(controller.Namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec) return req.Do().Get() @@ -83,7 +84,7 @@ func (c *SensorController) newControllerConfigMapWatch() *cache.ListWatch { options.Watch = true options.FieldSelector = fieldSelector.String() req := x.Get(). - Namespace(c.Namespace). + Namespace(controller.Namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec) return req.Watch() @@ -91,26 +92,26 @@ func (c *SensorController) newControllerConfigMapWatch() *cache.ListWatch { return &cache.ListWatch{ListFunc: listFunc, WatchFunc: watchFunc} } -// ResyncConfig reloads the sensor-controller config from the configmap -func (c *SensorController) ResyncConfig(namespace string) error { - cmClient := c.kubeClientset.CoreV1().ConfigMaps(namespace) - cm, err := cmClient.Get(c.ConfigMap, metav1.GetOptions{}) +// ResyncConfig reloads the controller config from the configmap +func (controller *Controller) ResyncConfig(namespace string) error { + cmClient := controller.k8sClient.CoreV1().ConfigMaps(namespace) + cm, err := cmClient.Get(controller.ConfigMap, metav1.GetOptions{}) if err != nil { return err } - return c.updateConfig(cm) + return controller.updateConfig(cm) } -func (c *SensorController) updateConfig(cm *corev1.ConfigMap) error { - configStr, ok := cm.Data[common.SensorControllerConfigMapKey] +func (controller *Controller) updateConfig(cm *corev1.ConfigMap) error { + configStr, ok := cm.Data[common.ControllerConfigMapKey] if !ok { - return fmt.Errorf("configMap '%s' does not have key '%s'", c.ConfigMap, common.SensorControllerConfigMapKey) + return errors.Errorf("configMap '%s' does not have key '%s'", controller.ConfigMap, common.ControllerConfigMapKey) } - var config SensorControllerConfig + var config ControllerConfig err := yaml.Unmarshal([]byte(configStr), &config) if err != nil { return err } - c.Config = config + controller.Config = config return nil } diff --git a/controllers/sensor/config_test.go b/controllers/sensor/config_test.go index 87ea54b68a..901c286015 100644 --- a/controllers/sensor/config_test.go +++ b/controllers/sensor/config_test.go @@ -20,52 +20,26 @@ import ( "testing" "github.com/argoproj/argo-events/common" - "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestSensorControllerConfigWatch(t *testing.T) { - sc := getSensorController() - - convey.Convey("Given a sensor", t, func() { - convey.Convey("Create a new watch and make sure watcher is not nil", func() { - watcher := sc.newControllerConfigMapWatch() - convey.So(watcher, convey.ShouldNotBeNil) - }) - }) - - convey.Convey("Given a sensor, resync config", t, func() { - convey.Convey("Update a sensor configmap with new instance id and remove namespace", func() { - cmObj := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: common.DefaultControllerNamespace, - Name: sc.ConfigMap, - }, - Data: map[string]string{ - common.SensorControllerConfigMapKey: `instanceID: fake-instance-id`, - }, - } - cm, err := sc.kubeClientset.CoreV1().ConfigMaps(sc.Namespace).Create(cmObj) - convey.Convey("Make sure no error occurs", func() { - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Updated sensor configmap must be non-nil", func() { - convey.So(cm, convey.ShouldNotBeNil) - - convey.Convey("Resync the sensor configuration", func() { - err := sc.ResyncConfig(cmObj.Namespace) - convey.Convey("No error should occur while resyncing sensor configuration", func() { - convey.So(err, convey.ShouldBeNil) - - convey.Convey("The updated instance id must be fake-instance-id", func() { - convey.So(sc.Config.InstanceID, convey.ShouldEqual, "fake-instance-id") - convey.So(sc.Config.Namespace, convey.ShouldBeEmpty) - }) - }) - }) - }) - }) - }) - }) + sensorController := getController() + configmap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: common.DefaultControllerNamespace, + Name: sensorController.ConfigMap, + }, + Data: map[string]string{ + common.ControllerConfigMapKey: `instanceID: fake-instance-id`, + }, + } + cm, err := sensorController.k8sClient.CoreV1().ConfigMaps(sensorController.Namespace).Create(configmap) + assert.Nil(t, err) + assert.NotNil(t, cm) + err = sensorController.ResyncConfig(sensorController.Namespace) + assert.Nil(t, err) + assert.Equal(t, sensorController.Config.InstanceID, "fake-instance-id") } diff --git a/controllers/sensor/controller.go b/controllers/sensor/controller.go index edf99cbfd1..b2923d670e 100644 --- a/controllers/sensor/controller.go +++ b/controllers/sensor/controller.go @@ -20,93 +20,85 @@ import ( "context" "errors" "fmt" - "github.com/argoproj/argo-events/pkg/apis/sensor" - "github.com/sirupsen/logrus" "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" + base "github.com/argoproj/argo-events" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/pkg/apis/sensor" + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + clientset "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/informers" - informersv1 "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - - base "github.com/argoproj/argo-events" - "github.com/argoproj/argo-events/common" - ccommon "github.com/argoproj/argo-events/controllers/common" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - clientset "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" ) // informer constants const ( - sensorResyncPeriod = 20 * time.Minute - sensorResourceResyncPeriod = 30 * time.Minute - rateLimiterBaseDelay = 5 * time.Second - rateLimiterMaxDelay = 1000 * time.Second + sensorResyncPeriod = 20 * time.Minute + rateLimiterBaseDelay = 5 * time.Second + rateLimiterMaxDelay = 1000 * time.Second ) -// SensorControllerConfig contain the configuration settings for the sensor-controller -type SensorControllerConfig struct { - // InstanceID is a label selector to limit the sensor-controller's watch of sensor jobs to a specific instance. - // If omitted, the sensor-controller watches sensors that *are not* labeled with an instance id. +// ControllerConfig contain the configuration settings for the controller +type ControllerConfig struct { + // InstanceID is a label selector to limit the controller'sensor watch of sensor jobs to a specific instance. + // If omitted, the controller watches sensors that *are not* labeled with an instance id. InstanceID string - - // Namespace is a label selector filter to limit sensor-controller's watch to specific namespace + // Namespace is a label selector filter to limit controller'sensor watch to specific namespace Namespace string } -// SensorController listens for new sensors and hands off handling of each sensor on the queue to the operator -type SensorController struct { - // EventSource is the name of the config map in which to derive configuration of the contoller +// Controller listens for new sensors and hands off handling of each sensor on the queue to the operator +type Controller struct { + // ConfigMap is the name of the config map in which to derive configuration of the controller ConfigMap string - // Namespace for sensor controller + // Namespace for controller Namespace string - // Config is the sensor-controller's configuration - Config SensorControllerConfig - // log is the logger for a gateway - log *logrus.Logger - - // kubernetes config and apis - kubeConfig *rest.Config - kubeClientset kubernetes.Interface - sensorClientset clientset.Interface - - // sensor informer and queue - podInformer informersv1.PodInformer - svcInformer informersv1.ServiceInformer - informer cache.SharedIndexInformer - queue workqueue.RateLimitingInterface + // Config is the controller'sensor configuration + Config ControllerConfig + // logger to logger stuff + logger *logrus.Logger + // kubeConfig is the rest K8s config + kubeConfig *rest.Config + // k8sClient is the Kubernetes client + k8sClient kubernetes.Interface + // sensorClient is the client for operations on the sensor custom resource + sensorClient clientset.Interface + // informer for sensor resource updates + informer cache.SharedIndexInformer + // queue to process watched sensor resources + queue workqueue.RateLimitingInterface } -// NewSensorController creates a new Controller -func NewSensorController(rest *rest.Config, configMap, namespace string) *SensorController { +// NewController creates a new Controller +func NewController(rest *rest.Config, configMap, namespace string) *Controller { rateLimiter := workqueue.NewItemExponentialFailureRateLimiter(rateLimiterBaseDelay, rateLimiterMaxDelay) - return &SensorController{ - ConfigMap: configMap, - Namespace: namespace, - kubeConfig: rest, - kubeClientset: kubernetes.NewForConfigOrDie(rest), - sensorClientset: clientset.NewForConfigOrDie(rest), - queue: workqueue.NewRateLimitingQueue(rateLimiter), - log: common.NewArgoEventsLogger(), + return &Controller{ + ConfigMap: configMap, + Namespace: namespace, + kubeConfig: rest, + k8sClient: kubernetes.NewForConfigOrDie(rest), + sensorClient: clientset.NewForConfigOrDie(rest), + queue: workqueue.NewRateLimitingQueue(rateLimiter), + logger: common.NewArgoEventsLogger(), } } -func (c *SensorController) processNextItem() bool { +// processNextItem processes the sensor resource object on the queue +func (controller *Controller) processNextItem() bool { // Wait until there is a new item in the queue - key, quit := c.queue.Get() + key, quit := controller.queue.Get() if quit { return false } - defer c.queue.Done(key) + defer controller.queue.Done(key) - obj, exists, err := c.informer.GetIndexer().GetByKey(key.(string)) + obj, exists, err := controller.informer.GetIndexer().GetByKey(key.(string)) if err != nil { - c.log.WithField(common.LabelSensorName, key.(string)).WithError(err).Warn("failed to get sensor from informer index") + controller.logger.WithField(common.LabelSensorName, key.(string)).WithError(err).Warnln("failed to get sensor from informer index") return true } @@ -117,21 +109,21 @@ func (c *SensorController) processNextItem() bool { s, ok := obj.(*v1alpha1.Sensor) if !ok { - c.log.WithField(common.LabelSensorName, key.(string)).WithError(err).Warn("key in index is not a sensor") + controller.logger.WithField(common.LabelSensorName, key.(string)).WithError(err).Warnln("key in index is not a sensor") return true } - ctx := newSensorOperationCtx(s, c) + ctx := newSensorContext(s, controller) err = ctx.operate() if err != nil { - if err := common.GenerateK8sEvent(c.kubeClientset, + if err := common.GenerateK8sEvent(controller.k8sClient, fmt.Sprintf("failed to operate on sensor %s", s.Name), common.EscalationEventType, "sensor operation failed", s.Name, s.Namespace, - c.Config.InstanceID, + controller.Config.InstanceID, sensor.Kind, map[string]string{ common.LabelSensorName: s.Name, @@ -139,96 +131,73 @@ func (c *SensorController) processNextItem() bool { common.LabelOperation: "controller_operation", }, ); err != nil { - ctx.log.WithError(err).Error("failed to create K8s event to escalate sensor operation failure") + ctx.logger.WithError(err).Errorln("failed to create K8s event to escalate sensor operation failure") } } - err = c.handleErr(err, key) + err = controller.handleErr(err, key) if err != nil { - ctx.log.WithError(err).Error("sensor controller is unable to handle the error") + ctx.logger.WithError(err).Errorln("controller is unable to handle the error") } return true } // handleErr checks if an error happened and make sure we will retry later // returns an error if unable to handle the error -func (c *SensorController) handleErr(err error, key interface{}) error { +func (controller *Controller) handleErr(err error, key interface{}) error { if err == nil { // Forget about the #AddRateLimited history of key on every successful sync // Ensure future updates for this key are not delayed because of outdated error history - c.queue.Forget(key) + controller.queue.Forget(key) return nil } // due to the base delay of 5ms of the DefaultControllerRateLimiter - // requeues will happen very quickly even after a sensor pod goes down - // we want to give the sensor pod a chance to come back up so we give a genorous number of retries - if c.queue.NumRequeues(key) < 20 { + // re-queues will happen very quickly even after a sensor pod goes down + // we want to give the sensor pod a chance to come back up so we give a generous number of retries + if controller.queue.NumRequeues(key) < 20 { // Re-enqueue the key rate limited. This key will be processed later again. - c.queue.AddRateLimited(key) + controller.queue.AddRateLimited(key) return nil } - return errors.New("exceeded max requeues") + return errors.New("exceeded max re-queues") } -// Run executes the sensor-controller -func (c *SensorController) Run(ctx context.Context, ssThreads, eventThreads int) { - defer c.queue.ShutDown() +// Run executes the controller +func (controller *Controller) Run(ctx context.Context, threads int) { + defer controller.queue.ShutDown() - c.log.WithFields( + controller.logger.WithFields( map[string]interface{}{ - common.LabelInstanceID: c.Config.InstanceID, + common.LabelInstanceID: controller.Config.InstanceID, common.LabelVersion: base.GetVersion().Version, - }).Info("starting sensor controller") - _, err := c.watchControllerConfigMap(ctx) - if err != nil { - c.log.WithError(err).Error("failed to register watch for sensor controller config map") - return - } + }).Infoln("starting the controller...") - c.informer = c.newSensorInformer() - go c.informer.Run(ctx.Done()) - - if !cache.WaitForCacheSync(ctx.Done(), c.informer.HasSynced) { - c.log.Panic("timed out waiting for the caches to sync for sensors") + _, err := controller.watchControllerConfigMap(ctx) + if err != nil { + controller.logger.WithError(err).Error("failed to register watch for controller config map") return } - listOptionsFunc := func(options *metav1.ListOptions) { - labelSelector := labels.NewSelector().Add(c.instanceIDReq()) - options.LabelSelector = labelSelector.String() - } - factory := ccommon.ArgoEventInformerFactory{ - OwnerGroupVersionKind: v1alpha1.SchemaGroupVersionKind, - OwnerInformer: c.informer, - SharedInformerFactory: informers.NewFilteredSharedInformerFactory(c.kubeClientset, sensorResourceResyncPeriod, c.Config.Namespace, listOptionsFunc), - Queue: c.queue, - } - - c.podInformer = factory.NewPodInformer() - go c.podInformer.Informer().Run(ctx.Done()) - - if !cache.WaitForCacheSync(ctx.Done(), c.podInformer.Informer().HasSynced) { - c.log.Panic("timed out waiting for the caches to sync for sensor pods") - return + controller.informer, err = controller.newSensorInformer() + if err != nil { + controller.logger.WithError(err).Errorln("failed to create a new sensor controller") } + go controller.informer.Run(ctx.Done()) - c.svcInformer = factory.NewServiceInformer() - go c.svcInformer.Informer().Run(ctx.Done()) - - if !cache.WaitForCacheSync(ctx.Done(), c.svcInformer.Informer().HasSynced) { - c.log.Panic("timed out waiting for the caches to sync for sensor services") + if !cache.WaitForCacheSync(ctx.Done(), controller.informer.HasSynced) { + controller.logger.Panic("timed out waiting for the caches to sync for sensors") return } - for i := 0; i < ssThreads; i++ { - go wait.Until(c.runWorker, time.Second, ctx.Done()) + for i := 0; i < threads; i++ { + go wait.Until(controller.runWorker, time.Second, ctx.Done()) } <-ctx.Done() } -func (c *SensorController) runWorker() { - for c.processNextItem() { +func (controller *Controller) runWorker() { + for controller.processNextItem() { } } diff --git a/controllers/sensor/controller_test.go b/controllers/sensor/controller_test.go index 6130abcb8c..94b00b6a98 100644 --- a/controllers/sensor/controller_test.go +++ b/controllers/sensor/controller_test.go @@ -19,18 +19,13 @@ package sensor import ( "fmt" "testing" - "time" "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" fakesensor "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/fake" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" ) @@ -39,84 +34,55 @@ var ( SensorControllerInstanceID = "argo-events" ) -func getFakePodSharedIndexInformer(clientset kubernetes.Interface) cache.SharedIndexInformer { - // NewListWatchFromClient doesn't work with fake client. - // ref: https://github.com/kubernetes/client-go/issues/352 - return cache.NewSharedIndexInformer(&cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - return clientset.CoreV1().Pods("").List(options) - }, - WatchFunc: clientset.CoreV1().Pods("").Watch, - }, &corev1.Pod{}, 1*time.Second, cache.Indexers{}) -} - -func getSensorController() *SensorController { +func getController() *Controller { clientset := fake.NewSimpleClientset() - done := make(chan struct{}) - informer := getFakePodSharedIndexInformer(clientset) - go informer.Run(done) - factory := informers.NewSharedInformerFactory(clientset, 0) - podInformer := factory.Core().V1().Pods() - go podInformer.Informer().Run(done) - svcInformer := factory.Core().V1().Services() - go svcInformer.Informer().Run(done) - return &SensorController{ + controller := &Controller{ ConfigMap: SensorControllerConfigmap, Namespace: common.DefaultControllerNamespace, - Config: SensorControllerConfig{ + Config: ControllerConfig{ Namespace: common.DefaultControllerNamespace, InstanceID: SensorControllerInstanceID, }, - kubeClientset: clientset, - sensorClientset: fakesensor.NewSimpleClientset(), - podInformer: podInformer, - svcInformer: svcInformer, - informer: informer, - queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), - log: common.NewArgoEventsLogger(), + k8sClient: clientset, + sensorClient: fakesensor.NewSimpleClientset(), + queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + logger: common.NewArgoEventsLogger(), } + informer, err := controller.newSensorInformer() + if err != nil { + panic(err) + } + controller.informer = informer + return controller } -func TestGatewayController(t *testing.T) { - convey.Convey("Given a sensor controller, process queue items", t, func() { - controller := getSensorController() - - convey.Convey("Create a resource queue, add new item and process it", func() { - controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - controller.informer = controller.newSensorInformer() - controller.queue.Add("hi") - res := controller.processNextItem() - - convey.Convey("Item from queue must be successfully processed", func() { - convey.So(res, convey.ShouldBeTrue) - }) - - convey.Convey("Shutdown queue and make sure queue does not process next item", func() { - controller.queue.ShutDown() - res := controller.processNextItem() - convey.So(res, convey.ShouldBeFalse) - }) - }) +func TestController_ProcessNextItem(t *testing.T) { + controller := getController() + err := controller.informer.GetIndexer().Add(&v1alpha1.Sensor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-sensor", + Namespace: "fake-namespace", + }, + Spec: v1alpha1.SensorSpec{}, }) + assert.Nil(t, err) + controller.queue.Add("fake-sensor") + res := controller.processNextItem() + assert.Equal(t, res, true) + controller.queue.ShutDown() + res = controller.processNextItem() + assert.Equal(t, res, false) +} - convey.Convey("Given a sensor controller, handle errors in queue", t, func() { - controller := getSensorController() - convey.Convey("Create a resource queue and add an item", func() { - controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - controller.queue.Add("hi") - convey.Convey("Handle an nil error", func() { - err := controller.handleErr(nil, "hi") - convey.So(err, convey.ShouldBeNil) - }) - convey.Convey("Exceed max requeues", func() { - controller.queue.Add("bye") - var err error - for i := 0; i < 21; i++ { - err = controller.handleErr(fmt.Errorf("real error"), "bye") - } - convey.So(err, convey.ShouldNotBeNil) - convey.So(err.Error(), convey.ShouldEqual, "exceeded max requeues") - }) - }) - }) +func TestController_HandleErr(t *testing.T) { + controller := getController() + controller.queue.Add("hi") + err := controller.handleErr(nil, "hi") + assert.Nil(t, err) + controller.queue.Add("bye") + for i := 0; i < 21; i++ { + err = controller.handleErr(fmt.Errorf("real error"), "bye") + } + assert.NotNil(t, err) + assert.Equal(t, err.Error(), "exceeded max re-queues") } diff --git a/controllers/sensor/informer.go b/controllers/sensor/informer.go index 7965c6bb74..8bcfbb31a7 100644 --- a/controllers/sensor/informer.go +++ b/controllers/sensor/informer.go @@ -17,49 +17,43 @@ limitations under the License. package sensor import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + sensorinformers "github.com/argoproj/argo-events/pkg/client/sensor/informers/externalversions" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/tools/cache" - - sensorinformers "github.com/argoproj/argo-events/pkg/client/sensor/informers/externalversions" "k8s.io/apimachinery/pkg/selection" + "k8s.io/client-go/tools/cache" ) -func (c *SensorController) instanceIDReq() labels.Requirement { +func (controller *Controller) instanceIDReq() (*labels.Requirement, error) { var instanceIDReq *labels.Requirement var err error - if c.Config.InstanceID == "" { - panic("controller instance id must be specified") + if controller.Config.InstanceID == "" { + return nil, errors.New("controller instance id must be specified") } - instanceIDReq, err = labels.NewRequirement(common.LabelKeySensorControllerInstanceID, selection.Equals, []string{c.Config.InstanceID}) + instanceIDReq, err = labels.NewRequirement(LabelControllerInstanceID, selection.Equals, []string{controller.Config.InstanceID}) if err != nil { panic(err) } - return *instanceIDReq + return instanceIDReq, nil } -func (c *SensorController) versionReq() labels.Requirement { - versionReq, err := labels.NewRequirement(common.LabelArgoEventsSensorVersion, selection.Equals, []string{v1alpha1.ArgoEventsSensorVersion}) +// The sensor informer adds new sensors to the controller'sensor queue based on Add, Update, and Delete event handlers for the sensor resources +func (controller *Controller) newSensorInformer() (cache.SharedIndexInformer, error) { + labelSelector, err := controller.instanceIDReq() if err != nil { - panic(err) + return nil, err } - return *versionReq -} -// The sensor informer adds new Sensors to the sensor-controller's queue based on Add, Update, and Delete Event Handlers for the Sensor Resources -func (c *SensorController) newSensorInformer() cache.SharedIndexInformer { - sensorInformerFactory := sensorinformers.NewFilteredSharedInformerFactory( - c.sensorClientset, + sensorInformerFactory := sensorinformers.NewSharedInformerFactoryWithOptions( + controller.sensorClient, sensorResyncPeriod, - c.Config.Namespace, - func(options *metav1.ListOptions) { + sensorinformers.WithNamespace(controller.Config.Namespace), + sensorinformers.WithTweakListOptions(func(options *metav1.ListOptions) { options.FieldSelector = fields.Everything().String() - labelSelector := labels.NewSelector().Add(c.instanceIDReq(), c.versionReq()) options.LabelSelector = labelSelector.String() - }, + }), ) informer := sensorInformerFactory.Argoproj().V1alpha1().Sensors().Informer() informer.AddEventHandler( @@ -67,22 +61,22 @@ func (c *SensorController) newSensorInformer() cache.SharedIndexInformer { AddFunc: func(obj interface{}) { key, err := cache.MetaNamespaceKeyFunc(obj) if err == nil { - c.queue.Add(key) + controller.queue.Add(key) } }, UpdateFunc: func(old, new interface{}) { key, err := cache.MetaNamespaceKeyFunc(new) if err == nil { - c.queue.Add(key) + controller.queue.Add(key) } }, DeleteFunc: func(obj interface{}) { key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) if err == nil { - c.queue.Add(key) + controller.queue.Add(key) } }, }, ) - return informer + return informer, nil } diff --git a/controllers/sensor/informer_test.go b/controllers/sensor/informer_test.go index 75a5a8042e..738d603fe8 100644 --- a/controllers/sensor/informer_test.go +++ b/controllers/sensor/informer_test.go @@ -17,29 +17,17 @@ limitations under the License. package sensor import ( - "github.com/argoproj/argo-events/common" "testing" - "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/selection" ) -func TestInformer(t *testing.T) { - convey.Convey("Given a sensor controller", t, func() { - controller := getSensorController() - convey.Convey("Instance ID required key must match", func() { - req := controller.instanceIDReq() - convey.So(req.Key(), convey.ShouldEqual, common.LabelKeySensorControllerInstanceID) - convey.So(req.Operator(), convey.ShouldEqual, selection.Equals) - convey.So(req.Values().Has("argo-events"), convey.ShouldBeTrue) - }) - }) - - convey.Convey("Given a sensor controller", t, func() { - controller := getSensorController() - convey.Convey("Get a new informer and make sure its not nil", func() { - i := controller.newSensorInformer() - convey.So(i, convey.ShouldNotBeNil) - }) - }) +func TestInformer_InstanceIDReq(t *testing.T) { + controller := getController() + req, err := controller.instanceIDReq() + assert.Nil(t, err) + assert.Equal(t, req.Key(), LabelControllerInstanceID) + assert.Equal(t, req.Operator(), selection.Equals) + assert.Equal(t, req.Values().Has("argo-events"), true) } diff --git a/controllers/sensor/state.go b/controllers/sensor/node.go similarity index 65% rename from controllers/sensor/state.go rename to controllers/sensor/node.go index d66584742a..a7b14c1d97 100644 --- a/controllers/sensor/state.go +++ b/controllers/sensor/node.go @@ -17,16 +17,13 @@ limitations under the License. package sensor import ( - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - "github.com/sirupsen/logrus" "time" "github.com/argoproj/argo-events/common" apicommon "github.com/argoproj/argo-events/pkg/apis/common" - sclient "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" - "k8s.io/apimachinery/pkg/api/errors" + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" ) // GetNodeByName returns a copy of the node from this sensor for the nodename @@ -48,7 +45,7 @@ func InitializeNode(sensor *v1alpha1.Sensor, nodeName string, nodeType v1alpha1. nodeID := sensor.NodeID(nodeName) oldNode, ok := sensor.Status.Nodes[nodeID] if ok { - log.WithField(common.LabelNodeName, nodeName).Info("node already initialized") + log.WithField(common.LabelNodeName, nodeName).Infoln("node already initialized") return &oldNode } node := v1alpha1.NodeStatus{ @@ -69,50 +66,10 @@ func InitializeNode(sensor *v1alpha1.Sensor, nodeName string, nodeType v1alpha1. common.LabelNodeName: node.DisplayName, "node-message": node.Message, }, - ).Info("node is initialized") + ).Infoln("node is initialized") return &node } -// PersistUpdates persists the updates to the Sensor resource -func PersistUpdates(client sclient.Interface, sensor *v1alpha1.Sensor, controllerInstanceId string, log *logrus.Logger) (*v1alpha1.Sensor, error) { - sensorClient := client.ArgoprojV1alpha1().Sensors(sensor.ObjectMeta.Namespace) - // in case persist update fails - oldsensor := sensor.DeepCopy() - - sensor, err := sensorClient.Update(sensor) - if err != nil { - if errors.IsConflict(err) { - log.WithError(err).Error("error updating sensor") - return oldsensor, err - } - - log.Info("re-applying updates on latest version and retrying update") - err = ReapplyUpdate(client, sensor) - if err != nil { - log.WithError(err).Error("failed to re-apply update") - return oldsensor, err - } - } - log.WithField(common.LabelPhase, string(sensor.Status.Phase)).Info("sensor state updated successfully") - return sensor, nil -} - -// Reapply the update to sensor -func ReapplyUpdate(sensorClient sclient.Interface, sensor *v1alpha1.Sensor) error { - return wait.ExponentialBackoff(common.DefaultRetry, func() (bool, error) { - client := sensorClient.ArgoprojV1alpha1().Sensors(sensor.Namespace) - s, err := client.Update(sensor) - if err != nil { - if !common.IsRetryableKubeAPIError(err) { - return false, err - } - return false, nil - } - sensor = s - return true, nil - }) -} - // MarkNodePhase marks the node with a phase, returns the node func MarkNodePhase(sensor *v1alpha1.Sensor, nodeName string, nodeType v1alpha1.NodeType, phase v1alpha1.NodePhase, event *apicommon.Event, log *logrus.Logger, message ...string) *v1alpha1.NodeStatus { node := GetNodeByName(sensor, nodeName) @@ -123,7 +80,7 @@ func MarkNodePhase(sensor *v1alpha1.Sensor, nodeName string, nodeType v1alpha1.N common.LabelNodeName: node.Name, common.LabelPhase: string(node.Phase), }, - ).Info("marking node phase") + ).Infoln("marking node phase") node.Phase = phase } diff --git a/controllers/sensor/node_test.go b/controllers/sensor/node_test.go new file mode 100644 index 0000000000..e66e16f9dd --- /dev/null +++ b/controllers/sensor/node_test.go @@ -0,0 +1,93 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sensor + +import ( + "testing" + + "github.com/argoproj/argo-events/common" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + fakesensor "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/fake" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSensorState(t *testing.T) { + fakeSensorClient := fakesensor.NewSimpleClientset() + logger := common.NewArgoEventsLogger() + fakeSensor := &v1alpha1.Sensor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sensor", + Namespace: "test", + }, + } + + fakeSensor, err := fakeSensorClient.ArgoprojV1alpha1().Sensors(fakeSensor.Namespace).Create(fakeSensor) + assert.Nil(t, err) + + tests := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "initialize a new node", + testFunc: func(t *testing.T) { + status := InitializeNode(fakeSensor, "first_node", v1alpha1.NodeTypeEventDependency, logger) + assert.Equal(t, status.Phase, v1alpha1.NodePhaseNew) + }, + }, + { + name: "persist updates to the sensor", + testFunc: func(t *testing.T) { + sensor, err := PersistUpdates(fakeSensorClient, fakeSensor, logger) + assert.Nil(t, err) + assert.Equal(t, len(sensor.Status.Nodes), 1) + }, + }, + { + name: "mark node state to active", + testFunc: func(t *testing.T) { + status := MarkNodePhase(fakeSensor, "first_node", v1alpha1.NodeTypeEventDependency, v1alpha1.NodePhaseActive, &apicommon.Event{ + Payload: []byte("test payload"), + }, logger) + assert.Equal(t, status.Phase, v1alpha1.NodePhaseActive) + }, + }, + { + name: "reapply the update", + testFunc: func(t *testing.T) { + err := ReapplyUpdate(fakeSensorClient, fakeSensor) + assert.Nil(t, err) + }, + }, + { + name: "fetch sensor and check updates are applied", + testFunc: func(t *testing.T) { + updatedSensor, err := fakeSensorClient.ArgoprojV1alpha1().Sensors(fakeSensor.Namespace).Get(fakeSensor.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.Equal(t, len(updatedSensor.Status.Nodes), 1) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.testFunc(t) + }) + } +} diff --git a/controllers/sensor/operator.go b/controllers/sensor/operator.go index 99c058cd8c..95227ac995 100644 --- a/controllers/sensor/operator.go +++ b/controllers/sensor/operator.go @@ -17,388 +17,327 @@ limitations under the License. package sensor import ( - "github.com/sirupsen/logrus" "time" - "github.com/pkg/errors" - "github.com/argoproj/argo-events/common" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" "github.com/argoproj/argo-events/pkg/apis/sensor" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - corev1 "k8s.io/api/core/v1" + sensorclientset "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" ) // the context of an operation on a sensor. -// the sensor-controller creates this context each time it picks a Sensor off its queue. -type sOperationCtx struct { - // s is the sensor object - s *v1alpha1.Sensor +// the controller creates this context each time it picks a Sensor off its queue. +type sensorContext struct { + // sensor is the sensor object + sensor *v1alpha1.Sensor // updated indicates whether the sensor object was updated and needs to be persisted back to k8 updated bool - // log is the logrus logging context to correlate logs with a sensor - log *logrus.Logger - // reference to the sensor-controller - controller *SensorController - // srctx is the context to handle child resource - srctx sResourceCtx + // logger logs stuff + logger *logrus.Logger + // reference to the controller + controller *Controller } -// newSensorOperationCtx creates and initializes a new sOperationCtx object -func newSensorOperationCtx(s *v1alpha1.Sensor, controller *SensorController) *sOperationCtx { - return &sOperationCtx{ - s: s.DeepCopy(), +// newSensorContext creates and initializes a new sensorContext object +func newSensorContext(sensorObj *v1alpha1.Sensor, controller *Controller) *sensorContext { + return &sensorContext{ + sensor: sensorObj.DeepCopy(), updated: false, - log: common.NewArgoEventsLogger().WithFields( + logger: common.NewArgoEventsLogger().WithFields( map[string]interface{}{ - common.LabelSensorName: s.Name, - common.LabelNamespace: s.Namespace, + common.LabelSensorName: sensorObj.Name, + common.LabelNamespace: sensorObj.Namespace, }).Logger, controller: controller, - srctx: NewSensorResourceContext(s, controller), } } -// operate on sensor resource -func (soc *sOperationCtx) operate() error { - defer func() { - if soc.updated { - // persist updates to sensor resource - labels := map[string]string{ - common.LabelSensorName: soc.s.Name, - common.LabelSensorKeyPhase: string(soc.s.Status.Phase), - common.LabelKeySensorControllerInstanceID: soc.controller.Config.InstanceID, - common.LabelOperation: "persist_state_update", - } - eventType := common.StateChangeEventType +// operate manages the lifecycle of a sensor object +func (ctx *sensorContext) operate() error { + defer ctx.updateSensorState() - updatedSensor, err := PersistUpdates(soc.controller.sensorClientset, soc.s, soc.controller.Config.InstanceID, soc.log) - if err != nil { - soc.log.WithError(err).Error("failed to persist sensor update, escalating...") + ctx.logger.Infoln("processing the sensor") - // escalate failure - eventType = common.EscalationEventType - } - - // update sensor ref. in case of failure to persist updates, this is a deep copy of old sensor resource - soc.s = updatedSensor - - labels[common.LabelEventType] = string(eventType) - if err := common.GenerateK8sEvent(soc.controller.kubeClientset, - "persist update", - eventType, - "sensor state update", - soc.s.Name, - soc.s.Namespace, - soc.controller.Config.InstanceID, - sensor.Kind, - labels); err != nil { - soc.log.WithError(err).Error("failed to create K8s event to log sensor state persist operation") - return - } - soc.log.Info("successfully persisted sensor resource update and created K8s event") - } - soc.updated = false - }() + // Validation failure prevents any sort processing of the sensor object + if err := ValidateSensor(ctx.sensor); err != nil { + ctx.logger.WithError(err).Errorln("failed to validate sensor") + ctx.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) + return err + } - switch soc.s.Status.Phase { + switch ctx.sensor.Status.Phase { case v1alpha1.NodePhaseNew: - err := soc.createSensorResources() - if err != nil { - return err + // If the sensor phase is new + // 1. Initialize all nodes - dependencies, dependency groups and triggers + // 2. Make dependencies and dependency groups as active + // 3. Create a deployment and service (if needed) for the sensor + ctx.initializeAllNodes() + ctx.markDependencyNodesActive() + + if err := ctx.createSensorResources(); err != nil { + ctx.logger.WithError(err).Errorln("failed to create resources for the sensor") + ctx.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) + return nil } + ctx.markSensorPhase(v1alpha1.NodePhaseActive, false, "sensor is active") + ctx.logger.Infoln("successfully created resources for the sensor. sensor is in active state") case v1alpha1.NodePhaseActive: - soc.log.Info("sensor is running") - - err := soc.updateSensorResources() - if err != nil { + ctx.logger.Infoln("checking for updates to the sensor object") + if err := ctx.updateSensorResources(); err != nil { + ctx.logger.WithError(err).Errorln("failed to update the sensor resources") return err } + ctx.updated = true + ctx.logger.Infoln("successfully processed sensor state update") case v1alpha1.NodePhaseError: - soc.log.Info("sensor is in error state. check sensor resource status information and corresponding escalated K8 event for the error") - - err := soc.updateSensorResources() - if err != nil { + // If the sensor is in error state and if the sensor podTemplate spec has changed, then update the corresponding deployment + ctx.logger.Info("sensor is in error state, checking for updates to the sensor object") + if err := ctx.updateSensorResources(); err != nil { + ctx.logger.WithError(err).Errorln("failed to update the sensor resources") return err } + ctx.markSensorPhase(v1alpha1.NodePhaseActive, false, "sensor is active") + ctx.logger.Infoln("successfully processed the update") } + return nil } -func (soc *sOperationCtx) createSensorResources() error { - err := ValidateSensor(soc.s) +// createSensorResources creates the K8s resources for a sensor object +func (ctx *sensorContext) createSensorResources() error { + if ctx.sensor.Status.Resources == nil { + ctx.sensor.Status.Resources = &v1alpha1.SensorResources{} + } + + ctx.logger.Infoln("generating deployment specification for the sensor") + deployment, err := ctx.deploymentBuilder() if err != nil { - soc.log.WithError(err).Error("failed to validate sensor") - err = errors.Wrap(err, "failed to validate sensor") - soc.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) return err } - - soc.initializeAllNodes() - pod, err := soc.createSensorPod() + ctx.logger.WithField("name", deployment.Name).Infoln("creating the deployment resource for the sensor") + deployment, err = ctx.createDeployment(deployment) if err != nil { - err = errors.Wrap(err, "failed to create sensor pod") - soc.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) return err } - soc.markAllNodePhases() - soc.log.WithField(common.LabelPodName, pod.Name).Info("sensor pod is created") + ctx.sensor.Status.Resources.Deployment = &deployment.ObjectMeta - // expose sensor if service is configured - if soc.srctx.getServiceTemplateSpec() != nil { - svc, err := soc.createSensorService() + if ctx.sensor.Spec.EventProtocol.Type == apicommon.HTTP { + ctx.logger.Infoln("generating service specification for the sensor") + service, err := ctx.serviceBuilder() if err != nil { - err = errors.Wrap(err, "failed to create sensor service") - soc.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) return err } - soc.log.WithField(common.LabelServiceName, svc.Name).Info("sensor service is created") - } - - // if we get here - we know the signals are running - soc.log.Info("marking sensor as active") - soc.markSensorPhase(v1alpha1.NodePhaseActive, false, "listening for events") - return nil -} -func (soc *sOperationCtx) createSensorPod() (*corev1.Pod, error) { - pod, err := soc.srctx.newSensorPod() - if err != nil { - soc.log.WithError(err).Error("failed to initialize pod for sensor") - return nil, err - } - pod, err = soc.srctx.createSensorPod(pod) - if err != nil { - soc.log.WithError(err).Error("failed to create pod for sensor") - return nil, err - } - return pod, nil -} - -func (soc *sOperationCtx) createSensorService() (*corev1.Service, error) { - svc, err := soc.srctx.newSensorService() - if err != nil { - soc.log.WithError(err).Error("failed to initialize service for sensor") - return nil, err - } - svc, err = soc.srctx.createSensorService(svc) - if err != nil { - soc.log.WithError(err).Error("failed to create service for sensor") - return nil, err - } - return svc, nil -} - -func (soc *sOperationCtx) updateSensorResources() error { - err := ValidateSensor(soc.s) - if err != nil { - soc.log.WithError(err).Error("failed to validate sensor") - err = errors.Wrap(err, "failed to validate sensor") - if soc.s.Status.Phase != v1alpha1.NodePhaseError { - soc.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) + ctx.logger.WithField("name", service.Name).Infoln("generating deployment specification for the sensor") + service, err = ctx.createService(service) + if err != nil { + return err } + ctx.sensor.Status.Resources.Service = &service.ObjectMeta return err } + return nil +} - _, podChanged, err := soc.updateSensorPod() +// updateSensorResources updates the sensor resources +func (ctx *sensorContext) updateSensorResources() error { + deployment, err := ctx.updateDeployment() if err != nil { - err = errors.Wrap(err, "failed to update sensor pod") - soc.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) return err } - - _, svcChanged, err := soc.updateSensorService() + ctx.sensor.Status.Resources.Deployment = &deployment.ObjectMeta + service, err := ctx.updateService() if err != nil { - err = errors.Wrap(err, "failed to update sensor service") - soc.markSensorPhase(v1alpha1.NodePhaseError, false, err.Error()) return err } - - if soc.s.Status.Phase != v1alpha1.NodePhaseActive && (podChanged || svcChanged) { - soc.markSensorPhase(v1alpha1.NodePhaseActive, false, "sensor is active") + if service == nil { + ctx.sensor.Status.Resources.Service = nil + return nil } - + ctx.sensor.Status.Resources.Service = &service.ObjectMeta return nil } -func (soc *sOperationCtx) updateSensorPod() (*corev1.Pod, bool, error) { - // Check if sensor spec has changed for pod. - existingPod, err := soc.srctx.getSensorPod() - if err != nil { - soc.log.WithError(err).Error("failed to get pod for sensor") - return nil, false, err - } - - // create a new pod spec - newPod, err := soc.srctx.newSensorPod() - if err != nil { - soc.log.WithError(err).Error("failed to initialize pod for sensor") - return nil, false, err - } - - // check if pod spec remained unchanged - if existingPod != nil { - if existingPod.Annotations != nil && existingPod.Annotations[common.AnnotationSensorResourceSpecHashName] == newPod.Annotations[common.AnnotationSensorResourceSpecHashName] { - soc.log.WithField(common.LabelPodName, existingPod.Name).Debug("sensor pod spec unchanged") - return nil, false, nil +// updateSensorState updates the sensor resource state +func (ctx *sensorContext) updateSensorState() { + if ctx.updated { + // persist updates to sensor resource + labels := map[string]string{ + common.LabelSensorName: ctx.sensor.Name, + LabelPhase: string(ctx.sensor.Status.Phase), + LabelControllerInstanceID: ctx.controller.Config.InstanceID, + common.LabelOperation: "persist_state_update", } + eventType := common.StateChangeEventType - // By now we are sure that the spec changed, so lets go ahead and delete the exisitng sensor pod. - soc.log.WithField(common.LabelPodName, existingPod.Name).Info("sensor pod spec changed") - - err := soc.srctx.deleteSensorPod(existingPod) + updatedSensor, err := PersistUpdates(ctx.controller.sensorClient, ctx.sensor, ctx.logger) if err != nil { - soc.log.WithError(err).Error("failed to delete pod for sensor") - return nil, false, err - } - - soc.log.WithField(common.LabelPodName, existingPod.Name).Info("sensor pod is deleted") - } - - // Create new pod for updated sensor spec. - createdPod, err := soc.srctx.createSensorPod(newPod) - if err != nil { - soc.log.WithError(err).Error("failed to create pod for sensor") - return nil, false, err - } - soc.log.WithField(common.LabelPodName, newPod.Name).Info("sensor pod is created") - - return createdPod, true, nil -} - -func (soc *sOperationCtx) updateSensorService() (*corev1.Service, bool, error) { - // Check if sensor spec has changed for service. - existingSvc, err := soc.srctx.getSensorService() - if err != nil { - soc.log.WithError(err).Error("failed to get service for sensor") - return nil, false, err - } - - // create a new service spec - newSvc, err := soc.srctx.newSensorService() - if err != nil { - soc.log.WithError(err).Error("failed to initialize service for sensor") - return nil, false, err - } - - if existingSvc != nil { - // updated spec doesn't have service defined, delete existing service. - if newSvc == nil { - if err := soc.srctx.deleteSensorService(existingSvc); err != nil { - return nil, false, err - } - return nil, true, nil - } + ctx.logger.WithError(err).Errorln("failed to persist sensor update") - // check if service spec remained unchanged - if existingSvc.Annotations[common.AnnotationSensorResourceSpecHashName] == newSvc.Annotations[common.AnnotationSensorResourceSpecHashName] { - soc.log.WithField(common.LabelServiceName, existingSvc.Name).Debug("sensor service spec unchanged") - return nil, false, nil + // escalate failure + eventType = common.EscalationEventType } - // service spec changed, delete existing service and create new one - soc.log.WithField(common.LabelServiceName, existingSvc.Name).Info("sensor service spec changed") - - if err := soc.srctx.deleteSensorService(existingSvc); err != nil { - return nil, false, err + // update sensor ref. in case of failure to persist updates, this is a deep copy of old sensor resource + ctx.sensor = updatedSensor + + labels[common.LabelEventType] = string(eventType) + if err := common.GenerateK8sEvent(ctx.controller.k8sClient, + "persist update", + eventType, + "sensor state update", + ctx.sensor.Name, + ctx.sensor.Namespace, + ctx.controller.Config.InstanceID, + sensor.Kind, + labels); err != nil { + ctx.logger.WithError(err).Error("failed to create K8s event to logger sensor state persist operation") + return } - } else if newSvc == nil { - // sensor service doesn't exist originally - return nil, false, nil - } - - // change createSensorService to take a service spec - createdSvc, err := soc.srctx.createSensorService(newSvc) - if err != nil { - soc.log.WithField(common.LabelServiceName, newSvc.Name).WithError(err).Error("failed to create service for sensor") - return nil, false, err + ctx.logger.Info("successfully persisted sensor resource update and created K8s event") } - soc.log.WithField(common.LabelServiceName, newSvc.Name).Info("sensor service is created") - - return createdSvc, true, nil + ctx.updated = false } // mark the overall sensor phase -func (soc *sOperationCtx) markSensorPhase(phase v1alpha1.NodePhase, markComplete bool, message ...string) { - justCompleted := soc.s.Status.Phase != phase +func (ctx *sensorContext) markSensorPhase(phase v1alpha1.NodePhase, markComplete bool, message ...string) { + justCompleted := ctx.sensor.Status.Phase != phase if justCompleted { - soc.log.WithFields( + ctx.logger.WithFields( map[string]interface{}{ - "old": string(soc.s.Status.Phase), + "old": string(ctx.sensor.Status.Phase), "new": string(phase), }, - ).Info("phase updated") + ).Infoln("phase updated") - soc.s.Status.Phase = phase - if soc.s.ObjectMeta.Labels == nil { - soc.s.ObjectMeta.Labels = make(map[string]string) + ctx.sensor.Status.Phase = phase + + if ctx.sensor.ObjectMeta.Labels == nil { + ctx.sensor.ObjectMeta.Labels = make(map[string]string) } - if soc.s.ObjectMeta.Annotations == nil { - soc.s.ObjectMeta.Annotations = make(map[string]string) + + if ctx.sensor.ObjectMeta.Annotations == nil { + ctx.sensor.ObjectMeta.Annotations = make(map[string]string) } - soc.s.ObjectMeta.Labels[common.LabelSensorKeyPhase] = string(phase) - // add annotations so a resource sensor can watch this sensor. - soc.s.ObjectMeta.Annotations[common.LabelSensorKeyPhase] = string(phase) + + ctx.sensor.ObjectMeta.Labels[LabelPhase] = string(phase) + ctx.sensor.ObjectMeta.Annotations[LabelPhase] = string(phase) } - if soc.s.Status.StartedAt.IsZero() { - soc.s.Status.StartedAt = metav1.Time{Time: time.Now().UTC()} + + if ctx.sensor.Status.StartedAt.IsZero() { + ctx.sensor.Status.StartedAt = metav1.Time{Time: time.Now().UTC()} } - if len(message) > 0 && soc.s.Status.Message != message[0] { - soc.log.WithFields( + + if len(message) > 0 && ctx.sensor.Status.Message != message[0] { + ctx.logger.WithFields( map[string]interface{}{ - "old": soc.s.Status.Message, + "old": ctx.sensor.Status.Message, "new": message[0], }, - ).Info("sensor message updated") - soc.s.Status.Message = message[0] + ).Infoln("sensor message updated") + + ctx.sensor.Status.Message = message[0] } switch phase { - case v1alpha1.NodePhaseComplete, v1alpha1.NodePhaseError: + case v1alpha1.NodePhaseError: if markComplete && justCompleted { - soc.log.Info("marking sensor complete") - soc.s.Status.CompletedAt = metav1.Time{Time: time.Now().UTC()} - if soc.s.ObjectMeta.Labels == nil { - soc.s.ObjectMeta.Labels = make(map[string]string) + ctx.logger.Infoln("marking sensor state as complete") + ctx.sensor.Status.CompletedAt = metav1.Time{Time: time.Now().UTC()} + + if ctx.sensor.ObjectMeta.Labels == nil { + ctx.sensor.ObjectMeta.Labels = make(map[string]string) + } + if ctx.sensor.ObjectMeta.Annotations == nil { + ctx.sensor.ObjectMeta.Annotations = make(map[string]string) } - soc.s.ObjectMeta.Labels[common.LabelSensorKeyComplete] = "true" - soc.s.ObjectMeta.Annotations[common.LabelSensorKeyComplete] = string(phase) + + ctx.sensor.ObjectMeta.Labels[LabelComplete] = "true" + ctx.sensor.ObjectMeta.Annotations[LabelComplete] = string(phase) } } - soc.updated = true + ctx.updated = true } -func (soc *sOperationCtx) initializeAllNodes() { +// initializeAllNodes initializes nodes of all types within a sensor +func (ctx *sensorContext) initializeAllNodes() { // Initialize all event dependency nodes - for _, dependency := range soc.s.Spec.Dependencies { - InitializeNode(soc.s, dependency.Name, v1alpha1.NodeTypeEventDependency, soc.log) + for _, dependency := range ctx.sensor.Spec.Dependencies { + InitializeNode(ctx.sensor, dependency.Name, v1alpha1.NodeTypeEventDependency, ctx.logger) } // Initialize all dependency groups - if soc.s.Spec.DependencyGroups != nil { - for _, group := range soc.s.Spec.DependencyGroups { - InitializeNode(soc.s, group.Name, v1alpha1.NodeTypeDependencyGroup, soc.log) + if ctx.sensor.Spec.DependencyGroups != nil { + for _, group := range ctx.sensor.Spec.DependencyGroups { + InitializeNode(ctx.sensor, group.Name, v1alpha1.NodeTypeDependencyGroup, ctx.logger) } } // Initialize all trigger nodes - for _, trigger := range soc.s.Spec.Triggers { - InitializeNode(soc.s, trigger.Template.Name, v1alpha1.NodeTypeTrigger, soc.log) + for _, trigger := range ctx.sensor.Spec.Triggers { + InitializeNode(ctx.sensor, trigger.Template.Name, v1alpha1.NodeTypeTrigger, ctx.logger) } } -func (soc *sOperationCtx) markAllNodePhases() { +// markDependencyNodesActive marks phase of all dependencies and dependency groups as active +func (ctx *sensorContext) markDependencyNodesActive() { // Mark all event dependency nodes as active - for _, dependency := range soc.s.Spec.Dependencies { - MarkNodePhase(soc.s, dependency.Name, v1alpha1.NodeTypeEventDependency, v1alpha1.NodePhaseActive, nil, soc.log, "node is active") + for _, dependency := range ctx.sensor.Spec.Dependencies { + MarkNodePhase(ctx.sensor, dependency.Name, v1alpha1.NodeTypeEventDependency, v1alpha1.NodePhaseActive, nil, ctx.logger, "node is active") } // Mark all dependency groups as active - if soc.s.Spec.DependencyGroups != nil { - for _, group := range soc.s.Spec.DependencyGroups { - MarkNodePhase(soc.s, group.Name, v1alpha1.NodeTypeDependencyGroup, v1alpha1.NodePhaseActive, nil, soc.log, "node is active") + if ctx.sensor.Spec.DependencyGroups != nil { + for _, group := range ctx.sensor.Spec.DependencyGroups { + MarkNodePhase(ctx.sensor, group.Name, v1alpha1.NodeTypeDependencyGroup, v1alpha1.NodePhaseActive, nil, ctx.logger, "node is active") + } + } +} + +// PersistUpdates persists the updates to the Sensor resource +func PersistUpdates(client sensorclientset.Interface, sensorObj *v1alpha1.Sensor, log *logrus.Logger) (*v1alpha1.Sensor, error) { + sensorClient := client.ArgoprojV1alpha1().Sensors(sensorObj.ObjectMeta.Namespace) + // in case persist update fails + oldsensor := sensorObj.DeepCopy() + + sensorObj, err := sensorClient.Update(sensorObj) + if err != nil { + if errors.IsConflict(err) { + log.WithError(err).Error("error updating sensor") + return oldsensor, err + } + + log.Infoln(err) + log.Infoln("re-applying updates on latest version and retrying update") + err = ReapplyUpdate(client, sensorObj) + if err != nil { + log.WithError(err).Error("failed to re-apply update") + return oldsensor, err } } + log.WithField(common.LabelPhase, string(sensorObj.Status.Phase)).Info("sensor state updated successfully") + return sensorObj, nil +} + +// Reapply the update to sensor +func ReapplyUpdate(sensorClient sensorclientset.Interface, sensor *v1alpha1.Sensor) error { + return wait.ExponentialBackoff(common.DefaultRetry, func() (bool, error) { + client := sensorClient.ArgoprojV1alpha1().Sensors(sensor.Namespace) + s, err := client.Update(sensor) + if err != nil { + if !common.IsRetryableKubeAPIError(err) { + return false, err + } + return false, nil + } + sensor = s + return true, nil + }) } diff --git a/controllers/sensor/operator_test.go b/controllers/sensor/operator_test.go index 7809e05c75..e81d804887 100644 --- a/controllers/sensor/operator_test.go +++ b/controllers/sensor/operator_test.go @@ -17,356 +17,146 @@ limitations under the License. package sensor import ( - "testing" - + "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/cache" + "testing" ) -var sensorStr = ` - -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: artifact-sensor - namespace: argo-events - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events -spec: - template: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: artifact-gateway:input - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - name: artifact-workflow-trigger - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world- - spec: - entrypoint: whalesay - templates: - - - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" - name: whalesay -` +func TestOperate(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + sensor, err := controller.sensorClient.ArgoprojV1alpha1().Sensors(sensorObj.Namespace).Create(sensorObj) + assert.Nil(t, err) + ctx.sensor = sensor.DeepCopy() + + tests := []struct { + name string + updateFunc func() + testFunc func(oldMetadata *v1alpha1.SensorResources) + }{ + { + name: "process a new sensor object", + updateFunc: func() {}, + testFunc: func(oldMetadata *v1alpha1.SensorResources) { + assert.NotNil(t, ctx.sensor.Status.Resources) + metadata := ctx.sensor.Status.Resources + deployment, err := controller.k8sClient.AppsV1().Deployments(metadata.Deployment.Namespace).Get(metadata.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + service, err := controller.k8sClient.CoreV1().Services(metadata.Service.Namespace).Get(metadata.Service.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.Equal(t, v1alpha1.NodePhaseActive, ctx.sensor.Status.Phase) + assert.Equal(t, 2, len(ctx.sensor.Status.Nodes)) + assert.Equal(t, "sensor is active", ctx.sensor.Status.Message) + }, + }, + { + name: "process a sensor object update", + updateFunc: func() { + ctx.sensor.Spec.Template.Spec.Containers[0].Name = "updated-name" + }, + testFunc: func(oldMetadata *v1alpha1.SensorResources) { + assert.NotNil(t, ctx.sensor.Status.Resources) + metadata := ctx.sensor.Status.Resources + deployment, err := controller.k8sClient.AppsV1().Deployments(metadata.Deployment.Namespace).Get(metadata.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.NotEqual(t, oldMetadata.Deployment.Annotations[common.AnnotationResourceSpecHash], deployment.Annotations[common.AnnotationResourceSpecHash]) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Name, "updated-name") + service, err := controller.k8sClient.CoreV1().Services(metadata.Service.Namespace).Get(metadata.Service.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.Equal(t, oldMetadata.Service.Annotations[common.AnnotationResourceSpecHash], service.Annotations[common.AnnotationResourceSpecHash]) + assert.Equal(t, v1alpha1.NodePhaseActive, ctx.sensor.Status.Phase) + assert.Equal(t, "sensor is active", ctx.sensor.Status.Message) + }, + }, + { + name: "process a sensor in error state", + updateFunc: func() { + ctx.sensor.Status.Phase = v1alpha1.NodePhaseError + ctx.sensor.Status.Message = "sensor is in error state" + ctx.sensor.Spec.Template.Spec.Containers[0].Name = "revert-name" + }, + testFunc: func(oldMetadata *v1alpha1.SensorResources) { + assert.Equal(t, v1alpha1.NodePhaseActive, ctx.sensor.Status.Phase) + assert.Equal(t, "sensor is active", ctx.sensor.Status.Message) + }, + }, + } -var ( - sensorPodName = "artifact-sensor" - sensorSvcName = "artifact-sensor-svc" -) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + metadata := ctx.sensor.Status.Resources.DeepCopy() + test.updateFunc() + err := ctx.operate() + assert.Nil(t, err) + test.testFunc(metadata) + }) + } +} -func getSensor() (*v1alpha1.Sensor, error) { - var sensor *v1alpha1.Sensor - err := yaml.Unmarshal([]byte(sensorStr), &sensor) - return sensor, err +func TestUpdateSensorState(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + sensor, err := controller.sensorClient.ArgoprojV1alpha1().Sensors(sensorObj.Namespace).Create(sensorObj) + assert.Nil(t, err) + ctx.sensor = sensor.DeepCopy() + assert.Equal(t, v1alpha1.NodePhaseNew, ctx.sensor.Status.Phase) + ctx.sensor.Status.Phase = v1alpha1.NodePhaseActive + ctx.updated = true + ctx.updateSensorState() + assert.Equal(t, v1alpha1.NodePhaseActive, ctx.sensor.Status.Phase) } -func waitForAllInformers(done chan struct{}, controller *SensorController) { - cache.WaitForCacheSync(done, controller.informer.HasSynced) - cache.WaitForCacheSync(done, controller.podInformer.Informer().HasSynced) - cache.WaitForCacheSync(done, controller.svcInformer.Informer().HasSynced) +func TestMarkSensorPhase(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + sensor, err := controller.sensorClient.ArgoprojV1alpha1().Sensors(sensorObj.Namespace).Create(sensorObj) + assert.Nil(t, err) + ctx.sensor = sensor.DeepCopy() + ctx.markSensorPhase(v1alpha1.NodePhaseActive, false, "sensor is active") + assert.Equal(t, v1alpha1.NodePhaseActive, ctx.sensor.Status.Phase) + assert.Equal(t, "sensor is active", ctx.sensor.Status.Message) } -func getPodAndService(controller *SensorController, namespace string) (*corev1.Pod, *corev1.Service, error) { - pod, err := controller.kubeClientset.CoreV1().Pods(namespace).Get(sensorPodName, metav1.GetOptions{}) - if err != nil { - return nil, nil, err +func TestInitializeAllNodes(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + ctx.initializeAllNodes() + for _, node := range ctx.sensor.Status.Nodes { + assert.Equal(t, v1alpha1.NodePhaseNew, node.Phase) + assert.NotEmpty(t, node.Name) + assert.NotEmpty(t, node.ID) } - svc, err := controller.kubeClientset.CoreV1().Services(namespace).Get(sensorSvcName, metav1.GetOptions{}) - if err != nil { - return nil, nil, err - } - return pod, svc, err } -func deletePodAndService(controller *SensorController, namespace string) error { - err := controller.kubeClientset.CoreV1().Pods(namespace).Delete(sensorPodName, &metav1.DeleteOptions{}) - if err != nil { - return err +func TestMarkDependencyNodesActive(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + ctx.initializeAllNodes() + ctx.markDependencyNodesActive() + for _, node := range ctx.sensor.Status.Nodes { + if node.Type == v1alpha1.NodeTypeEventDependency { + assert.Equal(t, v1alpha1.NodePhaseActive, node.Phase) + } else { + assert.Equal(t, v1alpha1.NodePhaseNew, node.Phase) + } } - err = controller.kubeClientset.CoreV1().Services(namespace).Delete(sensorSvcName, &metav1.DeleteOptions{}) - return err } -func TestSensorOperations(t *testing.T) { - done := make(chan struct{}) - convey.Convey("Given a sensor, parse it", t, func() { - sensor, err := getSensor() - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - - controller := getSensorController() - soc := newSensorOperationCtx(sensor, controller) - convey.ShouldPanic(soc.log, nil) - convey.So(soc, convey.ShouldNotBeNil) - - convey.Convey("Create the sensor", func() { - sensor, err = controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Create(sensor) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - - convey.Convey("Operate on a new sensor", func() { - soc.markSensorPhase(v1alpha1.NodePhaseNew, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Sensor should be marked as active with it's nodes initialized", func() { - sensor, err = controller.sensorClientset.ArgoprojV1alpha1().Sensors(soc.s.Namespace).Get(soc.s.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - - for _, node := range soc.s.Status.Nodes { - switch node.Type { - case v1alpha1.NodeTypeEventDependency: - convey.So(node.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - case v1alpha1.NodeTypeDependencyGroup: - convey.So(node.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - case v1alpha1.NodeTypeTrigger: - convey.So(node.Phase, convey.ShouldEqual, v1alpha1.NodePhaseNew) - } - } - }) - - convey.Convey("Sensor pod and service should be created", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Go to active state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) - - convey.Convey("Operate on sensor in active state", func() { - err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Delete(sensor.Name, &metav1.DeleteOptions{}) - convey.So(err, convey.ShouldBeNil) - sensor, err = controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Create(sensor) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - - soc.markSensorPhase(v1alpha1.NodePhaseNew, false, "test") - - // Operate it once to create pod and service - waitForAllInformers(done, controller) - err = soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Operation must succeed", func() { - soc.markSensorPhase(v1alpha1.NodePhaseActive, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Untouch pod and service", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Stay in active state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) - - convey.Convey("With deleted pod and service", func() { - err := deletePodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Operation must succeed", func() { - soc.markSensorPhase(v1alpha1.NodePhaseActive, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Create pod and service", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Stay in active state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) - }) - - convey.Convey("Change pod and service spec", func() { - soc.srctx.s.Spec.Template.Spec.RestartPolicy = "Never" - soc.srctx.s.Spec.EventProtocol.Http.Port = "1234" - - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Operation must succeed", func() { - soc.markSensorPhase(v1alpha1.NodePhaseActive, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Recreate pod and service", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod.Spec.RestartPolicy, convey.ShouldEqual, "Never") - convey.So(sensorSvc.Spec.Ports[0].TargetPort.IntVal, convey.ShouldEqual, 1234) - - convey.Convey("Stay in active state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) - }) - }) - - convey.Convey("Operate on sensor in error state", func() { - err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Delete(sensor.Name, &metav1.DeleteOptions{}) - convey.So(err, convey.ShouldBeNil) - sensor, err = controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Create(sensor) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - - soc.markSensorPhase(v1alpha1.NodePhaseNew, false, "test") - - // Operate it once to create pod and service - waitForAllInformers(done, controller) - err = soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Operation must succeed", func() { - soc.markSensorPhase(v1alpha1.NodePhaseError, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Untouch pod and service", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Stay in error state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseError) - }) - }) - }) - - convey.Convey("With deleted pod and service", func() { - err := deletePodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - - convey.Convey("Operation must succeed", func() { - soc.markSensorPhase(v1alpha1.NodePhaseError, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Create pod and service", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Go to active state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) - }) - - convey.Convey("Change pod and service spec", func() { - soc.srctx.s.Spec.Template.Spec.RestartPolicy = "Never" - soc.srctx.s.Spec.EventProtocol.Http.Port = "1234" - - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod, convey.ShouldNotBeNil) - convey.So(sensorSvc, convey.ShouldNotBeNil) - - convey.Convey("Operation must succeed", func() { - soc.markSensorPhase(v1alpha1.NodePhaseError, false, "test") - - waitForAllInformers(done, controller) - err := soc.operate() - convey.So(err, convey.ShouldBeNil) - waitForAllInformers(done, controller) - - convey.Convey("Recreate pod and service", func() { - sensorPod, sensorSvc, err := getPodAndService(controller, sensor.Namespace) - convey.So(err, convey.ShouldBeNil) - convey.So(sensorPod.Spec.RestartPolicy, convey.ShouldEqual, "Never") - convey.So(sensorSvc.Spec.Ports[0].TargetPort.IntVal, convey.ShouldEqual, 1234) - - convey.Convey("Go to active state", func() { - sensor, err := controller.sensorClientset.ArgoprojV1alpha1().Sensors(sensor.Namespace).Get(sensor.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(sensor.Status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) - }) - }) - }) - }) +func TestPersistUpdates(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + sensor, err := controller.sensorClient.ArgoprojV1alpha1().Sensors(sensorObj.Namespace).Create(sensorObj) + assert.Nil(t, err) + ctx.sensor = sensor.DeepCopy() + ctx.sensor.Spec.Circuit = "fake-group" + sensor, err = PersistUpdates(controller.sensorClient, ctx.sensor.DeepCopy(), ctx.logger) + assert.Nil(t, err) + assert.Equal(t, "fake-group", sensor.Spec.Circuit) + assert.Equal(t, "fake-group", ctx.sensor.Spec.Circuit) } diff --git a/controllers/sensor/resource.go b/controllers/sensor/resource.go index 83545344d0..d9d12d1e0c 100644 --- a/controllers/sensor/resource.go +++ b/controllers/sensor/resource.go @@ -1,182 +1,178 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package sensor import ( "github.com/argoproj/argo-events/common" controllerscommon "github.com/argoproj/argo-events/controllers/common" - pc "github.com/argoproj/argo-events/pkg/apis/common" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + "github.com/pkg/errors" + appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierror "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/intstr" ) -type sResourceCtx struct { - // s is the gateway-controller object - s *v1alpha1.Sensor - // reference to the gateway-controller-controller - controller *SensorController - - controllerscommon.ChildResourceContext +// generateServiceSpec returns a K8s service spec for the sensor +func (ctx *sensorContext) generateServiceSpec() *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + common.LabelSensorName: ctx.sensor.Name, + LabelControllerInstanceID: ctx.controller.Config.InstanceID, + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: intstr.Parse(ctx.sensor.Spec.EventProtocol.Http.Port).IntVal, + TargetPort: intstr.FromInt(int(intstr.Parse(ctx.sensor.Spec.EventProtocol.Http.Port).IntVal)), + }, + }, + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{ + common.LabelOwnerName: ctx.sensor.Name, + }, + }, + } } -// NewSensorResourceContext returns new sResourceCtx -func NewSensorResourceContext(s *v1alpha1.Sensor, controller *SensorController) sResourceCtx { - return sResourceCtx{ - s: s, - controller: controller, - ChildResourceContext: controllerscommon.ChildResourceContext{ - SchemaGroupVersionKind: v1alpha1.SchemaGroupVersionKind, - LabelOwnerName: common.LabelSensorName, - LabelKeyOwnerControllerInstanceID: common.LabelKeySensorControllerInstanceID, - AnnotationOwnerResourceHashName: common.AnnotationSensorResourceSpecHashName, - InstanceID: controller.Config.InstanceID, - }, +// serviceBuilder builds a new service that exposes sensor. +func (ctx *sensorContext) serviceBuilder() (*corev1.Service, error) { + service := ctx.generateServiceSpec() + if err := controllerscommon.SetObjectMeta(ctx.sensor, service, v1alpha1.SchemaGroupVersionKind); err != nil { + return nil, err } + return service, nil } -// sensorResourceLabelSelector returns label selector of the sensor of the context -func (src *sResourceCtx) sensorResourceLabelSelector() (labels.Selector, error) { - req, err := labels.NewRequirement(common.LabelSensorName, selection.Equals, []string{src.s.Name}) - if err != nil { +// deploymentBuilder builds the deployment specification for the sensor +func (ctx *sensorContext) deploymentBuilder() (*appv1.Deployment, error) { + replicas := int32(1) + podTemplateSpec := ctx.sensor.Spec.Template.DeepCopy() + if podTemplateSpec.Labels == nil { + podTemplateSpec.Labels = map[string]string{} + } + podTemplateSpec.Labels[common.LabelOwnerName] = ctx.sensor.Name + deployment := &appv1.Deployment{ + ObjectMeta: podTemplateSpec.ObjectMeta, + Spec: appv1.DeploymentSpec{ + Template: *podTemplateSpec, + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: podTemplateSpec.Labels, + }, + }, + } + envVars := []corev1.EnvVar{ + { + Name: common.SensorName, + Value: ctx.sensor.Name, + }, + { + Name: common.SensorNamespace, + Value: ctx.sensor.Namespace, + }, + { + Name: common.EnvVarControllerInstanceID, + Value: ctx.controller.Config.InstanceID, + }, + } + for i, container := range deployment.Spec.Template.Spec.Containers { + container.Env = append(container.Env, envVars...) + deployment.Spec.Template.Spec.Containers[i] = container + } + if err := controllerscommon.SetObjectMeta(ctx.sensor, deployment, v1alpha1.SchemaGroupVersionKind); err != nil { return nil, err } - return labels.NewSelector().Add(*req), nil + return deployment, nil } -// createSensorService creates a service -func (src *sResourceCtx) createSensorService(svc *corev1.Service) (*corev1.Service, error) { - return src.controller.kubeClientset.CoreV1().Services(src.s.Namespace).Create(svc) +// createDeployment creates a deployment for the sensor +func (ctx *sensorContext) createDeployment(deployment *appv1.Deployment) (*appv1.Deployment, error) { + return ctx.controller.k8sClient.AppsV1().Deployments(deployment.Namespace).Create(deployment) } -// deleteSensorService deletes a given service -func (src *sResourceCtx) deleteSensorService(svc *corev1.Service) error { - return src.controller.kubeClientset.CoreV1().Services(src.s.Namespace).Delete(svc.Name, &metav1.DeleteOptions{}) +// createService creates a service for the sensor +func (ctx *sensorContext) createService(service *corev1.Service) (*corev1.Service, error) { + return ctx.controller.k8sClient.CoreV1().Services(service.Namespace).Create(service) } -// getSensorService returns the service of sensor -func (src *sResourceCtx) getSensorService() (*corev1.Service, error) { - selector, err := src.sensorResourceLabelSelector() - if err != nil { - return nil, err - } - svcs, err := src.controller.svcInformer.Lister().Services(src.s.Namespace).List(selector) +// updateDeployment updates the deployment for the sensor +func (ctx *sensorContext) updateDeployment() (*appv1.Deployment, error) { + newDeployment, err := ctx.deploymentBuilder() if err != nil { return nil, err } - if len(svcs) == 0 { - return nil, nil - } - return svcs[0], nil -} -// newSensorService returns a new service that exposes sensor. -func (src *sResourceCtx) newSensorService() (*corev1.Service, error) { - serviceTemplateSpec := src.getServiceTemplateSpec() - if serviceTemplateSpec == nil { - return nil, nil + currentMetadata := ctx.sensor.Status.Resources.Deployment + if currentMetadata == nil { + return nil, errors.New("deployment metadata is expected to be set in gateway object") } - service := &corev1.Service{ - ObjectMeta: serviceTemplateSpec.ObjectMeta, - Spec: serviceTemplateSpec.Spec, - } - if service.Namespace == "" { - service.Namespace = src.s.Namespace - } - if service.Name == "" { - service.Name = common.DefaultServiceName(src.s.Name) - } - err := src.SetObjectMeta(src.s, service) - return service, err -} -// getSensorPod returns the pod of sensor -func (src *sResourceCtx) getSensorPod() (*corev1.Pod, error) { - selector, err := src.sensorResourceLabelSelector() - if err != nil { - return nil, err - } - pods, err := src.controller.podInformer.Lister().Pods(src.s.Namespace).List(selector) + currentDeployment, err := ctx.controller.k8sClient.AppsV1().Deployments(currentMetadata.Namespace).Get(currentMetadata.Name, metav1.GetOptions{}) if err != nil { + if apierror.IsNotFound(err) { + return ctx.controller.k8sClient.AppsV1().Deployments(newDeployment.Namespace).Create(newDeployment) + } return nil, err } - if len(pods) == 0 { - return nil, nil - } - return pods[0], nil -} -// createSensorPod creates a pod of sensor -func (src *sResourceCtx) createSensorPod(pod *corev1.Pod) (*corev1.Pod, error) { - return src.controller.kubeClientset.CoreV1().Pods(src.s.Namespace).Create(pod) + if currentDeployment.Annotations != nil && currentDeployment.Annotations[common.AnnotationResourceSpecHash] != newDeployment.Annotations[common.AnnotationResourceSpecHash] { + if err := ctx.controller.k8sClient.AppsV1().Deployments(currentDeployment.Namespace).Delete(currentDeployment.Name, &metav1.DeleteOptions{}); err != nil { + return nil, err + } + return ctx.controller.k8sClient.AppsV1().Deployments(newDeployment.Namespace).Create(newDeployment) + } + return currentDeployment, nil } -// deleteSensorPod deletes a given pod -func (src *sResourceCtx) deleteSensorPod(pod *corev1.Pod) error { - return src.controller.kubeClientset.CoreV1().Pods(src.s.Namespace).Delete(pod.Name, &metav1.DeleteOptions{}) -} +// updateService updates the service for the sensor +func (ctx *sensorContext) updateService() (*corev1.Service, error) { + isHttpTransport := ctx.sensor.Spec.EventProtocol.Type == apicommon.HTTP + currentMetadata := ctx.sensor.Status.Resources.Service -// newSensorPod returns a new pod of sensor -func (src *sResourceCtx) newSensorPod() (*corev1.Pod, error) { - podTemplateSpec := src.s.Spec.Template.DeepCopy() - pod := &corev1.Pod{ - ObjectMeta: podTemplateSpec.ObjectMeta, - Spec: podTemplateSpec.Spec, + if currentMetadata == nil && !isHttpTransport { + return nil, nil } - if pod.Namespace == "" { - pod.Namespace = src.s.Namespace + if currentMetadata != nil && !isHttpTransport { + if err := ctx.controller.k8sClient.CoreV1().Services(currentMetadata.Namespace).Delete(currentMetadata.Name, &metav1.DeleteOptions{}); err != nil { + // warning is sufficient instead of halting the entire sensor operation by marking it as failed. + ctx.logger.WithField("service-name", currentMetadata.Name).WithError(err).Warnln("failed to delete the current service") + } + return nil, nil } - if pod.Name == "" { - pod.Name = src.s.Name + newService, err := ctx.serviceBuilder() + if err != nil { + return nil, err } - src.setupContainersForSensorPod(pod) - err := src.SetObjectMeta(src.s, pod) - return pod, err -} - -// containers required for sensor deployment -func (src *sResourceCtx) setupContainersForSensorPod(pod *corev1.Pod) { - // env variables - envVars := []corev1.EnvVar{ - { - Name: common.SensorName, - Value: src.s.Name, - }, - { - Name: common.SensorNamespace, - Value: src.s.Namespace, - }, - { - Name: common.EnvVarSensorControllerInstanceID, - Value: src.controller.Config.InstanceID, - }, + if currentMetadata == nil && isHttpTransport { + return ctx.controller.k8sClient.CoreV1().Services(newService.Namespace).Create(newService) } - for i, container := range pod.Spec.Containers { - container.Env = append(container.Env, envVars...) - pod.Spec.Containers[i] = container + if currentMetadata == nil { + return nil, nil } -} - -func (src *sResourceCtx) getServiceTemplateSpec() *pc.ServiceTemplateSpec { - var serviceSpec *pc.ServiceTemplateSpec - // Create a ClusterIP service to expose sensor in cluster if the event protocol type is HTTP - if src.s.Spec.EventProtocol.Type == pc.HTTP { - serviceSpec = &pc.ServiceTemplateSpec{ - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - { - Port: intstr.Parse(src.s.Spec.EventProtocol.Http.Port).IntVal, - TargetPort: intstr.FromInt(int(intstr.Parse(src.s.Spec.EventProtocol.Http.Port).IntVal)), - }, - }, - Type: corev1.ServiceTypeClusterIP, - Selector: map[string]string{ - common.LabelSensorName: src.s.Name, - common.LabelKeySensorControllerInstanceID: src.controller.Config.InstanceID, - }, - }, + if currentMetadata.Annotations != nil && currentMetadata.Annotations[common.AnnotationResourceSpecHash] != newService.Annotations[common.AnnotationResourceSpecHash] { + if err := ctx.controller.k8sClient.CoreV1().Services(currentMetadata.Namespace).Delete(currentMetadata.Name, &metav1.DeleteOptions{}); err != nil { + return nil, err } + return ctx.controller.k8sClient.CoreV1().Services(newService.Namespace).Create(newService) } - return serviceSpec + return ctx.controller.k8sClient.CoreV1().Services(currentMetadata.Namespace).Get(currentMetadata.Name, metav1.GetOptions{}) } diff --git a/controllers/sensor/resource_test.go b/controllers/sensor/resource_test.go new file mode 100644 index 0000000000..3cc3b1b569 --- /dev/null +++ b/controllers/sensor/resource_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sensor + +import ( + "testing" + + "github.com/argoproj/argo-events/common" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var sensorObj = &v1alpha1.Sensor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-sensor", + Namespace: "faker", + }, + Spec: v1alpha1.SensorSpec{ + Template: &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-sensor", + Namespace: "faker", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "fake-sensor", + ImagePullPolicy: corev1.PullAlways, + Image: "argoproj/sensor", + }, + }, + }, + }, + EventProtocol: &apicommon.EventProtocol{ + Http: apicommon.Http{ + Port: "12000", + }, + Type: apicommon.HTTP, + }, + Triggers: []v1alpha1.Trigger{ + { + Template: &v1alpha1.TriggerTemplate{ + Name: "fake-trigger", + GroupVersionResource: &metav1.GroupVersionResource{ + Group: "k8s.io", + Version: "", + Resource: "pods", + }, + Source: &v1alpha1.ArtifactLocation{}, + }, + }, + }, + Dependencies: []v1alpha1.EventDependency{ + { + Name: "fake-gateway:fake-one", + }, + }, + }, +} + +func TestResource_BuildService(t *testing.T) { + controller := getController() + opctx := newSensorContext(sensorObj.DeepCopy(), controller) + service, err := opctx.serviceBuilder() + assert.Nil(t, err) + assert.NotNil(t, service) + assert.NotEmpty(t, service.Annotations[common.AnnotationResourceSpecHash]) +} + +func TestResource_BuildDeployment(t *testing.T) { + controller := getController() + opctx := newSensorContext(sensorObj.DeepCopy(), controller) + deployment, err := opctx.deploymentBuilder() + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.NotEmpty(t, deployment.Annotations[common.AnnotationResourceSpecHash]) + assert.Equal(t, int(*deployment.Spec.Replicas), 1) +} + +func TestResource_SetupContainers(t *testing.T) { + controller := getController() + opctx := newSensorContext(sensorObj.DeepCopy(), controller) + deployment, err := opctx.deploymentBuilder() + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Env[0].Name, common.SensorName) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Env[0].Value, opctx.sensor.Name) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Env[1].Name, common.SensorNamespace) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Env[1].Value, opctx.sensor.Namespace) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Env[2].Name, common.EnvVarControllerInstanceID) + assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Env[2].Value, controller.Config.InstanceID) +} + +func TestResource_UpdateResources(t *testing.T) { + controller := getController() + ctx := newSensorContext(sensorObj.DeepCopy(), controller) + err := ctx.createSensorResources() + assert.Nil(t, err) + + tests := []struct { + name string + updateFunc func() + testFunc func(t *testing.T, oldResources *v1alpha1.SensorResources) + }{ + { + name: "update deployment when sensor template is updated", + updateFunc: func() { + ctx.sensor.Spec.Template.Spec.Containers[0].ImagePullPolicy = corev1.PullIfNotPresent + }, + testFunc: func(t *testing.T, oldResources *v1alpha1.SensorResources) { + oldDeployment := oldResources.Deployment + deployment, err := ctx.controller.k8sClient.AppsV1().Deployments(ctx.sensor.Status.Resources.Deployment.Namespace).Get(ctx.sensor.Status.Resources.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.NotEqual(t, oldDeployment.Annotations[common.AnnotationResourceSpecHash], deployment.Annotations[common.AnnotationResourceSpecHash]) + + oldService := oldResources.Service + service, err := ctx.controller.k8sClient.CoreV1().Services(ctx.sensor.Status.Resources.Service.Namespace).Get(ctx.sensor.Status.Resources.Service.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, service) + assert.Equal(t, oldService.Annotations[common.AnnotationResourceSpecHash], service.Annotations[common.AnnotationResourceSpecHash]) + }, + }, + { + name: "update event protocol to NATS and check the service deletion", + updateFunc: func() { + ctx.sensor.Spec.EventProtocol.Type = apicommon.NATS + }, + testFunc: func(t *testing.T, oldResources *v1alpha1.SensorResources) { + oldDeployment := oldResources.Deployment + deployment, err := ctx.controller.k8sClient.AppsV1().Deployments(ctx.sensor.Status.Resources.Deployment.Namespace).Get(ctx.sensor.Status.Resources.Deployment.Name, metav1.GetOptions{}) + assert.Nil(t, err) + assert.NotNil(t, deployment) + assert.Equal(t, oldDeployment.Annotations[common.AnnotationResourceSpecHash], deployment.Annotations[common.AnnotationResourceSpecHash]) + + oldService := oldResources.Service + service, err := ctx.controller.k8sClient.CoreV1().Services(oldService.Namespace).Get(oldService.Name, metav1.GetOptions{}) + assert.NotNil(t, err) + assert.Nil(t, service) + }, + }, + } + + for _, test := range tests { + oldResources := ctx.sensor.Status.Resources.DeepCopy() + t.Run(test.name, func(t *testing.T) { + test.updateFunc() + err := ctx.updateSensorResources() + assert.Nil(t, err) + test.testFunc(t, oldResources) + }) + } +} diff --git a/controllers/sensor/state_test.go b/controllers/sensor/state_test.go deleted file mode 100644 index 3142d1ff3b..0000000000 --- a/controllers/sensor/state_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package sensor - -import ( - "testing" - - "github.com/argoproj/argo-events/common" - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - fakesensor "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/fake" - "github.com/smartystreets/goconvey/convey" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestSensorState(t *testing.T) { - fakeSensorClient := fakesensor.NewSimpleClientset() - logger := common.NewArgoEventsLogger() - sn := &v1alpha1.Sensor{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-sensor", - Namespace: "test", - }, - } - - convey.Convey("Given a sensor", t, func() { - convey.Convey("Create the sensor", func() { - sn, err := fakeSensorClient.ArgoprojV1alpha1().Sensors(sn.Namespace).Create(sn) - convey.So(err, convey.ShouldBeNil) - convey.So(sn, convey.ShouldNotBeNil) - }) - - convey.Convey("Initialize a new node", func() { - status := InitializeNode(sn, "first_node", v1alpha1.NodeTypeEventDependency, logger) - convey.So(status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseNew) - }) - - convey.Convey("Persist updates to sn", func() { - sensor, err := PersistUpdates(fakeSensorClient, sn, "1", logger) - convey.So(err, convey.ShouldBeNil) - convey.So(len(sensor.Status.Nodes), convey.ShouldEqual, 1) - }) - - convey.Convey("Mark sn node state to active", func() { - status := MarkNodePhase(sn, "first_node", v1alpha1.NodeTypeEventDependency, v1alpha1.NodePhaseActive, &apicommon.Event{ - Payload: []byte("test payload"), - }, logger) - convey.So(status.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - - convey.Convey("Reapply sn update", func() { - err := ReapplyUpdate(fakeSensorClient, sn) - convey.So(err, convey.ShouldBeNil) - }) - - convey.Convey("Fetch sn and check updates are applied", func() { - sensor, err := fakeSensorClient.ArgoprojV1alpha1().Sensors(sn.Namespace).Get(sn.Name, metav1.GetOptions{}) - convey.So(err, convey.ShouldBeNil) - convey.So(len(sensor.Status.Nodes), convey.ShouldEqual, 1) - convey.Convey("Get the first_node node", func() { - node := GetNodeByName(sensor, "first_node") - convey.So(node, convey.ShouldNotBeNil) - convey.So(node.Phase, convey.ShouldEqual, v1alpha1.NodePhaseActive) - }) - }) - }) -} diff --git a/controllers/sensor/validate.go b/controllers/sensor/validate.go index 5c1045e27a..abce9a72cb 100644 --- a/controllers/sensor/validate.go +++ b/controllers/sensor/validate.go @@ -22,7 +22,6 @@ import ( "time" "github.com/Knetic/govaluate" - "github.com/argoproj/argo-events/common" pc "github.com/argoproj/argo-events/pkg/apis/common" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" diff --git a/controllers/sensor/validate_test.go b/controllers/sensor/validate_test.go index 55ae563275..590b50eb88 100644 --- a/controllers/sensor/validate_test.go +++ b/controllers/sensor/validate_test.go @@ -20,26 +20,22 @@ import ( "fmt" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" "io/ioutil" "testing" - - "github.com/smartystreets/goconvey/convey" ) func TestValidateSensor(t *testing.T) { dir := "../../examples/sensors" - convey.Convey("Validate list of sensor", t, func() { - files, err := ioutil.ReadDir(dir) - convey.So(err, convey.ShouldBeNil) - for _, file := range files { - fmt.Println("filename: ", file.Name()) - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", dir, file.Name())) - convey.So(err, convey.ShouldBeNil) - var sensor *v1alpha1.Sensor - err = yaml.Unmarshal([]byte(content), &sensor) - convey.So(err, convey.ShouldBeNil) - err = ValidateSensor(sensor) - convey.So(err, convey.ShouldBeNil) - } - }) + files, err := ioutil.ReadDir(dir) + assert.Nil(t, err) + for _, file := range files { + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", dir, file.Name())) + assert.Nil(t, err) + var sensor *v1alpha1.Sensor + err = yaml.Unmarshal([]byte(content), &sensor) + assert.Nil(t, err) + err = ValidateSensor(sensor) + assert.Nil(t, err) + } } diff --git a/docs/assets/argo.png b/docs/assets/argo.png new file mode 100644 index 0000000000000000000000000000000000000000..1560ce2490938837d3144d6e8f8c40999dbb73a4 GIT binary patch literal 27999 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

Gx##C5n2WZ)N?LR<+NB&}OK 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�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 K{E4M+;(q{Od`Zm! literal 0 HcmV?d00001 diff --git a/docs/assets/gateways.png b/docs/assets/gateways.png deleted file mode 100644 index 39d040e225bc74b45ba3f1c69293ce3ed1137f0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120284 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 diff --git a/docs/assets/sensor.png b/docs/assets/sensor.png index 1a96f1791e51718e8beb2ce78f3ca7ef0fab4df8..726dfe007ba3c96953c71423d94d242f01e369f3 100644 GIT binary patch literal 86958 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 literal 82058 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 diff --git a/docs/communication.md b/docs/communication.md deleted file mode 100644 index 3a93d4135e..0000000000 --- a/docs/communication.md +++ /dev/null @@ -1,55 +0,0 @@ -# Persisting Events - -1. [How gateway forwards events to sensor](#how-gateway-forwards-events-to-sensor) -2. [HTTP](#http) -3. [NATS Standard & Streaming](#nats-standard--streaming) - -## How gateway forwards events to sensor? -There are two ways an event is dispatched from gateway to sensor: - - 1. **HTTP** - 2. **NATS standard or streaming service** - - -
- -

- Sensor -

- -
- -## HTTP -* To use HTTP as communication channel between gateway and sensor, you need to configure the `eventProtocol` in gateway as HTTP. Then, you need to specify -the port on which the HTTP server in sensor will be running. The HTTP server is spun up automatically with the port configured in sensor spec when -you create the sensor with `eventProtocol` as HTTP. - -* You don't need to specify address of sensor pod. The sensor pod is exposed through a ClusterIP service. This is taken care by the sensor controller. -The name of the sensor service is formatted in a specific way by sensor controller so that gateway can create the service name from sensor name. -This is how gateway gets the name of the service exposing sensor. Using the port defined in the spec, gateway makes HTTP POST requests to sensor service. - - * [**Gateway Example**](https://github.com/argoproj/argo-events/blob/master/examples/gateways/webhook-http.yaml) - * [**Sensor Example**](https://github.com/argoproj/argo-events/blob/master/examples/sensors/webhook-http.yaml) - -## NATS Standard & Streaming -* To use NATS standard or streaming as communication channel between gateway and sensor, you need to configure the `eventProtocol` in gateway as NATS and type as either `Standard` or `Streaming`. -You can read more about NATS [here](https://nats.io/documentation/) - -* In case of NATS, gateway doesn't need to be aware of sensors because the gateway acts as a publisher and sensors act as subscriber. - -* You can store events in external persistent volume. This gives you ability to replay events in future for any reasons. -Read more about storing NATS messages [here](https://nats.io/blog/use-cases-for-persistent-logs-with-nats-streaming/) - -* NATS also facilitates the components that are not part of Argo-Events to consume events generated by gateway. - -* For a sensor to consume the events from NATS, the `eventProtocol` needs to specified as NATS. You can then configure the Standard or Streaming connection detail in `eventProtocol`. - - 1. Standard NATS example - * [**Gateway Example**](https://github.com/argoproj/argo-events/blob/master/examples/gateways/webhook-nats-standard.yaml) - * [**Sensor Example**](https://github.com/argoproj/argo-events/blob/master/examples/sensors/webhook-nats.yaml) - - 2. Streaming NATS example - * [**Gateway Example**](https://github.com/argoproj/argo-events/blob/master/examples/gateways/webhook-nats-streaming.yaml) - * [**Sensor Example**](https://github.com/argoproj/argo-events/blob/master/examples/sensors/webhook-nats-streaming.yaml) - - **Note**: The framework **_does not_** provide a NATS installation. You can follow [this guide](https://github.com/nats-io/nats-streaming-operator) to install NATS onto your cluster. diff --git a/docs/concepts/event_source.md b/docs/concepts/event_source.md new file mode 100644 index 0000000000..dec9bb6eec --- /dev/null +++ b/docs/concepts/event_source.md @@ -0,0 +1,7 @@ +# Event Source + +Event Source are configuration store for a gateway to select from. The configuration stored in an Event Source is used by a gateway to consume events from +external entities like AWS SNS, SQS, GCP PubSub, Webhooks etc. + +## Specification +Complete specification is available [here](https://github.com/argoproj/argo-events/blob/master/api/event-source.md). diff --git a/docs/concepts/gateway.md b/docs/concepts/gateway.md new file mode 100644 index 0000000000..74d06bf956 --- /dev/null +++ b/docs/concepts/gateway.md @@ -0,0 +1,20 @@ +# Gateway + +## What is a gateway? +A gateway consumes events from outside entities, transforms them into the [cloudevents specification](https://github.com/cloudevents/spec) compliant events and dispatches them to sensors. + +
+ +

+ Gateway +

+ +
+ +## Relation between Gateway & Event Source +Event Source are event configuration store for a gateway. The configuration stored in an Event Source is used by a gateway to consume events from +external entities like AWS SNS, SQS, GCP PubSub, Webhooks etc. + +## Specification +Complete specification is available [here](https://github.com/argoproj/argo-events/blob/master/api/gateway.md) + diff --git a/docs/parameterization.md b/docs/concepts/parameterization.md similarity index 100% rename from docs/parameterization.md rename to docs/concepts/parameterization.md diff --git a/docs/concepts/sensor.md b/docs/concepts/sensor.md new file mode 100644 index 0000000000..ba50aa63f0 --- /dev/null +++ b/docs/concepts/sensor.md @@ -0,0 +1,16 @@ +# Sensor +Sensors define a set of event dependencies (inputs) and triggers (outputs). +
+ +

+ Sensor +

+ +
+ +## What is an event dependency? +A dependency is an event the sensor is expecting to happen. It is defined as "gateway-name:event-source-name". +Also, you can use [globs](https://github.com/gobwas/glob#syntax) to catch a set of events (e.g. "gateway-name:*"). + +## Specification +Complete specification is available [here](https://github.com/argoproj/argo-events/blob/master/api/sensor.md). diff --git a/docs/trigger.md b/docs/concepts/trigger.md similarity index 95% rename from docs/trigger.md rename to docs/concepts/trigger.md index 7e709cd691..6c52691290 100644 --- a/docs/trigger.md +++ b/docs/concepts/trigger.md @@ -5,8 +5,9 @@ Trigger is the resource executed by sensor once the event dependencies are resol ## How to define a trigger? The framework provides support to fetch trigger resources from different sources. + ### Inline -Inlined artifacts are included directly within the sensor resource and decoded as a string. [Example](https://github.com/argoproj/argo-events/tree/master/examples/sensors/artifact.yaml) +Inlined artifacts are included directly within the sensor resource and decoded as a string. [Example](https://github.com/argoproj/argo-events/tree/master/examples/sensors/minio.yaml) ### S3 Argo Events uses the [minio-go](https://github.com/minio/minio-go) client for access to any Amazon S3 compatible object store. [Example](https://github.com/argoproj/argo-events/tree/master/examples/sensors/context-filter-webhook.yaml) @@ -24,7 +25,7 @@ Artifacts stored in Kubernetes configmap are accessed using the key. [Example](h Artifacts stored in either public or private Git repository. [Example](https://github.com/argoproj/argo-events/blob/master/examples/sensors/trigger-source-git.yaml) ### Resource -Artifacts defined as generic K8s resource template. This is specially useful if you use tools like Kustomize to generate the sensor spec. [Example](https://github.com/argoproj/argo-events/blob/master/examples/sensors/trigger-resource.yaml) +Artifacts defined as generic K8s resource template. This is specially useful if you use tools like Kustomize to generate the sensor spec. ## What resource types are supported out of box? - [Argo Workflow](https://github.com/argoproj/argo) diff --git a/docs/controllers.md b/docs/controllers.md index 766f2d11ac..da011de24a 100644 --- a/docs/controllers.md +++ b/docs/controllers.md @@ -1,11 +1,11 @@ ## Controllers -* Sensor and Gateway controllers are the components which manage Sensor and Gateway resources respectively. -* Sensor and Gateway are Kubernetes Custom Resources. For more information on K8 CRDs visit [here.](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +* Sensor and Gateway controllers are the components which manage Sensor and Gateway objects respectively. +* Sensor and Gateway are Kubernetes Custom Resources. For more information on K8 CRDs visit [here.](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) -### Controller configmap -Defines the `instance-id` and the `namespace` for controller configmap +### Controller Configmap +Defines the `instance-id` and the `namespace` for the controller. e.g. ```yaml # The gateway-controller configmap includes configuration information for the gateway-controller @@ -21,16 +21,10 @@ data: `namespace`: If you don't provide namespace, controller will watch all namespaces for gateway resource. -`instanceID`: it is used to map a gateway or sensor to a controller. +`instanceID`: it is used to map a gateway or sensor object to a controller. e.g. when you create a gateway with label `gateways.argoproj.io/gateway-controller-instanceid: argo-events`, a - controller with label `argo-events` will process that gateway. `instanceID` for controller are managed using [controller-configmap](https://raw.githubusercontent.com/argoproj/argo-events/master/hack/k8s/manifests/gateway-controller-configmap.yaml). -Basically `instanceID` is used to horizontally scale controllers, so you won't end up overwhelming a controller with large - number of gateways or sensors. Also keep in mind that `instanceID` has nothing to do with namespace where you are - deploying controllers and gateways/sensors. + controller with label `argo-events` will process that gateway. - -### Gateway controller -Gateway controller watches gateway resource and manages lifecycle of a gateway. - -### Sensor controller -Sensor controller watches sensor resource and manages lifecycle of a sensor. +`instanceID` is used to horizontally scale controllers, so you won't end up overwhelming a single controller with large + number of gateways or sensors. Also keep in mind that `instanceID` has nothing to do with namespace where you are + deploying controllers and gateways/sensors objects. diff --git a/docs/developer_guide.md b/docs/developer_guide.md new file mode 100644 index 0000000000..ed6e1cde5b --- /dev/null +++ b/docs/developer_guide.md @@ -0,0 +1,89 @@ +# Developer Guider + +## Setup your DEV environment +Argo Events is native to Kubernetes so you'll need a running Kubernetes cluster. This guide includes steps for `Minikube` for local development, but if you have another cluster you can ignore the Minikube specific step 3. + +### Requirements +- Golang 1.11 +- Docker +- dep + +### Installation & Setup + +#### 1. Get the project +``` +go get github.com/argoproj/argo-events +cd $GOPATH/src/github.com/argoproj/argo-events +``` + +#### 2. Vendor dependencies +``` +dep ensure -vendor-only +``` + +#### 3. Start Minikube and point Docker Client to Minikube's Docker Daemon +``` +minikube start +eval $(minikube docker-env) +``` + +#### 5. Build the project +``` +make all +``` + +Follow [README](README.md#install) to install components. + +### Changing Types +If you're making a change to the `pkg/apis` package, please ensure you re-run the K8 code-generator scripts found in the `/hack` folder. First, ensure you have the `generate-groups.sh` script at the path: `vendor/k8s.io/code-generator/`. Next run the following commands in order: +``` +$ make codegen +``` + + +## How to write a custom gateway? +To implement a custom gateway, you need to create a gRPC server and implement the service defined below. +The framework code acts as a gRPC client consuming event stream from gateway server. + +
+
+ +

+ Sensor +

+ +
+ +### Proto Definition +1. The proto file is located [here](https://github.com/argoproj/argo-events/blob/master/gateways/eventing.proto) + +2. If you choose to implement the gateway in `Go`, then you can find generated client stubs [here](https://github.com/argoproj/argo-events/blob/master/gateways/eventing.pb.go) + +3. To create stubs in other languages, head over to [gRPC website](https://grpc.io/) + +4. Service, + + /** + * Service for handling event sources. + */ + service Eventing { + // StartEventSource starts an event source and returns stream of events. + rpc StartEventSource(EventSource) returns (stream Event); + // ValidateEventSource validates an event source. + rpc ValidateEventSource(EventSource) returns (ValidEventSource); + } + + +### Available Environment Variables to Server + + | Field | Description | + | ------------------------------- | ------------------------------------------------ | + | GATEWAY_NAMESPACE | K8s namespace of the gateway | + | GATEWAY_EVENT_SOURCE_CONFIG_MAP | K8s configmap containing event source | + | GATEWAY_NAME | name of the gateway | + | GATEWAY_CONTROLLER_INSTANCE_ID | gateway controller instance id | + | GATEWAY_CONTROLLER_NAME | gateway controller name | + | GATEWAY_SERVER_PORT | Port on which the gateway gRPC server should run | + +### Implementation + You can follow existing implementations [here](https://github.com/argoproj/argo-events/tree/master/gateways/core) diff --git a/docs/gateway.md b/docs/gateway.md deleted file mode 100644 index a711d9baa2..0000000000 --- a/docs/gateway.md +++ /dev/null @@ -1,179 +0,0 @@ -# Gateway - -## What is a gateway? -A gateway consumes events from event sources, transforms them into the [cloudevents specification](https://github.com/cloudevents/spec) compliant events and dispatches them to sensors. - -
- -

- Gateway -

- -
- -## Components -A gateway has two components: - - 1. gateway-client: It creates one or more gRPC clients depending on event sources configurations, consumes events from server, transforms these events into cloudevents and dispatches them to sensors. - - 2. gateway-server: It is a gRPC server that consumes events from event sources and streams them to gateway client. - -## Core gateways - - 1. **Calendar**: - Events produced are based on either a [cron](https://crontab.guru/) schedule or an [interval duration](https://golang.org/pkg/time/#ParseDuration). In addition, calendar gateway supports a `recurrence` field in which to specify special exclusion dates for which this gateway will not produce an event. - - 2. **Webhooks**: - Webhook gateway exposes REST API endpoints. The request received on these endpoints are treated as events. See Request Methods in RFC7231 to define the HTTP REST endpoint. - - 3. **Kubernetes Resources**: - Resource gateway supports watching Kubernetes resources. Users can specify `group`, `version`, `kind`, and filters including prefix of the object name, labels, annotations, and createdBy time. - - 4. **Artifacts**: - Artifact gateway supports S3 `bucket-notifications` via [Minio](https://docs.minio.io/docs/minio-bucket-notification-guide). Note that a supported notification target must be running, exposed. - - 5. **Streams**: - Stream gateways contain a generic specification for messages received on a queue and/or though messaging server. The following are the stream gateways offered out of box: - - 1. **NATS**: - [Nats](https://nats.io/) is an open-sourced, lightweight, secure, and scalable messaging system for cloud native applications and microservices architecture. It is currently a hosted CNCF Project. - - 2. **MQTT**: - [MMQP](http://mqtt.org/) is a M2M "Internet of Things" connectivity protocol (ISO/IEC PRF 20922) designed to be extremely lightweight and ideal for mobile applications. Some broker implementations can be found [here](https://github.com/mqtt/mqtt.github.io/wiki/brokers). - - 3. **Kafka**: - [Apache Kafka](https://kafka.apache.org/) is a distributed streaming platform. We use Shopify's [sarama](https://github.com/Shopify/sarama) client for consuming Kafka messages. - - 4. **AMQP**: - [AMQP](https://www.amqp.org/) is a open standard messaging protocol (ISO/IEC 19464). There are a variety of broker implementations including, but not limited to the following: - - [Apache ActiveMQ](http://activemq.apache.org/) - - [Apache Qpid](https://qpid.apache.org/) - - [StormMQ](http://stormmq.com/) - - [RabbitMQ](https://www.rabbitmq.com/) - - You can find core gateways [here](https://github.com/argoproj/argo-events/tree/master/gateways/core) - -## Community gateways -You can find gateways built by the community [here](https://github.com/argoproj/argo-events/tree/master/gateways/community). New gateway contributions are always welcome. - -## Example - - apiVersion: argoproj.io/v1alpha1 - kind: Gateway - metadata: - name: webhook-gateway - labels: - # gateway controller with instanceId "argo-events" will process this gateway - gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 - spec: - type: "webhook" - eventSource: "webhook-event-source" - processorPort: "9330" - eventProtocol: - type: "HTTP" - http: - port: "9300" - template: - metadata: - name: "webhook-gateway-http" - labels: - gateway-name: "webhook-gateway" - spec: - containers: - - name: "gateway-client" - image: "argoproj/gateway-client" - imagePullPolicy: "Always" - command: ["/bin/gateway-client"] - - name: "webhook-events" - image: "argoproj/webhook-gateway" - imagePullPolicy: "Always" - command: ["/bin/webhook-gateway"] - serviceAccountName: "argo-events-sa" - service: - metadata: - name: webhook-gateway-svc - spec: - selector: - gateway-name: "webhook-gateway" - ports: - - port: 12000 - targetPort: 12000 - type: LoadBalancer - watchers: - sensors: - - name: "webhook-sensor" - - -The gateway `spec` has following fields: - -1. `type`: Type of the gateway. This is defined by the user. - -2. `eventSource`: Refers to K8s configmap that holds the list of event sources. You can use `namespace/configmap-name` syntax to refer the configmap in a different namespace. - -3. `processorPort`: This is a gateway server port. You can leave this to `9330` unless you really have to change it to a different port. - -4. `eventProtocol`: Communication protocol between sensor and gateway. For more information, head over to [communication](./communication.md) - -5. `template`: Defines the specification for gateway pod. - -6. `service`: Specification of a K8s service to expose the gateway pod. - -7. `watchers`: List of sensors to which events must be dispatched. - -## Managing Event Sources - * The event sources configurations are managed using K8s configmap. Once the gateway resource is created with the configmap reference in it's spec, it starts watching the configmap. - The `gateway-client` sends each event source configuration to `gateway-server` over gRPC. The `gateway-server` then parses the configuration to start consuming events from - external event producing entity. - - * You can modify K8s configmap containing event sources configurations anytime and `gateway-client` will intelligently pick new/deleted configurations and send them over to `gateway-server` to either - start or stop the event sources. - -## How to write a custom gateway? -To implement a custom gateway, you need to create a gRPC server and implement the service defined below. -The framework code acts as a gRPC client consuming event stream from gateway server. - -
-
- -

- Sensor -

- -
- -### Proto Definition -1. The proto file is located [here](https://github.com/argoproj/argo-events/blob/master/gateways/eventing.proto) - -2. If you choose to implement the gateway in `Go`, then you can find generated client stubs [here](https://github.com/argoproj/argo-events/blob/master/gateways/eventing.pb.go) - -3. To create stubs in other languages, head over to [gRPC website](https://grpc.io/) - -4. Service, - - /** - * Service for handling event sources. - */ - service Eventing { - // StartEventSource starts an event source and returns stream of events. - rpc StartEventSource(EventSource) returns (stream Event); - // ValidateEventSource validates an event source. - rpc ValidateEventSource(EventSource) returns (ValidEventSource); - } - - -### Available Environment Variables to Server - - | Field | Description | - | ------------------------------- | ------------------------------------------------ | - | GATEWAY_NAMESPACE | K8s namespace of the gateway | - | GATEWAY_EVENT_SOURCE_CONFIG_MAP | K8s configmap containing event source | - | GATEWAY_NAME | name of the gateway | - | GATEWAY_CONTROLLER_INSTANCE_ID | gateway controller instance id | - | GATEWAY_CONTROLLER_NAME | gateway controller name | - | GATEWAY_SERVER_PORT | Port on which the gateway gRPC server should run | - -### Implementation - You can follow existing implementations [here](https://github.com/argoproj/argo-events/tree/master/gateways/core) diff --git a/docs/gateways/artifact.md b/docs/gateways/artifact.md deleted file mode 100644 index 127a410652..0000000000 --- a/docs/gateways/artifact.md +++ /dev/null @@ -1,32 +0,0 @@ -# Minio S3 - -The gateway listens to bucket notifications from Minio S3 server. If you are interested in AWS S3 then -read [AWS SNS Gateway](aws-sns.md) - -## Install Minio -If you dont have Minio installed already, follow this [link.](https://docs.min.io/docs/deploy-minio-on-kubernetes) - -## What types of bucket notifications minio offers? -Read about [notifications](https://docs.minio.io/docs/minio-bucket-notification-guide.html) - -## Event Payload Structure -Refer [AWS S3 Notitification](https://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html) - -## Setup - -1. Before you setup gateway and sensor, make sure you have necessary buckets created in Minio. - -2. Deploy [event source](https://github.com/argoproj/argo-events/blob/master/examples/event-sources/artifact.yaml) for the gateway. Change the -event source configmap according to your use case. - -3. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/artifact.yaml). Once the gateway pod spins up, check the logs of both `gateway-client` - and `artifact-gateway` containers and make sure no error occurs. - -4. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/artifact.yaml). Once the sensor pod spins up, make sure there -are no errors in sensor pod. - -Drop a file onto `input` bucket and monitor workflows - -## How to add new event source for a different bucket? -Simply edit the event source configmap and add new entry that contains the configuration required to listen to new bucket, save -the configmap. The gateway will now start listening to both old and new buckets. diff --git a/docs/gateways/aws-sns.md b/docs/gateways/aws-sns.md deleted file mode 100644 index 70f3384555..0000000000 --- a/docs/gateways/aws-sns.md +++ /dev/null @@ -1,26 +0,0 @@ -# AWS SNS - -The gateway listens to notifications from AWS SNS. - -## Why is there webhook in the gateway? -Because one of the ways you can receive notifications from SNS is over http. So, the gateway runs a http server internally. -Once you create an entry in the event source configmap, the gateway will register the url of the server on AWS. -All notifications for that topic will then be dispatched by SNS over to the endpoint specified in event source. - -The gateway spec defined in `examples` has a `serviceSpec`. This service is used to expose the gateway server to the outside world. - -## How to get the URL for the service? -Depending upon the Kubernetes provider, you can create the Ingress or Route. - -## Setup - -1. Deploy [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/aws-sns.yaml) before creating event sources because you need to have the gateway pod running and a service backed by the pod, so that you can get the URL for the service. - -2. Create the [event source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/aws-sns.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/aws-sns.yaml). - -## Trigger Workflow - -As soon as a message is published on your SNS topic, a workflow will be triggered. - \ No newline at end of file diff --git a/docs/gateways/aws-sqs.md b/docs/gateways/aws-sqs.md deleted file mode 100644 index 7aa254d35a..0000000000 --- a/docs/gateways/aws-sqs.md +++ /dev/null @@ -1,25 +0,0 @@ -# AWS SQS - -The gateway consumes messages from AWS SQS queue. - -## Setup - -1. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/aws-sqs.yaml) - -2. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/aws-sqs.yaml). Because SQS works on polling, you need to provide a `waitTimeSeconds`. - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/aws-sqs.yaml). - -## Trigger Workflow -As soon as there a message is consumed from SQS queue, a workflow will be triggered. - -## How to parse JSON payload -As you know, the SQS message may be plan text or JSON. In case that you will send a JSON structure, you can define the `path` field. -For example, the SQS message is `{"foo":"bar"}` and on the resourceParameters section will be defined like this: -```yaml -resourceParameters: - - src: - event: "aws-sqs-gateway:notification-1" - path: "foo" - dest: spec.arguments.parameters.0.value -``` diff --git a/docs/gateways/calendar.md b/docs/gateways/calendar.md deleted file mode 100644 index e98696cf0e..0000000000 --- a/docs/gateways/calendar.md +++ /dev/null @@ -1,11 +0,0 @@ -# Calendar - -The gateway helps schedule K8s resources on an interval or on a cron schedule. It is solution for triggering any standard or custom K8s -resource instead of using CronJob. - -## Setup -1. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/calendar.yaml). - -2. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/calendar.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/calendar.yaml). diff --git a/docs/gateways/file.md b/docs/gateways/file.md deleted file mode 100644 index cd4c4745e3..0000000000 --- a/docs/gateways/file.md +++ /dev/null @@ -1,22 +0,0 @@ -# File - -The gateway watches changes to a file within specified directory. - -## Where the directory should be? -The directory can be in the pod's own filesystem or you can mount a persistent volume and refer to a directory. -Make sure that the directory exists before you create the gateway configmap. - -## Setup -1. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/file.yaml). - -2. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/file.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/file.yaml). - -## Trigger Workflow - -Exec into the gateway pod and go to the directory specified in event source and create a file. That should generate an event causing sensor to trigger a workflow. - -## How to listen to notifications from different directories -Simply edit the event source configmap and add new entry that contains the configuration required to listen to file within different directory and save -the configmap. The gateway will start listening to file notifications from new directory as well. diff --git a/docs/gateways/gcp-pubsub.md b/docs/gateways/gcp-pubsub.md deleted file mode 100644 index d4e1561c78..0000000000 --- a/docs/gateways/gcp-pubsub.md +++ /dev/null @@ -1,15 +0,0 @@ -# GCP PubSub - -The gateway listens to event streams from google cloud pub sub topics. - -Make sure to mount credentials file for authentication in gateway pod and refer the path in `credentialsFile`. - -## Setup -1. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/gcp-pubsub.yaml). - -2. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/gcp-pubsub.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/gcp-pubsub.yaml). - -## Trigger Workflow -As soon as there a message is consumed from PubSub topic, a workflow will be triggered. diff --git a/docs/gateways/github.md b/docs/gateways/github.md deleted file mode 100644 index acaee48b94..0000000000 --- a/docs/gateways/github.md +++ /dev/null @@ -1,24 +0,0 @@ -# Github - -The gateway listens to events from GitHub. - -## Events types and webhook -Refer [here](https://developer.github.com/v3/activity/events/types/) for more information on type of events. - -Refer [here](https://developer.github.com/v3/repos/hooks/#get-single-hook) to understand the structure of webhook. - -The gateway spec defined in `examples` has a `serviceSpec`. This service is used to expose the gateway server and make it reachable from GitHub. -The event payload dispatched from gateway contains the type of the event in the headers. - -## How to get the URL for the service? -Depending upon the Kubernetes provider, you can create the Ingress or Route. - -## Setup -1. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/github.yaml) before creating the event source configmap, because you need to have the gateway pod running and a service backed by the pod, so that you can get the URL for the service. - -2. Create the [event source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/github.yaml). - -3. Deploy the [Sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/github.yaml). - -## Trigger Workflow -Depending upon the event you subscribe to, a workflow will be triggered. diff --git a/docs/gateways/gitlab.md b/docs/gateways/gitlab.md deleted file mode 100644 index f5d4c7c86c..0000000000 --- a/docs/gateways/gitlab.md +++ /dev/null @@ -1,21 +0,0 @@ -# Gitlab - -The gateway listens to events from Gitlab. - -The gateway spec defined in `examples` has a `serviceSpec`. This service is used to expose the gateway server and make it reachable from Gitlab. - -## How to get the URL for the service? -Depending upon the Kubernetes provider, you can create the Ingress or Route. - -## Setup - -1. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/gitlab.yaml) before creating the event source configmap, -because you need to have the gateway pod running and a service backed by the pod, so that you can get the URL for the service. - -2. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/gitlab.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/gitlab.yaml). - -## Trigger Workflow. -Depending upon the event you subscribe to, a workflow will be triggered. - diff --git a/docs/gateways/resource.md b/docs/gateways/resource.md deleted file mode 100644 index 689a16364d..0000000000 --- a/docs/gateways/resource.md +++ /dev/null @@ -1,11 +0,0 @@ -# Resource - -Resource gateway listens to updates on **any** Kubernetes resource. - -## Setup -1. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/resource.yaml). - -2. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/resource.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/resource.yaml). - diff --git a/docs/gateways/slack.md b/docs/gateways/slack.md deleted file mode 100644 index 8ce63a25a6..0000000000 --- a/docs/gateways/slack.md +++ /dev/null @@ -1,15 +0,0 @@ -# Slack - -The gateway listens to events from Slack. -The gateway will not register the webhook endpoint on Slack. You need to manually do it. - -## Setup - -1. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/slack.yaml). - -2. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/slack.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/slack.yaml). - -## Trigger Workflow -A workflow will be triggered when slack sends an event. diff --git a/docs/gateways/storage-grid.md b/docs/gateways/storage-grid.md deleted file mode 100644 index 4089ac9f04..0000000000 --- a/docs/gateways/storage-grid.md +++ /dev/null @@ -1,46 +0,0 @@ -# StorageGrid - -The gateway listens to bucket notifications from storage grid. - -Note: The gateway does not register the webhook endpoint on storage grid. You need to do it manually. This is mainly because limitations of storage grid api. -The gateway spec defined in `examples` has a `serviceSpec`. This service is used to expose the gateway server and make it reachable from StorageGrid. - -## How to get the URL for the service? -Depending upon the Kubernetes provider, you can create the Ingress or Route. - -## Setup - -1. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/storage-grid.yaml). - -2. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/storage-grid.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/storage-grid.yaml). - -4. Configure notifications - - Go to your tenant page on StorageGRID - Create an endpoint with the following values, and click save - - Display Name: S3 Notifications - URI: - URN: urn:mytext:sns:us-east::my_topic - Access Key: - Secret Key: - Certificate Validation: - - Go to the bucket for which you want to configure notifications. - Enter the following XML string, and click save - - - - - Object-Event - urn:mytext:sns:us-east::my_topic - s3:ObjectCreated:* - s3:ObjectRemoved:* - - - - -## Trigger Workflow -Drop a file into the bucket for which you configured the notifications and watch Argo workflow being triggered. diff --git a/docs/gateways/streams.md b/docs/gateways/streams.md deleted file mode 100644 index ed818a525c..0000000000 --- a/docs/gateways/streams.md +++ /dev/null @@ -1,20 +0,0 @@ -# Streams - -A Stream Gateway basically listens to messages on a message queue. - -The configuration for an event source is somewhat similar between all stream gateways. We will go through setup of NATS gateway. - -## NATS - -NATS gateway consumes messages by subscribing to NATS topic. - -## Setup - -1. Create the [event Source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/nats.yaml). - -2. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/nats.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/nats.yaml). - -## Trigger Workflow -Publish message to subject `foo`. You might find [this](https://github.com/nats-io/go-nats/tree/master/examples/nats-pub) useful. diff --git a/docs/gateways/webhook.md b/docs/gateways/webhook.md deleted file mode 100644 index d3b1531877..0000000000 --- a/docs/gateways/webhook.md +++ /dev/null @@ -1,35 +0,0 @@ -# Webhook - -The gateway runs one or more http servers in a pod. - -## Endpoints -Endpoints are activate or deactivated at the runtime. -The gateway pod continuously monitors the event source configmap. If you add a new endpoint entry in the configmap, the server will register it as -an active endpoint and if you remove an endpoint entry, server will mark that endpoint as inactive. - -## Why is there a service spec in gateway spec? -Because you'd probably want to expose the gateway to the outside world as gateway pod is running http servers. -If you don't to expose the gateway, just remove the `serviceSpec` from the gateway spec. - -## Setup - -1. Create the [event source](https://github.com/argoproj/argo-events/tree/master/examples/event-sources/webhook.yaml). - -2. Deploy the [gateway](https://github.com/argoproj/argo-events/tree/master/examples/gateways/webhook.yaml). - -3. Deploy the [sensor](https://github.com/argoproj/argo-events/tree/master/examples/sensors/webhook.yaml). - -## Trigger Workflow - -Note: the `WEBHOOK_SERVICE_URL` will differ based on the Kubernetes cluster. - - export WEBHOOK_SERVICE_URL=$(minikube service -n argo-events --url ) - echo $WEBHOOK_SERVICE_URL - curl -d '{"message":"this is my first webhook"}' -H "Content-Type: application/json" -X POST $WEBHOOK_SERVICE_URL/foo - - -Note: - -1. If you are facing an issue getting service url by running `minikube service -n argo-events --url `, you can use `kubectl port-forward` -2. Open another terminal window and enter `kubectl port-forward -n argo-events 9003:` -3. You can now use `localhost:9003` to query webhook gateway diff --git a/docs/getting_started.md b/docs/getting_started.md index e1943fa86f..34e9f791fb 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -1,28 +1,33 @@ # Getting Started -Lets deploy a webhook gateway and sensor, +We are going to set up a gateway, sensor and event-source for webhook. The goal is +to trigger an Argo workflow upon a HTTP Post request. - * First, we need to setup event sources for gateway to listen. The event sources for any gateway are managed using K8s configmap. + * First, we need to setup event sources for gateway to listen. - kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/master/examples/event-sources/webhook.yaml + kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/master/examples/event-sources/webhook.yaml + + The event-source drives the configuration required for a gateway to consume events from external sources. * Create webhook gateway, kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/master/examples/gateways/webhook.yaml - After running above command, gateway controller will create corresponding gateway pod and a LoadBalancing service. + After running above command, gateway controller will create corresponding a pod and service. * Create webhook sensor, kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/master/examples/sensors/webhook.yaml + + Once sensor object is created, sensor controller will create corresponding pod and service. - Once sensor resource is created, sensor controller will create corresponding sensor pod and a ClusterIP service. - - * Once the gateway and sensor pods are running, trigger the webhook via a http POST request to `/example` endpoint. + * Once the gateway and sensor pods are running, dispatch a HTTP POST request to `/example` endpoint. Note: the `WEBHOOK_SERVICE_URL` will differ based on the Kubernetes cluster. export WEBHOOK_SERVICE_URL=$(minikube service -n argo-events --url webhook-gateway-svc) + echo $WEBHOOK_SERVICE_URL + curl -d '{"message":"this is my first webhook"}' -H "Content-Type: application/json" -X POST $WEBHOOK_SERVICE_URL/example Note: @@ -30,16 +35,16 @@ Lets deploy a webhook gateway and sensor, minikube service -n argo-events --url webhook-gateway-svc - You can use port forwarding to access the service + You can use port forwarding to access the service as well, kubectl port-forward Open another terminal window and enter - kubectl port-forward -n argo-events 9003:9330 + kubectl port-forward -n argo-events 12000:12000 - You can now use `localhost:9003` to query webhook gateway + You can now use `localhost:12000` to query webhook gateway - Verify that the Argo workflow was run when the trigger was executed. + Verify that an Argo workflow was triggered. - argo list -n argo-events + kubectl -n argo-events get workflows | grep "webhook" diff --git a/docs/index.md b/docs/index.md index bf65620c79..eea4f2044a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,5 @@ # Argo Events -

- Logo -

- ## What is Argo Events? **Argo Events** is an event-based dependency manager for Kubernetes which helps you define multiple dependencies from a variety of event sources like webhook, s3, schedules, streams etc. and trigger Kubernetes objects after successful event dependencies resolution. @@ -26,15 +22,21 @@ and trigger Kubernetes objects after successful event dependencies resolution. * CloudEvents compliant. * Ability to manage event sources at runtime. -## Core Concepts -The framework is made up of three components: - - 1. [**Gateway**](gateway.md) which is implemented as a Kubernetes-native Custom Resource Definition processes events from event source. - - 2. [**Sensor**](sensor.md) which is implemented as a Kubernetes-native Custom Resource Definition defines a set of event dependencies and triggers K8s resources. - - 3. **Event Source** is a configmap that contains configurations which is interpreted by gateway as source for events producing entity. - -## In Nutshell -Gateway monitors event sources and starts routines in parallel that consume events from entities like S3, Github, SNS, SQS, -PubSub etc. and dispatch these events to sensor. Sensor upon receiving the events, evaluates the dependencies and triggers Argo workflows or other K8s resources. +## Event Listeners +1. AMQP +2. AWS SNS +3. AWS SQS +4. Cron Schedules +5. GCP PubSub +6. GitHub +7. GitLab +8. HDFS +9. File Based Events +10. Kafka +11. Minio +12. NATS +13. MQTT +14. K8s Resources +15. Slack +16. NetApp StorageGrid +17. Webhooks diff --git a/docs/installation.md b/docs/installation.md index 9fd11c0daa..579c648c23 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,13 +1,12 @@ # Installation - ### Requirements * Kubernetes cluster >v1.9 * Installed the [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line tool >v1.9.0 ### Using Helm Chart -Note: as of today (5 Dec 2019) this method does not work with Helm 3, only Helm 2. +Note: This method does not work with Helm 3, only Helm 2. Make sure you have helm client installed and Tiller server is running. To install helm, follow
the link. @@ -18,10 +17,16 @@ Make sure you have helm client installed and Tiller server is running. To instal 2. Install `argo-events` chart helm install argo/argo-events - ### Using kubectl -* Deploy Argo Events SA, Roles, ConfigMap, Sensor Controller and Gateway Controller + +#### One Command Installation + +1. Deploy Argo Events SA, Roles, ConfigMap, Sensor Controller and Gateway Controller + + kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-events/master/hack/k8s/manifests/installation.yaml + +#### Step-by-Step Installation 1. Create the namespace @@ -58,3 +63,15 @@ Make sure you have helm client installed and Tiller server is running. To instal 9. Deploy the gateway controller kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/master/hack/k8s/manifests/gateway-controller-deployment.yaml + +## Deploy at cluster level +To deploy Argo-Events controllers at cluster level where the controllers will be +able to process gateway and sensor objects created in any namespace, + +1. Make sure to apply cluster role and binding to the service account, + + kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-events/master/hack/k8s/manifests/argo-events-cluster-roles.yaml + +2. Update the configmap for both gateway and sensor and remove the `namespace` key from it. + +3. Deploy both gateway and sensor controllers and watch the magic. diff --git a/docs/sensor.md b/docs/sensor.md deleted file mode 100644 index a06a20a51e..0000000000 --- a/docs/sensor.md +++ /dev/null @@ -1,453 +0,0 @@ -# Sensor -Sensors define a set of event dependencies (inputs) and triggers (outputs). -
- -

- Sensor -

- -
- -## What is an event dependency? -A dependency is an event the sensor is expecting to happen. It is defined as "gateway-name:event-source-name". -Also, you can use [globs](https://github.com/gobwas/glob#syntax) to catch a set of events (e.g. "gateway-name:*"). - -## What is a dependency group? -A dependency group is basically a group of event dependencies. - -## What is a circuit? -Circuit is any arbitrary boolean logic that can be applied on dependency groups. - -## What is a trigger? -Refer to [Triggers](trigger.md). - -## How does it work? - 1. Once a Sensor receives an event from a Gateway, either over HTTP or through NATS, it validates - the event against dependencies defined in the Sensor spec. If the Sensor expects the event then the - event is marked as valid and the dependency is marked as resolved. - - 2. If you haven't defined dependency groups, a Sensor waits for all dependencies to resolve and then - kicks off each of its triggers in sequence. If filters are defined, the Sensor applies the filters to the - incoming event. If the event passes the filters the Sensor's triggers are fired. - - 3. If you have defined dependency groups, a Sensor evaluates the group that the incoming event belongs to - and marks the group as resolved if all other event dependencies in the group have already been resolved. - - 4. Whenever a dependency group is resolved, the Sensor evaluates the `circuit` defined in spec. If - the `circuit` resolves to true, the triggers are fired. Sensors always wait for a `circuit` to resolve - to true before firing triggers. - - 5. You may not want to fire all of the triggers defined in your Sensor spec. The `when` switch can be - used to control when a certain trigger should be fired depending on which dependency group has been - resolved. - - 6. After a Sensor fires its triggers, it transitions into `complete` state, increments completion - counter and initializes it's state back to running, starting the process all over again. Any event that - is received while the Sensor is waiting to restart is stored in an internal queue. - - **Note**: If you don't provide dependency groups and `circuit`, sensor performs an `AND` operation on event dependencies. - -## Basic Example - -Lets look at a basic example, - - apiVersion: argoproj.io/v1alpha1 - kind: Sensor - metadata: - name: webhook-sensor - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 - spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: "webhook-gateway:example" - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - name: webhook-workflow-trigger - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: webhook- - spec: - entrypoint: whalesay - arguments: - parameters: - - name: message - # this is the value that should be overridden - value: hello world - templates: - - name: whalesay - inputs: - parameters: - - name: message - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["{{inputs.parameters.message}}"] - resourceParameters: - - src: - event: "webhook-gateway:example" - dest: spec.arguments.parameters.0.value - -i. The `spec.template.spec` defines the template for the sensor pod. - -ii. The `dependencies` define list of events the sensor is expected to receive, meaning this is an AND operation. - -iii. `eventProtocol` express the mode of communication to receive events -from gateways. - -iv. `triggers` define list of templates, each containing specification for a K8s resource and optional parameters. - -## Circuit - -Now, lets look at a more complex example involving a circuit, - - apiVersion: argoproj.io/v1alpha1 - kind: Sensor - metadata: - name: webhook-sensor-http - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 - spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: "webhook-gateway-http:endpoint1" - filters: - name: "context-filter" - context: - source: - host: xyz.com - contentType: application/json - - name: "webhook-gateway-http:endpoint2" - - name: "webhook-gateway-http:endpoint3" - - name: "webhook-gateway-http:endpoint4" - filters: - name: "data-filter" - data: - - path: bucket - type: string - value: - - "argo-workflow-input" - - "argo-workflow-input1" - - name: "webhook-gateway-http:endpoint5" - - name: "webhook-gateway-http:endpoint6" - - name: "webhook-gateway-http:endpoint7" - - name: "webhook-gateway-http:endpoint8" - - name: "webhook-gateway-http:endpoint9" - dependencyGroups: - - name: "group_1" - dependencies: - - "webhook-gateway-http:endpoint1" - - "webhook-gateway-http:endpoint2" - - name: "group_2" - dependencies: - - "webhook-gateway-http:endpoint3" - - name: "group_3" - dependencies: - - "webhook-gateway-http:endpoint4" - - "webhook-gateway-http:endpoint5" - - name: "group_4" - dependencies: - - "webhook-gateway-http:endpoint6" - - "webhook-gateway-http:endpoint7" - - "webhook-gateway-http:endpoint8" - - name: "group_5" - dependencies: - - "webhook-gateway-http:endpoint9" - circuit: "group_1 || group_2 || ((group_3 || group_4) && group_5)" - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - when: - any: - - "group_1" - - "group_2" - name: webhook-workflow-trigger - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-1- - spec: - entrypoint: whalesay - arguments: - parameters: - - name: message - # this is the value that should be overridden - value: hello world - templates: - - name: whalesay - inputs: - parameters: - - name: message - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["{{inputs.parameters.message}}"] - resourceParameters: - - src: - event: "webhook-gateway-http:endpoint1" - dest: spec.arguments.parameters.0.value - - template: - name: webhook-workflow-trigger-2 - when: - all: - - "group_5" - - "group_4" - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world-2- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" - - template: - name: webhook-workflow-trigger-common - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world-common- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" - -The sensor defines the list of dependencies with few containing filters. The filters are explained next. These dependencies are then grouped using `dependenciesGroups`. - -The significance of `dependenciesGroups` is, if you don't define it, the sensor will apply an `AND` operation and wait for all events to occur. But you may not always want to wait for all the specified events to occur, -but rather trigger the workflows as soon as a group or groups of event dependencies are satisfied. - -To define the logic of when to trigger the workflows, `circuit` contains a boolean expression that is evaluated every time a event dependency -is satisfied. Template can optionally contain `when` switch that determines when to trigger this template. - -In the example, the first template will get triggered when either `group_1` or `group_2` dependencies groups are satisfied, the second template will get triggered only when both -`group_4` and `group_5` are triggered and the last template will be triggered every time the circuit evaluates to true. - -## Execution and Backoff Policy - - apiVersion: argoproj.io/v1alpha1 - kind: Sensor - metadata: - name: trigger-backoff - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 - spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: "webhook-gateway-http:foo" - eventProtocol: - type: "HTTP" - http: - port: "9300" - # If set to true, marks sensor state as `error` if the previous trigger round fails. - # Once sensor state is set to `error`, no further triggers will be processed. - errorOnFailedRound: true - triggers: - - template: - name: trigger-1 - # Policy to configure backoff and execution criteria for the trigger - # Because the sensor is able to trigger any K8s resource, it determines the resource state by looking at the resource's labels. - policy: - # Backoff before checking the resource labels - backoff: - # Duration is the duration in nanoseconds - duration: 1000000000 # 1 second - # Duration is multiplied by factor each iteration - factor: 2 - # The amount of jitter applied each iteration - jitter: 0.1 - # Exit with error after this many steps - steps: 5 - # the criteria to decide if a resource is in success or failure state. - # labels set on the resource decide if resource is in success or failed state. - state: - # Note: Set either success or failure labels. If you set both, only success labels will be considered. - - # Success defines labels required to identify a resource in success state - success: - workflows.argoproj.io/phase: Succeeded - # Failure defines labels required to identify a resource in failed state - failure: - workflows.argoproj.io/phase: Failed - # Determines whether trigger should be marked as failed if the backoff times out and sensor is still unable to decide the state of the trigger. - # defaults to false - errorOnBackoffTimeout: true - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: webhook- - spec: - entrypoint: whalesay - arguments: - parameters: - - name: message - # this is the value that should be overridden - value: hello world - templates: - - name: whalesay - inputs: - parameters: - - name: message - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["{{inputs.parameters.message}}"] - resourceParameters: - - src: - event: "webhook-gateway-http:foo" - dest: spec.arguments.parameters.0.value - - template: - name: trigger-2 - policy: - backoff: - duration: 1000000000 # 1 second - factor: 2 - jitter: 0.1 - steps: 5 - state: - failure: - workflows.argoproj.io/phase: Failed - errorOnBackoffTimeout: false - group: argoproj.io - version: v1alpha1 - kind: Workflow - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["hello world"] - -A trigger template can contain execution and backoff policy. Once the trigger is executed by template, it's -state is determined using `state` labels. If labels defined in `success` criteria matches the subset of labels defined on the -resource, the execution is treated as successful and vice-versa for labels defined in `failure` criteria. Please note that you can -only define either success or failure criteria. - -The `backoff` directs the sensor on when to check the labels of the executed trigger resource. If after the backoff retries, the sensor is not able to determine the -state of the resource, `errorOnBackoffTimeout` controls whether to mark trigger as failure. - -The `errorOnFailedRound` defined outside of triggers decides whether to set the sensor state to `error` if the previous -round of triggers execution fails. - -## Filters -You can apply following filters on an event dependency. If the event payload passes the filter, then only it will -be treated as a valid event. - -| Type | Description | -|----------|-------------------| -| **Time** | Filters the signal based on time constraints | -| **EventContext** | Filters metadata that provides circumstantial information about the signal. | -| **Data** | Describes constraints and filters for payload | - -
- -### Time Filter - - filters: - time: - start: "2016-05-10T15:04:05Z07:00" - stop: "2020-01-02T15:04:05Z07:00" - -[Example](https://github.com/argoproj/argo-events/blob/master/examples/sensors/time-filter-webhook.yaml) - -### EventContext Filter - - filters: - context: - source: - host: amazon.com - contentType: application/json - -[Example](https://github.com/argoproj/argo-events/blob/master/examples/sensors/context-filter-webhook.yaml) - -### Data filter - - filters: - data: - - path: bucket - type: string - value: argo-workflow-input - -[Example](https://github.com/argoproj/argo-events/blob/master/examples/sensors/data-filter-webhook.yaml) - -## Examples -You can find sensor examples [here](https://github.com/argoproj/argo-events/tree/master/examples/sensors) diff --git a/examples/event-sources/amqp.yaml b/examples/event-sources/amqp.yaml index 9c4543e553..6840a16d87 100644 --- a/examples/event-sources/amqp.yaml +++ b/examples/event-sources/amqp.yaml @@ -1,36 +1,32 @@ -# This configmap contains the event sources configurations for AMQP gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: amqp-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - # no retries if connection to amqp service is not successful - example-without-retry: |- - # url of the service - url: amqp://amqp.argo-events:5672 - # name of the exchange - exchangeName: "name of the exchange" - # type of the exchange - exchangeType: fanout - # routing key for the exchange - routingKey: "routing key" +spec: + type: amqp + amqp: + # no retries if connection to amqp service is not successful + example-without-retry: + # url of the service + url: "amqp://amqp.argo-events:5672" + # name of the exchange + exchangeName: "name of the exchange" + # type of the exchange + exchangeType: "fanout" + # routing key for the exchange + routingKey: "routing key" - # retry after each backoff to set up a successful connection - example-with-retry: |- - url: amqp://amqp.argo-events:5672 - exchangeName: "name of the exchange" - exchangeType: fanout - routingKey: "routing key" - backoff: - # duration in nanoseconds. following value is 10 seconds - duration: 10000000000 - # how many backoffs - steps: 5 - # factor to increase on each step. - # setting factor > 1 makes backoff exponential. - factor: 2 + # retry after each backoff to set up a successful connection + example-with-retry: + url: "amqp://amqp.argo-events:5672" + exchangeName: "name of the exchange" + exchangeType: "fanout" + routingKey: "routing key" + backoff: + # duration in nanoseconds. following value is 10 seconds + duration: 10000000000 + # how many backoffs + steps: 5 + # factor to increase on each step. + # setting factor > 1 makes backoff exponential. + factor: 2 diff --git a/examples/event-sources/artifact.yaml b/examples/event-sources/artifact.yaml deleted file mode 100644 index 32c214d8b9..0000000000 --- a/examples/event-sources/artifact.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# This configmap contains the event sources configurations for Artifact gateway - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: artifact-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-1: |- - # bucket information - bucket: - # name of the bucket - name: input - # s3 service endpoint - endpoint: minio-service.argo-events:9000 - # list of events to subscribe to - # Visit https://docs.minio.io/docs/minio-bucket-notification-guide.html - events: - - s3:ObjectCreated:Put - - s3:ObjectRemoved:Delete - # Filters to apply on the key - # Optional - # e.g. filter for key that starts with "hello-" and ends with ".txt" - filter: - prefix: "hello-" - suffix: ".txt" - # type of the connection - insecure: true - # accessKey refers to K8s secret that stores the access key - accessKey: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is access key - key: accesskey - # Name of the K8s secret that contains the access key - name: artifacts-minio - # secretKey contains information about K8s secret that stores the secret key - secretKey: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is secret key - key: secretkey - # Name of the K8s secret that contains the secret key - name: artifacts-minio - - example-2 : |- - bucket: - name: mybucket - endpoint: minio-service.argo-events:9000 - events: - - s3:ObjectCreated:Put - # no filter - filter: - prefix: "" - suffix: "" - insecure: true - accessKey: - key: accesskey - name: artifacts-minio - secretKey: - key: secretkey - name: artifacts-minio diff --git a/examples/event-sources/aws-sns.yaml b/examples/event-sources/aws-sns.yaml index b45cd2a8ce..95ebc63cdd 100644 --- a/examples/event-sources/aws-sns.yaml +++ b/examples/event-sources/aws-sns.yaml @@ -1,68 +1,64 @@ -# This configmap contains the event sources configurations for AWS SNS gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: aws-sns-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example: |- - # topic arn - topicArn: "topic-arn" - # hook contains configuration for the HTTP server running in the gateway. - # AWS will send events to following port and endpoint - hook: - # endpoint to listen events on - endpoint: "/" - # port to run HTTP server on - port: "12000" - # url the gateway will use to register at AWS. - # This url must be reachable from outside the cluster. - # The gateway pod is backed by the service defined in the gateway spec. So get the URL for that service AWS can reach to. - url: "http://myfakeurl.fake" - # accessKey contains information about K8s secret that stores the access key - accessKey: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is access key - key: accesskey - # Name of the K8s secret that contains the access key - name: aws-secret - # secretKey contains information about K8s secret that stores the secret key - secretKey: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is secret key - key: secretkey - # Name of the K8s secret that contains the secret key - name: aws-secret - # aws region - region: "us-east-1" +spec: + type: "sns" + sns: + example: + # topic arn + topicArn: "topic-arn" + # hook contains configuration for the HTTP server running in the gateway. + # AWS will send events to following port and endpoint + webhook: + # endpoint to listen events on + endpoint: "/" + # port to run HTTP server on + port: "12000" + # url the gateway will use to register at AWS. + # This url must be reachable from outside the cluster. + # The gateway pod is backed by the service defined in the gateway spec. So get the URL for that service AWS can reach to. + url: "http://myfakeurl.fake" + # accessKey contains information about K8s secret that stores the access key + accessKey: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is access key + key: accesskey + # Name of the K8s secret that contains the access key + name: aws-secret + # secretKey contains information about K8s secret that stores the secret key + secretKey: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is secret key + key: secretkey + # Name of the K8s secret that contains the secret key + name: aws-secret + # aws region + region: "us-east-1" - example-with-secure-connection: |- - topicArn: "topic-arn" - hook: - endpoint: "/" - # gateway can run multiple HTTP servers, just define a unique port. - port: "13000" - url: "http://mysecondfakeurl.fake" - # path to file that is mounted in gateway pod which contains certs - serverCertPath: "some path in pod" - # path to file that is mounted in gateway pod which contains private key - serverKeyPath: "some path in pod" - accessKey: - name: aws-secret - key: access - secretKey: - name: aws-secret - key: secret - region: "us-east-1" + example-with-secure-connection: + topicArn: "topic-arn" + webhook: + endpoint: "/" + # gateway can run multiple HTTP servers, just define a unique port. + port: "13000" + url: "http://mysecondfakeurl.fake" + # path to file that is mounted in gateway pod which contains certs + serverCertPath: "some path in pod" + # path to file that is mounted in gateway pod which contains private key + serverKeyPath: "some path in pod" + accessKey: + name: aws-secret + key: access + secretKey: + name: aws-secret + key: secret + region: "us-east-1" - example-without-credentials: |- - # If AWS access credentials are already present on the Pod's IAM role running the Gateway, - # the AWS session will utilize the existing config and hence we do not need to provide explicit credentials. - topicArn: "topic-arn" - hook: - endpoint: "/" - port: "13000" - url: "http://mysecondfakeurl.fake" - region: "us-east-1" + example-without-credentials: + # If AWS access credentials are already present on the Pod's IAM role running the Gateway, + # the AWS session will utilize the existing config and hence we do not need to provide explicit credentials. + topicArn: "topic-arn" + webhook: + endpoint: "/" + port: "13000" + url: "http://mysecondfakeurl.fake" + region: "us-east-1" diff --git a/examples/event-sources/aws-sqs.yaml b/examples/event-sources/aws-sqs.yaml index 72c2470d39..95f46c3631 100644 --- a/examples/event-sources/aws-sqs.yaml +++ b/examples/event-sources/aws-sqs.yaml @@ -1,47 +1,34 @@ -# This configmap contains the event sources configurations for AWS SQS gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: aws-sqs-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-1: |- - # accessKey contains information about K8s secret that stores the access key - accessKey: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is access key - key: accesskey - # Name of the K8s secret that contains the access key - name: aws-secret - # secretKey contains information about K8s secret that stores the secret key - secretKey: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is secret key - key: secretkey - # Name of the K8s secret that contains the secret key - name: aws-secret - # aws region - region: "us-east-1" - # name of the queue. The gateway resolves the url of the queue from the queue name. - queue: "my-fake-queue-1" - # The duration (in seconds) for which the call waits for a message to arrive in the queue before returning. - # MUST BE > 0 AND <= 20 - waitTimeSeconds: 20 - - example-2: |- - accessKey: - key: accesskey - name: aws-secret - secretKey: - key: secretkey - name: aws-secret - region: "us-east-1" - queue: "my-fake-queue-2" - waitTimeSeconds: 20 +spec: + type: "sqs" + spec: + example: + # accessKey contains information about K8s secret that stores the access key + accessKey: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is access key + key: accesskey + # Name of the K8s secret that contains the access key + name: aws-secret + # secretKey contains information about K8s secret that stores the secret key + secretKey: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is secret key + key: secretkey + # Name of the K8s secret that contains the secret key + name: aws-secret + # aws region + region: "us-east-1" + # name of the queue. The gateway resolves the url of the queue from the queue name. + queue: "my-fake-queue-1" + # The duration (in seconds) for which the call waits for a message to arrive in the queue before returning. + # MUST BE > 0 AND <= 20 + waitTimeSeconds: 20 - example-3: |- - region: "us-east-1" - queue: "my-fake-queue-2" - waitTimeSeconds: 20 + example-without-credentials: + # If AWS access credentials are already present on the Pod's IAM role running the Gateway, + # the AWS session will utilize the existing config and hence we do not need to provide explicit credentials. + region: "us-east-1" + queue: "my-fake-queue-2" + waitTimeSeconds: 20 diff --git a/examples/event-sources/calendar.yaml b/examples/event-sources/calendar.yaml index 3f819389e5..8d376526bf 100644 --- a/examples/event-sources/calendar.yaml +++ b/examples/event-sources/calendar.yaml @@ -1,44 +1,37 @@ -# This configmap contains the event sources configurations for Calendar gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: - name: calendar-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-with-interval: |- - # creates an event every 10 seconds - interval: 10s + name: calendar-source +spec: + type: "calendar" + calendar: + example-with-interval: + # creates an event every 10 seconds + interval: "10s" - example-with-schedule: |- - # schedules an event at 30 minutes past each hour - schedule: "30 * * * *" + example-with-schedule: + # schedules an event at 30 minutes past each hour + schedule: "30 * * * *" - schedule-with-static-user-payload: |- - schedule: "30 * * * *" - # userPayload is a static string that will be send to the the sensor with each event payload - # whatever you put here is blindly delivered to sensor. - userPayload: "{\"hello\": \"world\"}" + schedule-with-static-user-payload: + schedule: "30 * * * *" + # userPayload is a static string that will be send to the the sensor with each event payload + # whatever you put here is blindly delivered to sensor. + userPayload: "{\"hello\": \"world\"}" - schedule-in-specific-timezone: |- - # creates an event every 20 seconds - interval: 20s - # userPayload is a static string that will be send to the the sensor with each event payload - # whatever you put here is blindly delivered to sensor. - userPayload: "{\"hello\": \"world\"}" - # timezone - # more info: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - timezone: "America/New_York" + schedule-in-specific-timezone: + # creates an event every 20 seconds + interval: "20s" + # userPayload is a static string that will be send to the the sensor with each event payload + # whatever you put here is blindly delivered to sensor. + userPayload: "{\"hello\": \"world\"}" + # timezone + # more info: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: "America/New_York" - schedule-with-exclusion-dates: |- - schedule: "30 * * * *" - # more info https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html - # only exclusion dates are supported - # year, month and day are matched - recurrence: - - "EXDATE:20190102T150405Z" - - "EXDATE:20190602T160210Z" - timezone: "America/New_York" + schedule-with-exclusion-dates: + schedule: "30 * * * *" + # year, month and day are matched + exclusionDates: + - "EXDATE:20190102T150405Z" + - "EXDATE:20190602T160210Z" diff --git a/examples/event-sources/file.yaml b/examples/event-sources/file.yaml index 1f9d0bf2bd..fd3e486273 100644 --- a/examples/event-sources/file.yaml +++ b/examples/event-sources/file.yaml @@ -1,25 +1,23 @@ -# This configmap contains the event sources configurations for File gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: - name: file-configmap - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-with-path: |- - # directory to watch - directory: "/bin/" - # type of the event - # supported types are: CREATE, WRITE, REMOVE, RENAME, CHMOD - type: CREATE - # path to watch - path: x.txt + name: file-event-source +spec: + type: file + file: + example-with-path: + watchPathConfig: + # directory to watch + directory: "/bin/" + # path to watch + path: "x.txt" + # type of the event + # supported types are: CREATE, WRITE, REMOVE, RENAME, CHMOD + eventType: "CREATE" - example-with-path-regex: |- - directory: "/bin/" - type: CREATE - # the gateway will watch events for path that matches following regex - pathRegexp: "([a-z]+).txt" + example-with-path-regex: + watchPathConfig: + directory: "/bin/" + # the gateway will watch events for path that matches following regex + pathRegexp: "([a-z]+).txt" + eventType: "CREATE" diff --git a/examples/event-sources/gcp-pubsub.yaml b/examples/event-sources/gcp-pubsub.yaml index acdf5faf18..a0029d2e41 100644 --- a/examples/event-sources/gcp-pubsub.yaml +++ b/examples/event-sources/gcp-pubsub.yaml @@ -1,21 +1,17 @@ -# This configmap contains the event sources configurations for GCP PubSub gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: gcp-pubsub-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-1: |- - # id of your project - projectID: "my-fake-project-id" - # (optional) id of project for topic, same as projectID by default - # topicProjectID: "my-fake-topic-project-id" - # topic name - topic: "my-fake-topic" - # Refers to the credential file that is mounted in the gateway pod. - # ./validate.go is just a placeholder to make tests pass. Please place the path to actual credentials file :) - credentialsFile: "./validate.go" +spec: + type: "pubsub" + pubsub: + example-event-source: + # id of your project + projectID: "my-fake-project-id" + # (optional) id of project for topic, same as projectID by default + # topicProjectID: "my-fake-topic-project-id" + # topic name + topic: "my-fake-topic" + # Refers to the credential file that is mounted in the gateway pod. + # ./validate.go is just a placeholder to make tests pass. Please place the path to actual credentials file :) + credentialsFile: "./validate.go" diff --git a/examples/event-sources/github.yaml b/examples/event-sources/github.yaml index 9d6aa98409..8625718660 100644 --- a/examples/event-sources/github.yaml +++ b/examples/event-sources/github.yaml @@ -1,74 +1,70 @@ -# This configmap contains the event sources configurations for Github gateway -# More info: https://developer.github.com/v3/repos/hooks/#create-a-hook - ---- -apiVersion: v1 -kind: ConfigMap +# Info on GitHub Webhook: https://developer.github.com/v3/repos/hooks/#create-a-hook +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: github-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example: |- - # owner of the repo - owner: "argoproj" - # repository name - repository: "argo-events" - # Github will send events to following port and endpoint - hook: - # endpoint to listen to events on - endpoint: "/push" - # port to run internal HTTP server on - port: "12000" - # url the gateway will use to register at Github. - # This url must be reachable from outside the cluster. - # The gateway pod is backed by the service defined in the gateway spec. So get the URL for that service Github can reach to. - url: "http://myfakeurl.fake" - # type of events to listen to. - # following listens to everything, hence * - # You can find more info on https://developer.github.com/v3/activity/events/types/ - events: - - "*" - # apiToken refers to K8s secret that stores the github api token - apiToken: - # Name of the K8s secret that contains the access token - name: github-access - # Key within the K8s secret whose corresponding value (must be base64 encoded) is access token - key: token - # webHookSecret refers to K8s secret that stores the github hook secret - webHookSecret: - # Name of the K8s secret that contains the hook secret - name: github-access - # Key within the K8s secret whose corresponding value (must be base64 encoded) is hook secret - key: secret - # type of the connection between gateway and Github - insecure: false - # Determines if notifications are sent when the webhook is triggered - active: true - # The media type used to serialize the payloads - contentType: "json" +spec: + type: "github" + github: + example: + # owner of the repo + owner: "argoproj" + # repository name + repository: "argo-events" + # Github will send events to following port and endpoint + webhook: + # endpoint to listen to events on + endpoint: "/push" + # port to run internal HTTP server on + port: "12000" + # url the gateway will use to register at Github. + # This url must be reachable from outside the cluster. + # The gateway pod is backed by the service defined in the gateway spec. So get the URL for that service Github can reach to. + url: "http://myfakeurl.fake" + # type of events to listen to. + # following listens to everything, hence * + # You can find more info on https://developer.github.com/v3/activity/events/types/ + events: + - "*" + # apiToken refers to K8s secret that stores the github api token + apiToken: + # Name of the K8s secret that contains the access token + name: github-access + # Key within the K8s secret whose corresponding value (must be base64 encoded) is access token + key: token + # webHookSecret refers to K8s secret that stores the github hook secret + webHookSecret: + # Name of the K8s secret that contains the hook secret + name: github-access + # Key within the K8s secret whose corresponding value (must be base64 encoded) is hook secret + key: secret + # type of the connection between gateway and Github + insecure: false + # Determines if notifications are sent when the webhook is triggered + active: true + # The media type used to serialize the payloads + contentType: "json" - example-with-secure-connection: |- - owner: "argoproj" - repository: "argo" - hook: - endpoint: "/push" - port: "13000" - url: "http://myargofakeurl.fake" - # path to file that is mounted in gateway pod which contains certs - serverCertPath: "some path in pod" - # path to file that is mounted in gateway pod which contains private key - serverKeyPath: "some path in pod" - events: - - "push" - - "delete" - apiToken: - name: github-access - key: token - webHookSecret: - name: github-access - key: secret - insecure: true - active: true - contentType: "json" + example-with-secure-connection: + owner: "argoproj" + repository: "argo" + webhook: + endpoint: "/push" + port: "13000" + url: "http://myargofakeurl.fake" + # path to file that is mounted in gateway pod which contains certs + serverCertPath: "some path in pod" + # path to file that is mounted in gateway pod which contains private key + serverKeyPath: "some path in pod" + events: + - "push" + - "delete" + apiToken: + name: github-access + key: token + webHookSecret: + name: github-access + key: secret + insecure: true + active: true + contentType: "json" diff --git a/examples/event-sources/gitlab.yaml b/examples/event-sources/gitlab.yaml index 9cc3655c8d..ee124383a8 100644 --- a/examples/event-sources/gitlab.yaml +++ b/examples/event-sources/gitlab.yaml @@ -1,55 +1,51 @@ -# This configmap contains the event sources configurations for Gitlab gateway -# More info: https://docs.gitlab.com/ce/api/projects.html#add-project-hook - ---- -apiVersion: v1 -kind: ConfigMap +# More info on GitLab project hooks: https://docs.gitlab.com/ce/api/projects.html#add-project-hook +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: gitlab-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example: |- - # id of the project - projectId: "1" - # Github will send events to following port and endpoint - hook: - # endpoint to listen to events on - endpoint: "/push" - # port to run internal HTTP server on - port: "12000" - # url the gateway will use to register at Github. - # This url must be reachable from outside the cluster. - # The gateway pod is backed by the service defined in the gateway spec. So get the URL for that service Github can reach to. - url: "http://myfakeurl.fake" - # event to listen to - # Visit https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#events - event: "PushEvents" - # accessToken refers to K8s secret that stores the gitlab api token - accessToken: - # Key within the K8s secret whose corresponding value (must be base64 encoded) is access token - key: accesskey - # Name of the K8s secret that contains the access token - name: gitlab-access - # Do SSL verification when triggering the hook - enableSSLVerification: false - # Gitlab Base url - gitlabBaseUrl: "YOUR_GITLAB_URL" +spec: + type: "gitlab" + gitlab: + example: + # id of the project + projectId: "1" + # Github will send events to following port and endpoint + webhook: + # endpoint to listen to events on + endpoint: "/push" + # port to run internal HTTP server on + port: "12000" + # url the gateway will use to register at Github. + # This url must be reachable from outside the cluster. + # The gateway pod is backed by the service defined in the gateway spec. So get the URL for that service Github can reach to. + url: "http://myfakeurl.fake" + # event to listen to + # Visit https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#events + event: "PushEvents" + # accessToken refers to K8s secret that stores the gitlab api token + accessToken: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is access token + key: accesskey + # Name of the K8s secret that contains the access token + name: gitlab-access + # Do SSL verification when triggering the hook + enableSSLVerification: false + # Gitlab Base url + gitlabBaseUrl: "YOUR_GITLAB_URL" - example-secure: |- - projectId: "2" - hook: - endpoint: "/push" - port: "13000" - url: "http://mysecondfakeurl.fake" - # path to file that is mounted in gateway pod which contains certs - serverCertPath: "some path in pod" - # path to file that is mounted in gateway pod which contains private key - serverKeyPath: "some path in pod" - event: "PushEvents" - accessToken: - key: accesskey - name: gitlab-access - enableSSLVerification: true - gitlabBaseUrl: "YOUR_GITLAB_URL" + example-secure: + projectId: "2" + webhook: + endpoint: "/push" + port: "13000" + url: "http://mysecondfakeurl.fake" + # path to file that is mounted in gateway pod which contains certs + serverCertPath: "some path in pod" + # path to file that is mounted in gateway pod which contains private key + serverKeyPath: "some path in pod" + event: "PushEvents" + accessToken: + key: accesskey + name: gitlab-access + enableSSLVerification: true + gitlabBaseUrl: "YOUR_GITLAB_URL" diff --git a/examples/event-sources/hdfs.yaml b/examples/event-sources/hdfs.yaml index e170f8e4f8..fe0677d023 100644 --- a/examples/event-sources/hdfs.yaml +++ b/examples/event-sources/hdfs.yaml @@ -1,31 +1,27 @@ -# This configmap contains the event sources configurations for HDFS gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: - name: hdfs-gateway-configmap - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-1: |- - directory: "/tmp/" - type: "CREATE" - path: x.txt - addresses: - - my-hdfs-namenode-0.my-hdfs-namenode.default.svc.cluster.local:8020 - - my-hdfs-namenode-1.my-hdfs-namenode.default.svc.cluster.local:8020 - hdfsUser: root - # krbCCacheSecret: - # name: krb - # key: krb5cc_0 - # krbKeytabSecret: - # name: krb - # key: user1.keytab - # krbUsername: "user1" - # krbRealm: "MYCOMPANY.COM" - # krbConfigConfigMap: - # name: my-hdfs-krb5-config - # key: krb5.conf - # krbServicePrincipalName: hdfs/_HOST + name: hdfs-event-source +spec: + type: "hdfs" + hdfs: + example: + directory: "/tmp/" + type: "CREATE" + path: x.txt + addresses: + - my-hdfs-namenode-0.my-hdfs-namenode.default.svc.cluster.local:8020 + - my-hdfs-namenode-1.my-hdfs-namenode.default.svc.cluster.local:8020 + hdfsUser: root + # krbCCacheSecret: + # name: krb + # key: krb5cc_0 + # krbKeytabSecret: + # name: krb + # key: user1.keytab + # krbUsername: "user1" + # krbRealm: "MYCOMPANY.COM" + # krbConfigConfigMap: + # name: my-hdfs-krb5-config + # key: krb5.conf + # krbServicePrincipalName: hdfs/_HOST diff --git a/examples/event-sources/kafka.yaml b/examples/event-sources/kafka.yaml index c26cf181b6..b09443008d 100644 --- a/examples/event-sources/kafka.yaml +++ b/examples/event-sources/kafka.yaml @@ -1,33 +1,29 @@ -# This configmap contains the event sources configurations for Kafka gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: kafka-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - # no retries if connection to kafka service is not successful - example-without-retry: |- - # url of the service - url: kafka.argo-events:9092 - # name of the topic - topic: "topic-1" - # partition number - partition: "0" +spec: + type: "kafka" + kafka: + # no retries if connection to kafka service is not successful + example-without-retry: + # url of the service + url: "kafka.argo-events:9092" + # name of the topic + topic: "topic-1" + # partition number + partition: "0" - # retry after each backoff to set up a successful connection - example-with-retry: |- - url: kafka.argo-events:9092 - topic: "topic-2" - partition: "1" - backoff: - # duration in nanoseconds. following value is 10 seconds - duration: 10000000000 - # how many backoffs - steps: 5 - # factor to increase on each step. - # setting factor > 1 makes backoff exponential. - factor: 2 + # retry after each backoff to set up a successful connection + example-with-retry: + url: "kafka.argo-events:9092" + topic: "topic-2" + partition: "1" + backoff: + # duration in nanoseconds. following value is 10 seconds + duration: 10000000000 + # how many backoffs + steps: 5 + # factor to increase on each step. + # setting factor > 1 makes backoff exponential. + factor: 2 diff --git a/examples/event-sources/minio.yaml b/examples/event-sources/minio.yaml new file mode 100644 index 0000000000..0ee09be9b8 --- /dev/null +++ b/examples/event-sources/minio.yaml @@ -0,0 +1,57 @@ +apiVersion: argoproj.io/v1alpha1 +kind: EventSource +metadata: + name: minio-event-source +spec: + type: "minio" + minio: + example-with-filter: + # bucket information + bucket: + # name of the bucket + name: input + # s3 service endpoint + endpoint: minio-service.argo-events:9000 + # list of events to subscribe to + # Visit https://docs.minio.io/docs/minio-bucket-notification-guide.html + events: + - s3:ObjectCreated:Put + - s3:ObjectRemoved:Delete + # Filters to apply on the key + # Optional + # e.g. filter for key that starts with "hello-" and ends with ".txt" + filter: + prefix: "hello-" + suffix: ".txt" + # type of the connection + insecure: true + # accessKey refers to K8s secret that stores the access key + accessKey: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is access key + key: accesskey + # Name of the K8s secret that contains the access key + name: artifacts-minio + # secretKey contains information about K8s secret that stores the secret key + secretKey: + # Key within the K8s secret whose corresponding value (must be base64 encoded) is secret key + key: secretkey + # Name of the K8s secret that contains the secret key + name: artifacts-minio + + example-without-filter: + bucket: + name: mybucket + endpoint: minio-service.argo-events:9000 + events: + - s3:ObjectCreated:Put + # no filter + filter: + prefix: "" + suffix: "" + insecure: true + accessKey: + key: accesskey + name: artifacts-minio + secretKey: + key: secretkey + name: artifacts-minio diff --git a/examples/event-sources/mqtt.yaml b/examples/event-sources/mqtt.yaml index 2a6bd63f11..2767db0fef 100644 --- a/examples/event-sources/mqtt.yaml +++ b/examples/event-sources/mqtt.yaml @@ -1,34 +1,30 @@ -# This configmap contains the event sources configurations for MQTT gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: mqtt-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - # no retries if connection to mqtt service is not successful - example-without-retry: |- - # url of your mqtt service - url: tcp://mqtt.argo-events:1883 - # topic name - topic: foo - # client id - clientId: 1234 +spec: + type: "mqtt" + mqtt: + # no retries if connection to mqtt service is not successful + example-without-retry: + # url of your mqtt service + url: "tcp://mqtt.argo-events:1883" + # topic name + topic: "foo" + # client id + clientId: 1234 - # retry after each backoff to set up a successful connection - example-with-retry: |- - url: tcp://mqtt.argo-events:1883 - topic: bar - # client id - clientId: 2345 - backoff: - # duration in nanoseconds. following value is 10 seconds - duration: 10000000000 - # how many backoffs - steps: 5 - # factor to increase on each step. - # setting factor > 1 makes backoff exponential. - factor: 2 + # retry after each backoff to set up a successful connection + example-with-retry: + url: "tcp://mqtt.argo-events:1883" + topic: "bar" + # client id + clientId: 2345 + backoff: + # duration in nanoseconds. following value is 10 seconds + duration: 10000000000 + # how many backoffs + steps: 5 + # factor to increase on each step. + # setting factor > 1 makes backoff exponential. + factor: 2 diff --git a/examples/event-sources/nats.yaml b/examples/event-sources/nats.yaml index b869ec5e9a..2f0884cf16 100644 --- a/examples/event-sources/nats.yaml +++ b/examples/event-sources/nats.yaml @@ -1,32 +1,28 @@ -# This configmap contains the event sources configurations for NATS gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: nats-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - # no retries if connection to nats service is not successful - example-without-retry: |- - # url of the nats service - url: nats://nats.argo-events:4222 - # subject name - subject: foo +spec: + type: "nats" + nats: + # no retries if connection to nats service is not successful + example-without-retry: + # url of the nats service + url: "nats://nats.argo-events:4222" + # subject name + subject: "foo" - # retry after each backoff to set up a successful connection - example-with-retry: |- - # url of the nats service - url: nats://nats.argo-events:4222 - # subject name - subject: foo - backoff: - # duration in nanoseconds. following value is 10 seconds - duration: 10000000000 - # how many backoffs - steps: 5 - # factor to increase on each step. - # setting factor > 1 makes backoff exponential. - factor: 2 + # retry after each backoff to set up a successful connection + example-with-retry: + # url of the nats service + url: "nats://nats.argo-events:4222" + # subject name + subject: "foo" + backoff: + # duration in nanoseconds. following value is 10 seconds + duration: 10000000000 + # how many backoffs + steps: 5 + # factor to increase on each step. + # setting factor > 1 makes backoff exponential. + factor: 2 diff --git a/examples/event-sources/resource.yaml b/examples/event-sources/resource.yaml index 5cecf018ff..251df1d32a 100644 --- a/examples/event-sources/resource.yaml +++ b/examples/event-sources/resource.yaml @@ -1,81 +1,77 @@ -# This configmap contains the event sources configurations for Resource gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: resource-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - # watch workflows that are in successful state - example: |- - # namespace to listen events within - namespace: argo-events - # resource group - group: "argoproj.io" - # resource version - version: "v1alpha1" - # resource kind - resource: "workflows" - # type of event - # possible values are ADD, DELETE, UPDATE - # Optional - type: ADD - # Filters to apply on watched object - # Optional - filter: - labels: - workflows.argoproj.io/phase: Succeeded - name: "my-workflow" +spec: + type: "resource" + resource: + # watch workflows that are in successful state + example: + # namespace to listen events within + namespace: "argo-events" + # resource group + group: "argoproj.io" + # resource version + version: "v1alpha1" + # resource kind + resource: "workflows" + # type of event + # possible values are ADD, DELETE, UPDATE + # Optional + type: ADD + # Filters to apply on watched object + # Optional + filter: + labels: + workflows.argoproj.io/phase: Succeeded + name: "my-workflow" - # watch all namespace related events - example-with-all-types-and-no-filter: |- - namespace: argo-events - group: "" - version: "v1" - resource: "namespaces" + # watch all namespace related events + example-with-all-types-and-no-filter: + namespace: "argo-events" + group: "" + version: "v1" + resource: "namespaces" - # create event if workflow with prefix "my-workflow" gets modified - example-with-prefix-filter: |- - namespace: argo-events - group: argoproj.io - version: v1alpha1 - resource: workflows - type: MODIFIED - filter: - prefix: "my-workflow" + # create event if workflow with prefix "my-workflow" gets modified + example-with-prefix-filter: + namespace: "argo-events" + group: argoproj.io + version: v1alpha1 + resource: workflows + type: MODIFIED + filter: + prefix: "my-workflow" - # create event when a pod is created before 2019-03-27T010:52:32Z - example-with-created-by-filter: |- - namespace: argo-events - group: argoproj.io - version: v1alpha1 - resource: workflows - type: ADDED - filter: - createdBy: "2019-04-06T12:52:11Z" + # create event when a pod is created before 2019-03-27T010:52:32Z + example-with-created-by-filter: + namespace: "argo-events" + group: argoproj.io + version: v1alpha1 + resource: workflows + type: ADDED + filter: + createdBy: "2019-04-06T12:52:11Z" - example-with-multi-filters: |- - namespace: argo-events - group: "" - version: v1 - resource: pods - type: ADDED - filter: - createdBy: "2019-04-06T12:52:11Z" - labels: - workflows.argoproj.io/completed: "true" - prefix: "hello" + example-with-multi-filters: + namespace: "argo-events" + group: "" + version: v1 + resource: pods + type: ADDED + filter: + createdBy: "2019-04-06T12:52:11Z" + labels: + workflows.argoproj.io/completed: "true" + prefix: "hello" - # watch for completed workflows in any namespace - example-without-namespace: |- - # namespace: (omitted to match any namespace) - group: "k8s.io" - version: v1 - resource: workflows - type: ADDED - filter: - labels: - workflows.argoproj.io/completed: "true" + # watch for completed workflows in any namespace + example-without-namespace: + # namespace: (omitted to match any namespace) + group: "k8s.io" + version: v1 + resource: workflows + type: ADDED + filter: + labels: + workflows.argoproj.io/completed: "true" diff --git a/examples/event-sources/slack.yaml b/examples/event-sources/slack.yaml index f06a3d53cb..3b40719410 100644 --- a/examples/event-sources/slack.yaml +++ b/examples/event-sources/slack.yaml @@ -1,54 +1,48 @@ -# This configmap contains the event sources configurations for Slack gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: slack-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example-1: |- - # hook contains configuration for the HTTP server running in the gateway. - # Slack will send events to following port and endpoint - hook: - # endpoint to listen events on - endpoint: "/" - # port to run HTTP server on - port: "12000" - # token contains information about K8s secret that stores the token - token: - # Name of the K8s secret that contains the token - name: slack-secret - # Key within the K8s secret whose corresponding value (must be base64 encoded) is token - key: tokenkey - # signingSecret contains information about the K8s secret that stores - # Slack Signing Secret used to sign every request from Slack - signingSecret: - # Name of the K8s secret that contains the signingSecret - name: slack-secret - # Key within the K8s secret whose corresponding value contains the - # base64-encoded Slack signing secret - key: signingSecret - - example-2: |- - hook: - endpoint: "/" - port: "13000" - token: - name: slack-secret-2 - key: tokenkey +spec: + type: "slack" + slack: + example-insecure: + # hook contains configuration for the HTTP server running in the gateway. + # Slack will send events to following port and endpoint + webhook: + # endpoint to listen events on + endpoint: "/" + # port to run HTTP server on + port: "12000" + # token contains information about K8s secret that stores the token + token: + # Name of the K8s secret that contains the token + name: "slack-secret" + # Key within the K8s secret whose corresponding value (must be base64 encoded) is token + key: tokenkey + # signingSecret contains information about the K8s secret that stores + # Slack Signing Secret used to sign every request from Slack + signingSecret: + # Name of the K8s secret that contains the signingSecret + name: "slack-secret" + # Key within the K8s secret whose corresponding value contains the + # base64-encoded Slack signing secret + key: signingSecret - # with secure connection - example-3: |- - hook: - endpoint: "/" - port: "14000" - # path to file that is mounted in gateway pod which contains certs - serverCertPath: "some path in pod" - # path to file that is mounted in gateway pod which contains private key - serverKeyPath: "some path in pod" - token: - name: slack-secret-3 - key: tokenkey + # with secure connection + example-secure: + webhook: + endpoint: "/" + port: "14000" + # path to file that is mounted in gateway pod which contains certs + serverCertPath: "some path in pod" + # path to file that is mounted in gateway pod which contains private key + serverKeyPath: "some path in pod" + token: + name: "slack-secret" + key: tokenkey + signingSecret: + # Name of the K8s secret that contains the signingSecret + name: "slack-secret" + # Key within the K8s secret whose corresponding value contains the + # base64-encoded Slack signing secret + key: signingSecret diff --git a/examples/event-sources/storage-grid.yaml b/examples/event-sources/storage-grid.yaml index d21f41b35d..74a45694ba 100644 --- a/examples/event-sources/storage-grid.yaml +++ b/examples/event-sources/storage-grid.yaml @@ -1,38 +1,34 @@ -# This configmap contains the event sources configurations for StorageGrid gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: storage-grid-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - example: |- - # hook contains configuration for the HTTP server running in the gateway. - # StorageGrid will send events to following port and endpoint - hook: - # port to run HTTP server on - port: "8080" - # endpoint to listen events on - endpoint: "/" - # List of supported events can be derived from AWS S3 events https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types - # Remove S3 prefix from event type to make it a StorageGrid event. - events: - - "ObjectCreated:Put" +spec: + type: "storage-grid" + storageGrid: + example-insecure: + # hook contains configuration for the HTTP server running in the gateway. + # StorageGrid will send events to following port and endpoint + webhook: + # port to run HTTP server on + port: "8080" + # endpoint to listen events on + endpoint: "/" + # List of supported events can be derived from AWS S3 events https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types + # Remove S3 prefix from event type to make it a StorageGrid event. + events: + - "ObjectCreated:Put" - example-secure: |- - hook: - # port to run HTTP server on - port: "8090" - # endpoint to listen events on - endpoint: "/" - # path to file that is mounted in gateway pod which contains certs - serverCertPath: "some path in pod" - # path to file that is mounted in gateway pod which contains private key - serverKeyPath: "some path in pod" - # for events object PUT, POST, COPY and object removal - events: - - "ObjectCreated:*" - - "ObjectRemoved:Delete" + example-secure: + webhook: + # port to run HTTP server on + port: "8090" + # endpoint to listen events on + endpoint: "/" + # path to file that is mounted in gateway pod which contains certs + serverCertPath: "some path in pod" + # path to file that is mounted in gateway pod which contains private key + serverKeyPath: "some path in pod" + # for events object PUT, POST, COPY and object removal + events: + - "ObjectCreated:*" + - "ObjectRemoved:Delete" diff --git a/examples/event-sources/webhook.yaml b/examples/event-sources/webhook.yaml index 65bb52f771..955638c85e 100644 --- a/examples/event-sources/webhook.yaml +++ b/examples/event-sources/webhook.yaml @@ -1,41 +1,25 @@ -# This configmap contains the event sources configurations for Webhook gateway - ---- -apiVersion: v1 -kind: ConfigMap +apiVersion: argoproj.io/v1alpha1 +kind: EventSource metadata: name: webhook-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - # gateway can run multiple HTTP servers. Simply define a unique port to start a new HTTP server - - example: |- - # port to run HTTP server on - port: "12000" - # endpoint to listen to - endpoint: "/example" - # HTTP request method to allow. In this case, only POST requests are accepted - method: "POST" - - example-secure: |- - port: "13000" - endpoint: "/secure" - method: "POST" - # path to file that is mounted in gateway pod which contains certs - serverCertPath: "/bin/webhook-secure/crt" - # path to file that is mounted in gateway pod which contains private key - serverKeyPath: "/bin/webhook-secure/key" - - # example 3 and 4 shows how you can add multiple endpoints on same HTTP server - - example-3: |- - port: "14000" - endpoint: "/example3" - method: "PUT" +spec: + type: "webhook" + webhook: + # gateway can run multiple HTTP servers. Simply define a unique port to start a new HTTP server + example: + # port to run HTTP server on + port: "12000" + # endpoint to listen to + endpoint: "/example" + # HTTP request method to allow. In this case, only POST requests are accepted + method: "POST" - example-4: |- - port: "14000" - endpoint: "/example4" - method: "POST" +# Uncomment to use secure webhook +# example-secure: +# port: "13000" +# endpoint: "/secure" +# method: "POST" +# # path to file that is mounted in gateway pod which contains certs +# serverCertPath: "/bin/webhook-secure/crt" +# # path to file that is mounted in gateway pod which contains private key +# serverKeyPath: "/bin/webhook-secure/key" diff --git a/examples/gateways/amqp.yaml b/examples/gateways/amqp.yaml index 63148b17f4..e3bdf7b62b 100644 --- a/examples/gateways/amqp.yaml +++ b/examples/gateways/amqp.yaml @@ -5,14 +5,16 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 # type of the gateway type: "amqp" # event source configmap name - eventSource: "amqp-event-source" + eventSourceRef: + name: "amqp-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" + # port of the gateway server to send event source configuration to. # you can configure it to any open port processorPort: "9330" @@ -42,4 +44,4 @@ spec: # sensors to send events to watchers: sensors: - - name: "amqp-sensor" + - name: "amqp-sensor" diff --git a/examples/gateways/aws-sns.yaml b/examples/gateways/aws-sns.yaml index 851ffaac3a..98e08a8a33 100644 --- a/examples/gateways/aws-sns.yaml +++ b/examples/gateways/aws-sns.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: - type: "aws-sns" - eventSource: "aws-sns-event-source" + replica: 1 + type: "sns" + eventSourceRef: + name: "aws-sns-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/aws-sqs.yaml b/examples/gateways/aws-sqs.yaml index 6338e259a2..5c853dd9e1 100644 --- a/examples/gateways/aws-sqs.yaml +++ b/examples/gateways/aws-sqs.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: - type: "aws-sqs" - eventSource: "aws-sqs-event-source" + replica: 1 + type: "sqs" + eventSourceRef: + name: "aws-sqs-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/calendar.yaml b/examples/gateways/calendar.yaml index d9002d9b85..3ce80748c6 100644 --- a/examples/gateways/calendar.yaml +++ b/examples/gateways/calendar.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "calendar" - eventSource: "calendar-event-source" + eventSourceRef: + name: "calendar-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/file.yaml b/examples/gateways/file.yaml index 34cf1eec84..03b375f19d 100644 --- a/examples/gateways/file.yaml +++ b/examples/gateways/file.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "file" - eventSource: "file-event-source" + eventSourceRef: + name: "file-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/gcp-pubsub.yaml b/examples/gateways/gcp-pubsub.yaml index c6a6083438..510c856d70 100644 --- a/examples/gateways/gcp-pubsub.yaml +++ b/examples/gateways/gcp-pubsub.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "gcp-pubsub" - eventSource: "gcp-pubsub-event-source" + eventSourceRef: + name: "gcp-pubsub-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/github.yaml b/examples/gateways/github.yaml index bff7bc302e..a19e4b6145 100644 --- a/examples/gateways/github.yaml +++ b/examples/gateways/github.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "github" - eventSource: "github-event-source" + eventSourceRef: + name: "github-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/gitlab.yaml b/examples/gateways/gitlab.yaml index 13533d91e5..af06647fc2 100644 --- a/examples/gateways/gitlab.yaml +++ b/examples/gateways/gitlab.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "gitlab" - eventSource: "gitlab-event-source" + eventSourceRef: + name: "gitlab-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/hdfs.yaml b/examples/gateways/hdfs.yaml index 6a6f82c42b..610c896244 100644 --- a/examples/gateways/hdfs.yaml +++ b/examples/gateways/hdfs.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "hdfs" - eventSource: "hdfs-event-source" + eventSourceRef: + name: "hdfs-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" @@ -23,15 +24,15 @@ spec: gateway-name: "hdfs-gateway" spec: containers: - - name: "gateway-client" - image: "argoproj/gateway-client" - imagePullPolicy: "Always" - command: ["/bin/gateway-client"] - - name: "hdfs-events" - image: "argoproj/hdfs-gateway" - imagePullPolicy: "Always" - command: ["/bin/hdfs-gateway"] + - name: "gateway-client" + image: "argoproj/gateway-client" + imagePullPolicy: "Always" + command: ["/bin/gateway-client"] + - name: "hdfs-events" + image: "argoproj/hdfs-gateway" + imagePullPolicy: "Always" + command: ["/bin/hdfs-gateway"] serviceAccountName: "argo-events-sa" watchers: sensors: - - name: "hdfs-sensor" + - name: "hdfs-sensor" diff --git a/examples/gateways/kafka.yaml b/examples/gateways/kafka.yaml index a7b00894de..ba07d22475 100644 --- a/examples/gateways/kafka.yaml +++ b/examples/gateways/kafka.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "kafka" - eventSource: "kafka-event-source" + eventSourceRef: + name: "kafka-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/artifact-nats-standard.yaml b/examples/gateways/minio-nats-standard.yaml similarity index 59% rename from examples/gateways/artifact-nats-standard.yaml rename to examples/gateways/minio-nats-standard.yaml index df3a60f20b..2956141f8c 100644 --- a/examples/gateways/artifact-nats-standard.yaml +++ b/examples/gateways/minio-nats-standard.yaml @@ -1,16 +1,17 @@ apiVersion: argoproj.io/v1alpha1 kind: Gateway metadata: - name: artifact-gateway-nats-standard + name: minio-gateway-nats-standard labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: - type: "artifact" - eventSource: "artifact-event-source" + replica: 1 + type: "minio" + eventSourceRef: + name: "minio-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "NATS" @@ -19,17 +20,17 @@ spec: type: "Standard" template: metadata: - name: "artifact-gateway-nats-standard" + name: "minio-gateway-nats-standard" labels: - gateway-name: "artifact-gateway-nats-standard" + gateway-name: "minio-gateway-nats-standard" spec: containers: - name: "gateway-client" image: "argoproj/gateway-client" imagePullPolicy: "Always" command: ["/bin/gateway-client"] - - name: "artifact-events" - image: "argoproj/artifact-gateway" + - name: "minio-events" + image: "argoproj/minio-gateway" imagePullPolicy: "Always" - command: ["/bin/artifact-gateway"] + command: ["/bin/minio-gateway"] serviceAccountName: "argo-events-sa" diff --git a/examples/gateways/artifact-nats-streaming.yaml b/examples/gateways/minio-nats-streaming.yaml similarity index 61% rename from examples/gateways/artifact-nats-streaming.yaml rename to examples/gateways/minio-nats-streaming.yaml index 86107f4749..5e111319a8 100644 --- a/examples/gateways/artifact-nats-streaming.yaml +++ b/examples/gateways/minio-nats-streaming.yaml @@ -1,16 +1,17 @@ apiVersion: argoproj.io/v1alpha1 kind: Gateway metadata: - name: artifact-gateway-nats-streaming + name: minio-gateway-nats-streaming labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: - type: "artifact" - eventSource: "artifact-event-source" + replica: 1 + type: "minio" + eventSourceRef: + name: "minio-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "NATS" @@ -21,17 +22,17 @@ spec: type: "Streaming" template: metadata: - name: "artifact-gateway-nats-streaming" + name: "minio-gateway-nats-streaming" labels: - gateway-name: "artifact-gateway-nats-streaming" + gateway-name: "minio-gateway-nats-streaming" spec: containers: - name: "gateway-client" image: "argoproj/gateway-client" imagePullPolicy: "Always" command: ["/bin/gateway-client"] - - name: "artifact-events" - image: "argoproj/artifact-gateway" + - name: "minio-events" + image: "argoproj/minio-gateway" imagePullPolicy: "Always" - command: ["/bin/artifact-gateway"] + command: ["/bin/minio-gateway"] serviceAccountName: "argo-events-sa" diff --git a/examples/gateways/artifact.yaml b/examples/gateways/minio.yaml similarity index 59% rename from examples/gateways/artifact.yaml rename to examples/gateways/minio.yaml index e1804b3378..b8492768e3 100644 --- a/examples/gateways/artifact.yaml +++ b/examples/gateways/minio.yaml @@ -1,14 +1,14 @@ apiVersion: argoproj.io/v1alpha1 kind: Gateway metadata: - name: artifact-gateway + name: minio-gateway labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 + # type of the gateway + type: "minio" processorPort: "9330" eventProtocol: type: "HTTP" @@ -16,23 +16,25 @@ spec: port: "9300" template: metadata: - name: "artifact-gateway" + name: "minio-gateway" labels: - gateway-name: "artifact-gateway" + gateway-name: "minio-gateway" spec: containers: - name: "gateway-client" image: "argoproj/gateway-client" imagePullPolicy: "Always" command: ["/bin/gateway-client"] - - name: "artifact-events" - image: "argoproj/artifact-gateway" + - name: "minio-events" + image: "argoproj/minio-gateway" imagePullPolicy: "Always" - command: ["/bin/artifact-gateway"] + command: ["/bin/minio-gateway"] serviceAccountName: "argo-events-sa" - eventSource: "artifact-event-source" + eventSourceRef: + name: "minio-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" eventVersion: "1.0" - type: "artifact" watchers: sensors: - - name: "artifact-sensor" + - name: "minio-sensor" diff --git a/examples/gateways/mqtt.yaml b/examples/gateways/mqtt.yaml index 7a2606585e..31f36cc03a 100644 --- a/examples/gateways/mqtt.yaml +++ b/examples/gateways/mqtt.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "mqtt" - eventSource: "mqtt-event-source" + eventSourceRef: + name: "mqtt-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/multi-watchers.yaml b/examples/gateways/multi-watchers.yaml index c9041d5309..fdc5ef4a7f 100644 --- a/examples/gateways/multi-watchers.yaml +++ b/examples/gateways/multi-watchers.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "webhook" - eventSource: "webhook-event-source" + eventSourceRef: + name: "webhook-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" @@ -47,10 +48,10 @@ spec: # and user must provide port and endpoint on which event should be dispatched. # Adding gateways as watchers are particularly useful when you want to chain events. gateways: - - name: "webhook-gateway" - port: "9070" - endpoint: "/notifications" + - name: "webhook-gateway" + port: "9070" + endpoint: "/notifications" sensors: - - name: "webhook-sensor" - - name: "multi-signal-sensor" - - name: "webhook-time-filter-sensor" + - name: "webhook-sensor" + - name: "multi-signal-sensor" + - name: "webhook-time-filter-sensor" diff --git a/examples/gateways/nats.yaml b/examples/gateways/nats.yaml index 7dfff8682e..d740095ea3 100644 --- a/examples/gateways/nats.yaml +++ b/examples/gateways/nats.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "nats" - eventSource: "nats-event-source" + eventSourceRef: + name: "nats-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/resource.yaml b/examples/gateways/resource.yaml index 2ae45045d4..c6982da838 100644 --- a/examples/gateways/resource.yaml +++ b/examples/gateways/resource.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "resource" - eventSource: "resource-event-source" + eventSourceRef: + name: "resource-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/secure-webhook.yaml b/examples/gateways/secure-webhook.yaml index 1912efdaba..ba745fac99 100644 --- a/examples/gateways/secure-webhook.yaml +++ b/examples/gateways/secure-webhook.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "webhook" - eventSource: "webhook-event-source" + eventSourceRef: + name: "webhook-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/sensor-in-different-namespace.yaml b/examples/gateways/sensor-in-different-namespace.yaml index a1ee0f8fe9..5e39b7f4b8 100644 --- a/examples/gateways/sensor-in-different-namespace.yaml +++ b/examples/gateways/sensor-in-different-namespace.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "webhook" - eventSource: "webhook-event-source" + eventSourceRef: + name: "webhook-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/slack.yaml b/examples/gateways/slack.yaml index 5af65db03e..e7f045bf01 100644 --- a/examples/gateways/slack.yaml +++ b/examples/gateways/slack.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "slack" - eventSource: "slack-event-source" + eventSourceRef: + name: "slack-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" diff --git a/examples/gateways/storage-grid.yaml b/examples/gateways/storage-grid.yaml index f250c424a4..ac18c5a14d 100644 --- a/examples/gateways/storage-grid.yaml +++ b/examples/gateways/storage-grid.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "storage-grid" - eventSource: "storage-grid-event-source" + eventSourceRef: + name: "storage-grid-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" @@ -43,4 +44,4 @@ spec: type: LoadBalancer watchers: sensors: - - name: "storage-grid-watcher-sensor" + - name: "storage-grid-watcher-sensor" diff --git a/examples/gateways/webhook-nats-standard.yaml b/examples/gateways/webhook-nats-standard.yaml index b3d82a184b..d452eaae2a 100644 --- a/examples/gateways/webhook-nats-standard.yaml +++ b/examples/gateways/webhook-nats-standard.yaml @@ -5,11 +5,12 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: - eventSource: "webhook-event-source" + replica: 1 + eventSourceRef: + name: "webhook-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" type: "webhook" processorPort: "9330" eventProtocol: diff --git a/examples/gateways/webhook-nats-streaming.yaml b/examples/gateways/webhook-nats-streaming.yaml index e337492a70..26e251134c 100644 --- a/examples/gateways/webhook-nats-streaming.yaml +++ b/examples/gateways/webhook-nats-streaming.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "webhook" - eventSource: "webhook-event-source" + eventSourceRef: + name: "webhook-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "NATS" diff --git a/examples/gateways/webhook.yaml b/examples/gateways/webhook.yaml index 3452bfdfee..c7dc133d07 100644 --- a/examples/gateways/webhook.yaml +++ b/examples/gateways/webhook.yaml @@ -5,12 +5,13 @@ metadata: labels: # gateway controller with instanceId "argo-events" will process this gateway gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 spec: + replica: 1 type: "webhook" - eventSource: "webhook-event-source" + eventSourceRef: + name: "webhook-event-source" + # optional, if event source is deployed in a different namespace than the gateway + # namespace: "other-namespace" processorPort: "9330" eventProtocol: type: "HTTP" @@ -24,22 +25,22 @@ spec: spec: containers: - name: "gateway-client" - image: "argoproj/gateway-client" + image: "argoproj/gateway-client:v0.12-test" imagePullPolicy: "Always" command: ["/bin/gateway-client"] - name: "webhook-events" - image: "argoproj/webhook-gateway" + image: "argoproj/webhook-gateway:v0.12-test" imagePullPolicy: "Always" command: ["/bin/webhook-gateway"] -# To make webhook secure, mount the secret that contains certificate and private key in the container -# and refer that mountPath in the event source. -# volumeMounts: -# - mountPath: "/bin/webhook-secure" -# name: secure -# volumes: -# - name: secure -# secret: -# secretName: webhook-secure + # To make webhook secure, mount the secret that contains certificate and private key in the container + # and refer that mountPath in the event source. + # volumeMounts: + # - mountPath: "/bin/webhook-secure" + # name: secure + # volumes: + # - name: secure + # secret: + # secretName: webhook-secure serviceAccountName: "argo-events-sa" service: metadata: diff --git a/examples/sensors/amqp.yaml b/examples/sensors/amqp.yaml index 2463c0136e..f6eb7683ba 100644 --- a/examples/sensors/amqp.yaml +++ b/examples/sensors/amqp.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -38,18 +35,18 @@ spec: entrypoint: whalesay arguments: parameters: - - name: message - # this is the value that should be overridden - value: hello world - templates: - - name: whalesay - inputs: - parameters: - name: message - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["{{inputs.parameters.message}}"] + # this is the value that should be overridden + value: hello world + templates: + - name: whalesay + inputs: + parameters: + - name: message + container: + image: docker/whalesay:latest + command: [cowsay] + args: ["{{inputs.parameters.message}}"] resourceParameters: - src: event: "amqp-gateway:example-with-retry" diff --git a/examples/sensors/artifact-with-param-nats-standard.yaml b/examples/sensors/artifact-with-param-nats-standard.yaml deleted file mode 100644 index 91d95f0895..0000000000 --- a/examples/sensors/artifact-with-param-nats-standard.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: artifact-with-param-nats-standard-sensor - labels: - # sensor controller with instanceId "argo-events" will process this sensor - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 -spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - eventProtocol: - type: "NATS" - nats: - type: "Standard" - url: "nats://example-nats.argo-events:4222" - dependencies: - - name: "artifact-gateway-nats-standard:example-1" - triggers: - - template: - name: argo-workflow - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: artifact-workflow- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - command: - - cowsay - image: "docker/whalesay:latest" - # The container args from the workflow are overridden by the s3 notification key - resourceParameters: - - src: - event: "artifact-gateway-nats-standard:example-1" - path: s3.object.key - dest: spec.templates.0.container.args.0 diff --git a/examples/sensors/artifact-with-param-nats-streaming.yaml b/examples/sensors/artifact-with-param-nats-streaming.yaml deleted file mode 100644 index 58c0066a32..0000000000 --- a/examples/sensors/artifact-with-param-nats-streaming.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: artifact-with-param-nats-streaming-sensor - labels: - # sensor controller with instanceId "argo-events" will process this sensor - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 -spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - eventProtocol: - type: "NATS" - nats: - type: "Streaming" - url: "nats://example-nats.argo-events:4222" - clusterId: "example-stan" - clientId: "myclient1" - dependencies: - - name: "artifact-gateway-nats-streaming:example-1" - triggers: - - template: - name: argo-workflow - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: artifact-workflow- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - command: - - cowsay - image: "docker/whalesay:latest" - # The container args from the workflow are overridden by the s3 notification key - resourceParameters: - - src: - event: "artifact-gateway-nats-streaming:example-1" - path: s3.object.key - dest: spec.templates.0.container.args.0 diff --git a/examples/sensors/aws-sns.yaml b/examples/sensors/aws-sns.yaml index 66c4829297..24a9453169 100644 --- a/examples/sensors/aws-sns.yaml +++ b/examples/sensors/aws-sns.yaml @@ -5,10 +5,8 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: + version: "v0.11" template: spec: containers: @@ -29,7 +27,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/aws-sqs.yaml b/examples/sensors/aws-sqs.yaml index 95a6f5163e..51e8feae81 100644 --- a/examples/sensors/aws-sqs.yaml +++ b/examples/sensors/aws-sqs.yaml @@ -5,10 +5,8 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: + version: "v0.11" template: spec: containers: @@ -21,7 +19,7 @@ spec: http: port: "9300" dependencies: - - name: "aws-sqs-gateway:example-1" + - name: "aws-sqs-gateway:example" triggers: - template: name: sqs-workflow @@ -29,7 +27,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -52,5 +50,5 @@ spec: args: ["{{inputs.parameters.message}}"] resourceParameters: - src: - event: "aws-sqs-gateway:example-1" + event: "aws-sqs-gateway:example" dest: spec.arguments.parameters.0.value diff --git a/examples/sensors/calendar.yaml b/examples/sensors/calendar.yaml index 4d4d8cc54b..8743634b9b 100644 --- a/examples/sensors/calendar.yaml +++ b/examples/sensors/calendar.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/complete-trigger-parameterization.yaml b/examples/sensors/complete-trigger-parameterization.yaml index 2c7ad93b55..28c4eaf50e 100644 --- a/examples/sensors/complete-trigger-parameterization.yaml +++ b/examples/sensors/complete-trigger-parameterization.yaml @@ -21,9 +21,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: diff --git a/examples/sensors/context-filter-webhook.yaml b/examples/sensors/context-filter-webhook.yaml index 4fc6d05c8a..78e57f6f47 100644 --- a/examples/sensors/context-filter-webhook.yaml +++ b/examples/sensors/context-filter-webhook.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: diff --git a/examples/sensors/data-filter-webhook.yaml b/examples/sensors/data-filter-webhook.yaml index 41f56e58a5..6136382d08 100644 --- a/examples/sensors/data-filter-webhook.yaml +++ b/examples/sensors/data-filter-webhook.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -24,9 +21,7 @@ spec: - path: bucket type: string value: - # regular expression - - "^bucket-.*" - # normal value + - "argo-workflow-input" - "argo-workflow-input1" eventProtocol: type: "HTTP" @@ -39,7 +34,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/dependencies-circuit-complex.yaml b/examples/sensors/dependencies-circuit-complex.yaml deleted file mode 100644 index 78b683ef14..0000000000 --- a/examples/sensors/dependencies-circuit-complex.yaml +++ /dev/null @@ -1,148 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: webhook-sensor-http - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 -spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: "webhook-gateway-http:endpoint1" - filters: - name: "context-filter" - context: - source: - host: xyz.com - contentType: application/json - - name: "webhook-gateway-http:endpoint2" - - name: "webhook-gateway-http:endpoint3" - - name: "webhook-gateway-http:endpoint4" - filters: - name: "data-filter" - data: - - path: bucket - type: string - value: - - "argo-workflow-input" - - "argo-workflow-input1" - - name: "webhook-gateway-http:endpoint5" - - name: "webhook-gateway-http:endpoint6" - - name: "webhook-gateway-http:endpoint7" - - name: "webhook-gateway-http:endpoint8" - - name: "webhook-gateway-http:endpoint9" - dependencyGroups: - - name: "group_1" - dependencies: - - "webhook-gateway-http:endpoint1" - - "webhook-gateway-http:endpoint2" - - name: "group_2" - dependencies: - - "webhook-gateway-http:endpoint3" - - name: "group_3" - dependencies: - - "webhook-gateway-http:endpoint4" - - "webhook-gateway-http:endpoint5" - - name: "group_4" - dependencies: - - "webhook-gateway-http:endpoint6" - - "webhook-gateway-http:endpoint7" - - "webhook-gateway-http:endpoint8" - - name: "group_5" - dependencies: - - "webhook-gateway-http:endpoint9" - circuit: "group_1 || group_2 || ((group_3 || group_4) && group_5)" - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - when: - any: - - "group_1" - - "group_2" - name: webhook-workflow-trigger - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-1- - spec: - entrypoint: whalesay - arguments: - parameters: - - name: message - # this is the value that should be overridden - value: hello world - templates: - - name: whalesay - inputs: - parameters: - - name: message - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["{{inputs.parameters.message}}"] - resourceParameters: - - src: - event: "webhook-gateway-http:endpoint1" - dest: spec.arguments.parameters.0.value - - template: - name: webhook-workflow-trigger-2 - when: - all: - - "group_5" - - "group_4" - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world-2- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" - - template: - name: webhook-workflow-trigger-common - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world-common- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" diff --git a/examples/sensors/dependencies-circuit.yaml b/examples/sensors/dependencies-circuit.yaml index 8f51bc671f..3d96d3aa78 100644 --- a/examples/sensors/dependencies-circuit.yaml +++ b/examples/sensors/dependencies-circuit.yaml @@ -4,9 +4,6 @@ metadata: name: webhook-sensor-http-boolean-op labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -17,17 +14,17 @@ spec: serviceAccountName: argo-events-sa # defines list of all events sensor will accept dependencies: - - name: "webhook-gateway-http:foo" - - name: "webhook-gateway-http:index" + - name: "webhook-gateway:example" + - name: "webhook-gateway:example-secure" # divides event dependencies into groups dependencyGroups: - name: "group_1" dependencies: - - "webhook-gateway-http:foo" + - "webhook-gateway:example" - name: "group_2" dependencies: - - "webhook-gateway-http:index" - # either "webhook-gateway-http:foo" or "webhook-gateway-http:index" happens + - "webhook-gateway:example-secure" + # either "webhook-gateway:example" or "webhook-gateway-http:index" happens circuit: "group_1 || group_2" eventProtocol: type: "HTTP" @@ -43,7 +40,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -67,7 +64,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/file.yaml b/examples/sensors/file.yaml index 50c9fabe30..bd45fd7833 100644 --- a/examples/sensors/file.yaml +++ b/examples/sensors/file.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/gcp-pubsub.yaml b/examples/sensors/gcp-pubsub.yaml index 2c9f87d40a..bcaa4ac6e7 100644 --- a/examples/sensors/gcp-pubsub.yaml +++ b/examples/sensors/gcp-pubsub.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -21,7 +18,7 @@ spec: http: port: "9300" dependencies: - - name: "gcp-pubsub-gateway:example-1" + - name: "gcp-pubsub-gateway:example" triggers: - template: name: gcp-workflow @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -52,5 +49,5 @@ spec: args: ["{{inputs.parameters.message}}"] resourceParameters: - src: - event: "gcp-pubsub-gateway:example-1" + event: "gcp-pubsub-gateway:example" dest: spec.arguments.parameters.0.value diff --git a/examples/sensors/github.yaml b/examples/sensors/github.yaml index 1d1ee72191..5ae40f3af7 100644 --- a/examples/sensors/github.yaml +++ b/examples/sensors/github.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/gitlab.yaml b/examples/sensors/gitlab.yaml index 8cd3ea7ea9..0396bdec25 100644 --- a/examples/sensors/gitlab.yaml +++ b/examples/sensors/gitlab.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -49,7 +46,7 @@ spec: container: image: docker/whalesay:latest command: [cowsay] - args: ["{{inputs.parameters.message}}"] + args: ["{{inputs.parameters.message}}"] resourceParameters: - src: event: "gitlab-gateway:example" diff --git a/examples/sensors/hdfs.yaml b/examples/sensors/hdfs.yaml index 6fe21c3728..4b0622d44e 100644 --- a/examples/sensors/hdfs.yaml +++ b/examples/sensors/hdfs.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -17,7 +14,7 @@ spec: imagePullPolicy: Always serviceAccountName: argo-events-sa dependencies: - - name: "hdfs-gateway:example-1" + - name: "hdfs-gateway:example" eventProtocol: type: "HTTP" http: @@ -29,23 +26,23 @@ spec: version: v1alpha1 resource: workflows source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - args: - - "hello " - command: - - cowsay - image: "docker/whalesay:latest" + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: hello-world- + spec: + entrypoint: whalesay + templates: + - name: whalesay + container: + args: + - "hello " + command: + - cowsay + image: "docker/whalesay:latest" resourceParameters: - src: - event: "hdfs-gateway:example-1" + event: "hdfs-gateway:example" path: name dest: spec.templates.0.container.args.1 diff --git a/examples/sensors/kafka.yaml b/examples/sensors/kafka.yaml index 0022f60a23..382e92f697 100644 --- a/examples/sensors/kafka.yaml +++ b/examples/sensors/kafka.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/artifact.yaml b/examples/sensors/minio.yaml similarity index 77% rename from examples/sensors/artifact.yaml rename to examples/sensors/minio.yaml index e93e7166d8..ce11008a14 100644 --- a/examples/sensors/artifact.yaml +++ b/examples/sensors/minio.yaml @@ -1,13 +1,10 @@ apiVersion: argoproj.io/v1alpha1 kind: Sensor metadata: - name: artifact-sensor + name: minio-sensor labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -21,15 +18,15 @@ spec: http: port: "9300" dependencies: - - name: "artifact-gateway:example-1" + - name: "minio-gateway:example-with-filter" triggers: - template: - name: artifact-workflow-trigger + name: minio-workflow-trigger group: argoproj.io version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -43,10 +40,10 @@ spec: - cowsay image: "docker/whalesay:latest" args: - - THIS_WILL_BE_REPLACED + - THIS_WILL_BE_REPLACED # The container args from the workflow are overridden by the s3 notification key resourceParameters: - src: - event: "artifact-gateway:example-1" + event: "minio-gateway:example-with-filter" path: s3.object.key dest: spec.templates.0.container.args.0 diff --git a/examples/sensors/mqtt-sensor.yaml b/examples/sensors/mqtt-sensor.yaml index f69d70fbe7..a20cc44172 100644 --- a/examples/sensors/mqtt-sensor.yaml +++ b/examples/sensors/mqtt-sensor.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/multi-signal-sensor.yaml b/examples/sensors/multi-signal-sensor.yaml index e07dec477f..7273074461 100644 --- a/examples/sensors/multi-signal-sensor.yaml +++ b/examples/sensors/multi-signal-sensor.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -20,9 +17,9 @@ spec: type: "HTTP" http: port: "9300" - # wait for both "webhook-gateway-http:foo" and "calendar-gateway:interval" to happen + # wait for both "webhook-gateway:example" and "calendar-gateway:interval" to happen dependencies: - - name: "webhook-gateway-http:example" + - name: "webhook-gateway:example" - name: "calendar-gateway:example-with-interval" triggers: - template: @@ -31,7 +28,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -41,7 +38,7 @@ spec: arguments: parameters: - name: message1 - # this is the value that should be overridden by event payload from webhook-gateway-http:foo + # this is the value that should be overridden by event payload from webhook-gateway:example value: hello world - name: message2 # this is the value that should be overridden by event payload from calendar-gateway:interval diff --git a/examples/sensors/multi-trigger-sensor.yaml b/examples/sensors/multi-trigger-sensor.yaml index b27fd27034..4c93c6697e 100644 --- a/examples/sensors/multi-trigger-sensor.yaml +++ b/examples/sensors/multi-trigger-sensor.yaml @@ -4,9 +4,6 @@ metadata: name: nats-multi-trigger-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -46,19 +43,19 @@ spec: version: v1alpha1 resource: workflows source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world- - spec: - entrypoint: whalesay - templates: - - - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" - name: whalesay + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: hello-world- + spec: + entrypoint: whalesay + templates: + - + container: + args: + - "hello world" + command: + - cowsay + image: "docker/whalesay:latest" + name: whalesay diff --git a/examples/sensors/nats.yaml b/examples/sensors/nats.yaml index 6823258895..c5107b5954 100644 --- a/examples/sensors/nats.yaml +++ b/examples/sensors/nats.yaml @@ -6,9 +6,6 @@ metadata: name: nats-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -30,7 +27,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/resource.yaml b/examples/sensors/resource.yaml index 9f2a8d46e4..5e239f1499 100644 --- a/examples/sensors/resource.yaml +++ b/examples/sensors/resource.yaml @@ -4,9 +4,6 @@ metadata: name: resource-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -28,7 +25,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/slack.yaml b/examples/sensors/slack.yaml index d17dff1a3c..257b524d97 100644 --- a/examples/sensors/slack.yaml +++ b/examples/sensors/slack.yaml @@ -4,9 +4,6 @@ metadata: name: slack-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -20,7 +17,7 @@ spec: http: port: "9300" dependencies: - - name: "slack-gateway:example-1" + - name: "slack-gateway:example-insecure" triggers: - template: name: slack-workflow @@ -28,7 +25,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -51,5 +48,5 @@ spec: args: ["{{inputs.parameters.message}}"] resourceParameters: - src: - event: "slack-gateway:example-1" + event: "slack-gateway:example-insecure" dest: spec.arguments.parameters.0.value diff --git a/examples/sensors/storage-grid.yaml b/examples/sensors/storage-grid.yaml index 583064de68..5c92f03cf3 100644 --- a/examples/sensors/storage-grid.yaml +++ b/examples/sensors/storage-grid.yaml @@ -4,9 +4,6 @@ metadata: name: storage-grid-watcher-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -28,7 +25,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + source: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/time-filter-webhook.yaml b/examples/sensors/time-filter-webhook.yaml index 0549edeeb8..0b1aa63df7 100644 --- a/examples/sensors/time-filter-webhook.yaml +++ b/examples/sensors/time-filter-webhook.yaml @@ -4,9 +4,6 @@ metadata: name: webhook-time-filter-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -32,19 +29,19 @@ spec: version: v1alpha1 resource: workflows source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: time-filter-hello-world- - spec: - entrypoint: whalesay - templates: - - - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" - name: whalesay + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: time-filter-hello-world- + spec: + entrypoint: whalesay + templates: + - + container: + args: + - "hello world" + command: + - cowsay + image: "docker/whalesay:latest" + name: whalesay diff --git a/examples/sensors/trigger-gateway.yaml b/examples/sensors/trigger-gateway.yaml deleted file mode 100644 index 27385d9297..0000000000 --- a/examples/sensors/trigger-gateway.yaml +++ /dev/null @@ -1,96 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: trigger-gateway-sensor - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 -spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - eventProtocol: - type: "HTTP" - http: - port: "9300" - dependencies: - - name: "webhook-gateway:example" - # once sensor receives an event from webhook gateway, it will create an artifact gateway. - triggers: - - template: - name: artifact-event-source-trigger - group: "" - version: v1 - resource: configmaps - source: - inline: | - apiVersion: v1 - kind: Configmap - metadata: - name: artifact-event-source - labels: - argo-events-event-source-version: v0.11 - spec: - data: - example: |- - bucket: - name: input - endpoint: minio-service.argo-events:9000 - event: s3:ObjectCreated:Put - filter: - prefix: "" - suffix: "" - insecure: true - accessKey: - key: accesskey - name: artifacts-minio - secretKey: - key: secretkey - name: artifacts-minio - - template: - name: artifact-gateway-trigger - group: argoproj.io - version: v1alpha1 - resource: gateways - source: - inline: |- - apiVersion: argoproj.io/v1alpha1 - kind: Gateway - metadata: - name: artifact-gateway - labels: - gateways.argoproj.io/gateway-controller-instanceid: argo-events - argo-events-gateway-version: v0.11 - spec: - type: "artifact" - eventSource: "artifact-event-source" - processorPort: "9330" - eventProtocol: - type: "HTTP" - http: - port: "9300" - template: - metadata: - name: "artifact-gateway" - labels: - gateway-name: "artifact-gateway" - spec: - containers: - - name: "gateway-client" - image: "argoproj/gateway-client" - imagePullPolicy: "Always" - command: ["/bin/gateway-client"] - - name: "artifact-events" - image: "argoproj/artifact-gateway" - imagePullPolicy: "Always" - command: ["/bin/artifact-gateway"] - serviceAccountName: "argo-events-sa" - watchers: - sensors: - - name: "artifact-sensor" diff --git a/examples/sensors/trigger-resource.yaml b/examples/sensors/trigger-resource.yaml deleted file mode 100644 index c59990753e..0000000000 --- a/examples/sensors/trigger-resource.yaml +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: trigger-resource-sensor - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events -spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: "webhook-gateway-http:foo" - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - name: trigger1 - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - # resource is generic template for K8s resource - # This is similar to `inline` trigger but useful if you are using kustomize and want to parameterize the trigger - resource: - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: webhook- - spec: - entrypoint: whalesay - arguments: - parameters: - - name: message - # this is the value that should be overridden - value: hello world - templates: - - name: whalesay - inputs: - parameters: - - name: message - container: - image: docker/whalesay:latest - command: [cowsay] - args: ["{{inputs.parameters.message}}"] - resourceParameters: - - src: - event: "webhook-gateway-http:foo" - dest: spec.arguments.parameters.0.value diff --git a/examples/sensors/trigger-source-configmap.yaml b/examples/sensors/trigger-source-configmap.yaml index 229c49c361..b82b1a3a99 100644 --- a/examples/sensors/trigger-source-configmap.yaml +++ b/examples/sensors/trigger-source-configmap.yaml @@ -4,9 +4,6 @@ metadata: name: trigger-source-configmap-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -20,10 +17,10 @@ spec: http: port: "9300" dependencies: - - name: "artifact-gateway:example-1" + - name: "minio-gateway:example-1" triggers: - template: - name: artifact-workflow-trigger + name: minio-workflow-trigger group: argoproj.io version: v1alpha1 resource: workflows diff --git a/examples/sensors/trigger-source-file.yaml b/examples/sensors/trigger-source-file.yaml index 95906341e0..3340d3962b 100644 --- a/examples/sensors/trigger-source-file.yaml +++ b/examples/sensors/trigger-source-file.yaml @@ -5,9 +5,6 @@ metadata: labels: # sensor controller with instanceId "argo-events" will process this sensor sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: diff --git a/examples/sensors/trigger-source-git.yaml b/examples/sensors/trigger-source-git.yaml index 2c10ad1002..b0d48f374d 100644 --- a/examples/sensors/trigger-source-git.yaml +++ b/examples/sensors/trigger-source-git.yaml @@ -4,9 +4,6 @@ metadata: name: trigger-source-git labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -39,7 +36,7 @@ spec: secretName: git-known-hosts serviceAccountName: argo-events-sa dependencies: - - name: "webhook-gateway-http:foo" + - name: "webhook-gateway:example" eventProtocol: type: "HTTP" http: diff --git a/examples/sensors/trigger-standard-k8s-resource.yaml b/examples/sensors/trigger-standard-k8s-resource.yaml index 75fc4d9ddb..8cc35761da 100644 --- a/examples/sensors/trigger-standard-k8s-resource.yaml +++ b/examples/sensors/trigger-standard-k8s-resource.yaml @@ -4,9 +4,6 @@ metadata: name: webhook-sensor-http labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -16,7 +13,7 @@ spec: imagePullPolicy: Always serviceAccountName: argo-events-sa dependencies: - - name: "webhook-gateway-http:foo" + - name: "webhook-gateway:example" eventProtocol: type: "HTTP" http: @@ -29,7 +26,7 @@ spec: version: v1 resource: pods source: - inline: | + resource: apiVersion: v1 kind: Pod metadata: @@ -48,7 +45,7 @@ spec: version: v1 resource: deployments source: - inline: | + resource: apiVersion: apps/v1 kind: Deployment metadata: diff --git a/examples/sensors/trigger-with-backoff.yaml b/examples/sensors/trigger-with-backoff.yaml index 014140cde9..b50ce7dc1f 100644 --- a/examples/sensors/trigger-with-backoff.yaml +++ b/examples/sensors/trigger-with-backoff.yaml @@ -4,9 +4,6 @@ metadata: name: trigger-backoff labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -16,7 +13,7 @@ spec: imagePullPolicy: Always serviceAccountName: argo-events-sa dependencies: - - name: "webhook-gateway-http:foo" + - name: "webhook-gateway:example" eventProtocol: type: "HTTP" http: @@ -58,7 +55,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -81,7 +78,7 @@ spec: args: ["{{inputs.parameters.message}}"] resourceParameters: - src: - event: "webhook-gateway-http:foo" + event: "webhook-gateway:example" dest: spec.arguments.parameters.0.value - template: name: trigger-2 @@ -97,9 +94,9 @@ spec: errorOnBackoffTimeout: false group: argoproj.io version: v1alpha1 - resource: workflows + kind: Workflow source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/url-sensor.yaml b/examples/sensors/url-sensor.yaml index 550d7b6698..96be1455a2 100644 --- a/examples/sensors/url-sensor.yaml +++ b/examples/sensors/url-sensor.yaml @@ -4,9 +4,6 @@ metadata: name: url-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -20,7 +17,7 @@ spec: http: port: "9300" dependencies: - - name: "artifact-gateway:input" + - name: "minio-gateway:example-with-filter" triggers: - template: name: url-workflow-trigger diff --git a/examples/sensors/webhook-nats-streaming.yaml b/examples/sensors/webhook-nats-streaming.yaml index 4aa7d0ca83..44749a3410 100644 --- a/examples/sensors/webhook-nats-streaming.yaml +++ b/examples/sensors/webhook-nats-streaming.yaml @@ -4,9 +4,6 @@ metadata: name: webhook-nats-streaming labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -32,7 +29,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/webhook-nats.yaml b/examples/sensors/webhook-nats.yaml index 13633b444c..3d1d940331 100644 --- a/examples/sensors/webhook-nats.yaml +++ b/examples/sensors/webhook-nats.yaml @@ -4,9 +4,6 @@ metadata: name: webhook-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -29,7 +26,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/examples/sensors/webhook.yaml b/examples/sensors/webhook.yaml index 8bff451423..0f4329e22b 100644 --- a/examples/sensors/webhook.yaml +++ b/examples/sensors/webhook.yaml @@ -4,9 +4,6 @@ metadata: name: webhook-sensor labels: sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 spec: template: spec: @@ -28,7 +25,7 @@ spec: version: v1alpha1 resource: workflows source: - inline: | + resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: diff --git a/gateways/Dockerfile b/gateways/client/Dockerfile similarity index 100% rename from gateways/Dockerfile rename to gateways/client/Dockerfile diff --git a/gateways/cmd/main.go b/gateways/client/client.go similarity index 78% rename from gateways/cmd/main.go rename to gateways/client/client.go index 4ea1bf8e8a..b86599b1f6 100644 --- a/gateways/cmd/main.go +++ b/gateways/client/client.go @@ -24,13 +24,12 @@ import ( "time" "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" "k8s.io/apimachinery/pkg/util/wait" ) func main() { - // initialize gateway configuration - gc := gateways.NewGatewayConfiguration() + // initialize gateway context + ctx := NewGatewayContext() serverPort, ok := os.LookupEnv(common.EnvVarGatewayServerPort) if !ok { @@ -53,19 +52,19 @@ func main() { panic(fmt.Errorf("failed to connect to server on port %s", serverPort)) } - // handle event source's status updates + // handle gateway status updates go func() { - for status := range gc.StatusCh { - gc.UpdateGatewayResourceState(&status) + for status := range ctx.statusCh { + ctx.UpdateGatewayState(&status) } }() // watch updates to gateway resource - if _, err := gc.WatchGateway(context.Background()); err != nil { + if _, err := ctx.WatchGatewayUpdates(context.Background()); err != nil { panic(err) } // watch for event source updates - if _, err := gc.WatchGatewayEventSources(context.Background()); err != nil { + if _, err := ctx.WatchGatewayEventSources(context.Background()); err != nil { panic(err) } select {} diff --git a/gateways/client/context.go b/gateways/client/context.go new file mode 100644 index 0000000000..91417d18a5 --- /dev/null +++ b/gateways/client/context.go @@ -0,0 +1,158 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + pc "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + eventsourceClientset "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned" + gwclientset "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" + "github.com/nats-io/go-nats" + snats "github.com/nats-io/go-nats-streaming" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// GatewayContext holds the context for a gateway +type GatewayContext struct { + // logger logs stuff + logger *logrus.Logger + // k8sClient is client for kubernetes API + k8sClient kubernetes.Interface + // eventSourceRef refers to event-source for the gateway + eventSourceRef *v1alpha1.EventSourceRef + // eventSourceClient is the client for EventSourceRef resource + eventSourceClient eventsourceClientset.Interface + // name of the gateway + name string + // namespace where gateway is deployed + namespace string + // gateway refers to Gateway custom resource + gateway *v1alpha1.Gateway + // gatewayClient is gateway clientset + gatewayClient gwclientset.Interface + // updated indicates whether gateway resource is updated + updated bool + // serverPort is gateway server port to listen events from + serverPort string + // eventSourceContexts stores information about current event sources that are running in the gateway + eventSourceContexts map[string]*EventSourceContext + // controllerInstanceId is instance ID of the gateway controller + controllerInstanceID string + // statusCh is used to communicate the status of an event source + statusCh chan EventSourceStatus + // natsConn is the standard nats connection used to publish events to cluster. Only used if dispatch protocol is NATS + natsConn *nats.Conn + // natsStreamingConn is the nats connection used for streaming. + natsStreamingConn snats.Conn + // sensorHttpPort is the http server running in sensor that listens to event. Only used if dispatch protocol is HTTP + sensorHttpPort string +} + +// EventSourceContext contains information of a event source for gateway to run. +type EventSourceContext struct { + // source holds the actual event source + source *gateways.EventSource + // ctx contains context for the connection + ctx context.Context + // cancel upon invocation cancels the connection context + cancel context.CancelFunc + // client is grpc client + client gateways.EventingClient + // conn is grpc connection + conn *grpc.ClientConn +} + +// NewGatewayContext returns a new gateway context +func NewGatewayContext() *GatewayContext { + kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig) + restConfig, err := common.GetClientConfig(kubeConfig) + if err != nil { + panic(err) + } + name, ok := os.LookupEnv(common.EnvVarResourceName) + if !ok { + panic("gateway name not provided") + } + namespace, ok := os.LookupEnv(common.EnvVarNamespace) + if !ok { + panic("no namespace provided") + } + controllerInstanceID, ok := os.LookupEnv(common.EnvVarControllerInstanceID) + if !ok { + panic("gateway controller instance ID is not provided") + } + serverPort, ok := os.LookupEnv(common.EnvVarGatewayServerPort) + if !ok { + panic("server port is not provided") + } + + clientset := kubernetes.NewForConfigOrDie(restConfig) + gatewayClient := gwclientset.NewForConfigOrDie(restConfig) + eventSourceClient := eventsourceClientset.NewForConfigOrDie(restConfig) + + gateway, err := gatewayClient.ArgoprojV1alpha1().Gateways(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + panic(err) + } + + gatewayConfig := &GatewayContext{ + logger: common.NewArgoEventsLogger().WithFields( + map[string]interface{}{ + common.LabelResourceName: gateway.Name, + common.LabelNamespace: gateway.Namespace, + }).Logger, + k8sClient: clientset, + namespace: namespace, + name: name, + eventSourceContexts: make(map[string]*EventSourceContext), + eventSourceRef: gateway.Spec.EventSourceRef, + eventSourceClient: eventSourceClient, + gatewayClient: gatewayClient, + gateway: gateway, + controllerInstanceID: controllerInstanceID, + serverPort: serverPort, + statusCh: make(chan EventSourceStatus), + } + + switch gateway.Spec.EventProtocol.Type { + case pc.HTTP: + gatewayConfig.sensorHttpPort = gateway.Spec.EventProtocol.Http.Port + case pc.NATS: + if gatewayConfig.natsConn, err = nats.Connect(gateway.Spec.EventProtocol.Nats.URL); err != nil { + panic(fmt.Errorf("failed to obtain NATS standard connection. err: %+v", err)) + } + gatewayConfig.logger.WithField(common.LabelURL, gateway.Spec.EventProtocol.Nats.URL).Infoln("connected to nats service") + + if gatewayConfig.gateway.Spec.EventProtocol.Nats.Type == pc.Streaming { + gatewayConfig.natsStreamingConn, err = snats.Connect(gatewayConfig.gateway.Spec.EventProtocol.Nats.ClusterId, gatewayConfig.gateway.Spec.EventProtocol.Nats.ClientId, snats.NatsConn(gatewayConfig.natsConn)) + if err != nil { + panic(fmt.Errorf("failed to obtain NATS streaming connection. err: %+v", err)) + } + gatewayConfig.logger.WithField(common.LabelURL, gateway.Spec.EventProtocol.Nats.URL).Infoln("nats streaming connection successful") + } + } + return gatewayConfig +} diff --git a/gateways/client/event-source_test.go b/gateways/client/event-source_test.go new file mode 100644 index 0000000000..a825c13ac3 --- /dev/null +++ b/gateways/client/event-source_test.go @@ -0,0 +1,240 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "net" + "net/http" + "testing" + "time" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + esv1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + gwfake "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/fake" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func getGatewayContext() *GatewayContext { + return &GatewayContext{ + logger: common.NewArgoEventsLogger(), + serverPort: "20000", + statusCh: make(chan EventSourceStatus), + gateway: &v1alpha1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-gateway", + Namespace: "fake-namespace", + }, + Spec: v1alpha1.GatewaySpec{ + Watchers: &v1alpha1.NotificationWatchers{ + Sensors: []v1alpha1.SensorNotificationWatcher{}, + }, + EventProtocol: &apicommon.EventProtocol{ + Type: apicommon.HTTP, + Http: apicommon.Http{ + Port: "9000", + }, + }, + Type: apicommon.WebhookEvent, + }, + }, + eventSourceContexts: make(map[string]*EventSourceContext), + k8sClient: fake.NewSimpleClientset(), + gatewayClient: gwfake.NewSimpleClientset(), + } +} + +func getEventSource() *esv1alpha1.EventSource { + return &esv1alpha1.EventSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-event-source", + Namespace: "fake-namespace", + }, + Spec: &esv1alpha1.EventSourceSpec{ + Webhook: map[string]webhook.Context{ + "first-webhook": { + Endpoint: "/first-webhook", + Method: http.MethodPost, + Port: "13000", + }, + }, + Type: apicommon.WebhookEvent, + }, + } +} + +// Set up a fake gateway server +type testEventListener struct{} + +func (listener *testEventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer func() { + if r := recover(); r != nil { + fmt.Println(r) + } + }() + _ = eventStream.Send(&gateways.Event{ + Name: eventSource.Name, + Payload: []byte("test payload"), + }) + + <-eventStream.Context().Done() + + return nil +} + +func (listener *testEventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func getGatewayServer() *grpc.Server { + srv := grpc.NewServer() + gateways.RegisterEventingServer(srv, &testEventListener{}) + return srv +} + +func TestInitEventSourceContexts(t *testing.T) { + gatewayContext := getGatewayContext() + eventSource := getEventSource().DeepCopy() + + lis, err := net.Listen("tcp", fmt.Sprintf(":%s", gatewayContext.serverPort)) + if err != nil { + panic(err) + } + + server := getGatewayServer() + stopCh := make(chan struct{}) + + go func() { + if err := server.Serve(lis); err != nil { + return + } + }() + + go func() { + <-stopCh + server.GracefulStop() + fmt.Println("server is stopped") + }() + + contexts := gatewayContext.initEventSourceContexts(eventSource) + assert.NotNil(t, contexts) + for _, esContext := range contexts { + assert.Equal(t, "first-webhook", esContext.source.Name) + assert.NotNil(t, esContext.conn) + } + + stopCh <- struct{}{} + + time.Sleep(5 * time.Second) +} + +func TestSyncEventSources(t *testing.T) { + gatewayContext := getGatewayContext() + eventSource := getEventSource().DeepCopy() + + lis, err := net.Listen("tcp", fmt.Sprintf(":%s", gatewayContext.serverPort)) + if err != nil { + panic(err) + } + + server := getGatewayServer() + stopCh := make(chan struct{}) + stopStatus := make(chan struct{}) + + go func() { + if err := server.Serve(lis); err != nil { + fmt.Println(err) + return + } + }() + + go func() { + for { + select { + case status := <-gatewayContext.statusCh: + fmt.Println(status.Message) + case <-stopStatus: + fmt.Println("returning from status") + return + } + } + }() + + go func() { + <-stopCh + server.GracefulStop() + fmt.Println("server is stopped") + stopStatus <- struct{}{} + }() + + err = gatewayContext.syncEventSources(eventSource) + assert.Nil(t, err) + + time.Sleep(5 * time.Second) + + delete(eventSource.Spec.Webhook, "first-webhook") + + eventSource.Spec.Webhook["second-webhook"] = webhook.Context{ + Endpoint: "/second-webhook", + Method: http.MethodPost, + Port: "13000", + } + + err = gatewayContext.syncEventSources(eventSource) + assert.Nil(t, err) + + time.Sleep(5 * time.Second) + + delete(eventSource.Spec.Webhook, "second-webhook") + + err = gatewayContext.syncEventSources(eventSource) + assert.Nil(t, err) + + time.Sleep(5 * time.Second) + + stopCh <- struct{}{} + + time.Sleep(5 * time.Second) +} + +func TestDiffEventSources(t *testing.T) { + gatewayContext := getGatewayContext() + eventSourceContexts := map[string]*EventSourceContext{ + "first-webhook": {}, + } + assert.NotNil(t, eventSourceContexts) + staleEventSources, newEventSources := gatewayContext.diffEventSources(eventSourceContexts) + assert.Nil(t, staleEventSources) + assert.NotNil(t, newEventSources) + gatewayContext.eventSourceContexts = map[string]*EventSourceContext{ + "first-webhook": {}, + } + delete(eventSourceContexts, "first-webhook") + staleEventSources, newEventSources = gatewayContext.diffEventSources(eventSourceContexts) + assert.NotNil(t, staleEventSources) + assert.Nil(t, newEventSources) +} diff --git a/gateways/client/event-sources.go b/gateways/client/event-sources.go new file mode 100644 index 0000000000..0b0126a109 --- /dev/null +++ b/gateways/client/event-sources.go @@ -0,0 +1,356 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + eventSourceV1Alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/argoproj/argo-events/pkg/apis/gateway" + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + "github.com/ghodss/yaml" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "io" +) + +// populateEventSourceContexts sets up the contexts for event sources +func (gatewayContext *GatewayContext) populateEventSourceContexts(name string, value interface{}, eventSourceContexts map[string]*EventSourceContext) { + body, err := yaml.Marshal(value) + if err != nil { + gatewayContext.logger.WithField("event-source-name", name).Errorln("failed to marshal the event source value, won't process it") + return + } + + hashKey := common.Hasher(name + string(body)) + + logger := gatewayContext.logger.WithFields(logrus.Fields{ + "name": name, + "value": value, + }) + + logger.WithField("hash", hashKey).Debugln("hash of the event source") + + // create a connection to gateway server + connCtx, cancel := context.WithCancel(context.Background()) + conn, err := grpc.Dial( + fmt.Sprintf("localhost:%s", gatewayContext.serverPort), + grpc.WithBlock(), + grpc.WithInsecure()) + if err != nil { + logger.WithError(err).Errorln("failed to connect to gateway server") + cancel() + return + } + + logger.WithField("state", conn.GetState().String()).Info("state of the connection to gateway server") + + eventSourceContexts[hashKey] = &EventSourceContext{ + source: &gateways.EventSource{ + Id: hashKey, + Name: name, + Value: body, + Type: string(gatewayContext.gateway.Spec.Type), + }, + cancel: cancel, + ctx: connCtx, + client: gateways.NewEventingClient(conn), + conn: conn, + } +} + +// diffConfig diffs currently active event sources and updated event sources. +// It simply matches the event source strings. So, if event source string differs through some sequence of definition +// and although the event sources are actually same, this method will treat them as different event sources. +// old event sources - event sources to be deactivate +// new event sources - new event sources to activate +func (gatewayContext *GatewayContext) diffEventSources(eventSourceContexts map[string]*EventSourceContext) (staleEventSources []string, newEventSources []string) { + var currentEventSources []string + var updatedEventSources []string + + for currentEventSource := range gatewayContext.eventSourceContexts { + currentEventSources = append(currentEventSources, currentEventSource) + } + for updatedEventSource := range eventSourceContexts { + updatedEventSources = append(updatedEventSources, updatedEventSource) + } + + gatewayContext.logger.WithField("current-event-sources-keys", currentEventSources).Debugln("event sources hashes") + gatewayContext.logger.WithField("updated-event-sources-keys", updatedEventSources).Debugln("event sources hashes") + + swapped := false + // iterates over current event sources and updated event sources + // and creates two arrays, first one containing event sources that need to removed + // and second containing new event sources that need to be added and run. + for i := 0; i < 2; i++ { + for _, currentEventSource := range currentEventSources { + found := false + for _, updatedEventSource := range updatedEventSources { + if currentEventSource == updatedEventSource { + found = true + break + } + } + if !found { + if swapped { + newEventSources = append(newEventSources, currentEventSource) + } else { + staleEventSources = append(staleEventSources, currentEventSource) + } + } + } + if i == 0 { + currentEventSources, updatedEventSources = updatedEventSources, currentEventSources + swapped = true + } + } + return +} + +// activateEventSources activate new event sources +func (gatewayContext *GatewayContext) activateEventSources(eventSources map[string]*EventSourceContext, keys []string) { + for _, key := range keys { + eventSource := eventSources[key] + // register the event source + gatewayContext.eventSourceContexts[key] = eventSource + + logger := gatewayContext.logger.WithField(common.LabelEventSource, eventSource.source.Name) + + logger.Infoln("activating new event source...") + + go func() { + // conn should be in READY state + if eventSource.conn.GetState() != connectivity.Ready { + logger.Errorln("connection is not in ready state.") + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseError, + Id: eventSource.source.Id, + Message: "connection_is_not_in_ready_state", + Name: eventSource.source.Name, + } + return + } + + // validate event source + if valid, _ := eventSource.client.ValidateEventSource(eventSource.ctx, eventSource.source); !valid.IsValid { + logger.WithFields( + map[string]interface{}{ + "validation-failure": valid.Reason, + }, + ).Errorln("event source is not valid") + if err := eventSource.conn.Close(); err != nil { + logger.WithError(err).Errorln("failed to close client connection") + } + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseError, + Id: eventSource.source.Id, + Message: "event_source_is_not_valid", + Name: eventSource.source.Name, + } + return + } + + logger.Infoln("event source is valid") + + // mark event source as running + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseRunning, + Message: "event_source_is_running", + Id: eventSource.source.Id, + Name: eventSource.source.Name, + } + + // listen to events from gateway server + eventStream, err := eventSource.client.StartEventSource(eventSource.ctx, eventSource.source) + if err != nil { + logger.WithError(err).Errorln("error occurred while starting event source") + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseError, + Message: "failed_to_receive_event_stream", + Name: eventSource.source.Name, + Id: eventSource.source.Id, + } + return + } + + logger.Infoln("listening to events from gateway server...") + for { + event, err := eventStream.Recv() + if err != nil { + if err == io.EOF { + logger.Infoln("event source has stopped") + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseCompleted, + Message: "event_source_has_been_stopped", + Name: eventSource.source.Name, + Id: eventSource.source.Id, + } + return + } + + logger.WithError(err).Errorln("failed to receive event from stream") + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseError, + Message: "failed_to_receive_event_from_event_source_stream", + Name: eventSource.source.Name, + Id: eventSource.source.Id, + } + return + } + err = gatewayContext.DispatchEvent(event) + if err != nil { + // escalate error through a K8s event + labels := map[string]string{ + common.LabelEventType: string(common.EscalationEventType), + common.LabelEventSourceName: eventSource.source.Name, + common.LabelResourceName: gatewayContext.name, + common.LabelEventSourceID: eventSource.source.Id, + common.LabelOperation: "dispatch_event_to_watchers", + } + if err := common.GenerateK8sEvent(gatewayContext.k8sClient, fmt.Sprintf("failed to dispatch event to watchers"), common.EscalationEventType, "event dispatch failed", gatewayContext.name, gatewayContext.namespace, gatewayContext.controllerInstanceID, gateway.Kind, labels); err != nil { + logger.WithError(err).Errorln("failed to create K8s event to escalate event dispatch failure") + } + logger.WithError(err).Errorln("failed to dispatch event to watchers") + } + } + }() + } +} + +// deactivateEventSources inactivate an existing event sources +func (gatewayContext *GatewayContext) deactivateEventSources(eventSourceNames []string) { + for _, eventSourceName := range eventSourceNames { + eventSource := gatewayContext.eventSourceContexts[eventSourceName] + if eventSource == nil { + continue + } + + logger := gatewayContext.logger.WithField(common.LabelEventSource, eventSourceName) + + logger.WithField(common.LabelEventSource, eventSource.source.Name).Infoln("stopping the event source") + delete(gatewayContext.eventSourceContexts, eventSourceName) + gatewayContext.statusCh <- EventSourceStatus{ + Phase: v1alpha1.NodePhaseRemove, + Id: eventSource.source.Id, + Message: "event_source_is_removed", + Name: eventSource.source.Name, + } + eventSource.cancel() + if err := eventSource.conn.Close(); err != nil { + logger.WithField(common.LabelEventSource, eventSource.source.Name).WithError(err).Errorln("failed to close client connection") + } + } +} + +// syncEventSources syncs active event-sources and the updated ones +func (gatewayContext *GatewayContext) syncEventSources(eventSource *eventSourceV1Alpha1.EventSource) error { + eventSourceContexts := gatewayContext.initEventSourceContexts(eventSource) + + staleEventSources, newEventSources := gatewayContext.diffEventSources(eventSourceContexts) + gatewayContext.logger.WithField(common.LabelEventSource, staleEventSources).Infoln("deleted event sources") + gatewayContext.logger.WithField(common.LabelEventSource, newEventSources).Infoln("new event sources") + + // stop existing event sources + gatewayContext.deactivateEventSources(staleEventSources) + + // start new event sources + gatewayContext.activateEventSources(eventSourceContexts, newEventSources) + + return nil +} + +// initEventSourceContext creates an internal representation of event sources. +func (gatewayContext *GatewayContext) initEventSourceContexts(eventSource *eventSourceV1Alpha1.EventSource) map[string]*EventSourceContext { + eventSourceContexts := make(map[string]*EventSourceContext) + + switch gatewayContext.gateway.Spec.Type { + case apicommon.SNSEvent: + for key, value := range eventSource.Spec.SNS { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.SQSEvent: + for key, value := range eventSource.Spec.SQS { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.PubSubEvent: + for key, value := range eventSource.Spec.PubSub { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.NATSEvent: + for key, value := range eventSource.Spec.NATS { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.FileEvent: + for key, value := range eventSource.Spec.File { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.CalendarEvent: + for key, value := range eventSource.Spec.Calendar { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.AMQPEvent: + for key, value := range eventSource.Spec.AMQP { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.GitHubEvent: + for key, value := range eventSource.Spec.Github { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.GitLabEvent: + for key, value := range eventSource.Spec.Gitlab { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.HDFSEvent: + for key, value := range eventSource.Spec.HDFS { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.KafkaEvent: + for key, value := range eventSource.Spec.Kafka { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.MinioEvent: + for key, value := range eventSource.Spec.Minio { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.MQTTEvent: + for key, value := range eventSource.Spec.MQTT { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.ResourceEvent: + for key, value := range eventSource.Spec.Resource { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.SlackEvent: + for key, value := range eventSource.Spec.Slack { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.StorageGridEvent: + for key, value := range eventSource.Spec.StorageGrid { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + case apicommon.WebhookEvent: + for key, value := range eventSource.Spec.Webhook { + gatewayContext.populateEventSourceContexts(key, value, eventSourceContexts) + } + } + + return eventSourceContexts +} diff --git a/gateways/state.go b/gateways/client/state.go similarity index 50% rename from gateways/state.go rename to gateways/client/state.go index c292f89cb5..007bf8eb93 100644 --- a/gateways/state.go +++ b/gateways/client/state.go @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package main import ( - gtw "github.com/argoproj/argo-events/controllers/gateway" "time" - "github.com/argoproj/argo-events/pkg/apis/gateway" - "github.com/argoproj/argo-events/common" + gtw "github.com/argoproj/argo-events/controllers/gateway" + "github.com/argoproj/argo-events/pkg/apis/gateway" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -38,38 +37,38 @@ type EventSourceStatus struct { // Phase of the event source Phase v1alpha1.NodePhase // Gateway reference - Gw *v1alpha1.Gateway + Gateway *v1alpha1.Gateway } // markGatewayNodePhase marks the node with a phase, returns the node -func (gc *GatewayConfig) markGatewayNodePhase(nodeStatus *EventSourceStatus) *v1alpha1.NodeStatus { - log := gc.Log.WithFields( +func (gatewayContext *GatewayContext) markGatewayNodePhase(nodeStatus *EventSourceStatus) *v1alpha1.NodeStatus { + logger := gatewayContext.logger.WithFields( map[string]interface{}{ common.LabelNodeName: nodeStatus.Name, common.LabelPhase: string(nodeStatus.Phase), }, ) - log.Info("marking node phase") + logger.Infoln("marking node phase") - node := gc.getNodeByID(nodeStatus.Id) + node := gatewayContext.getNodeByID(nodeStatus.Id) if node == nil { - log.Warn("node is not initialized") + logger.Warnln("node is not initialized") return nil } if node.Phase != nodeStatus.Phase { - log.WithField("new-phase", string(nodeStatus.Phase)).Info("phase updated") + logger.WithField("new-phase", string(nodeStatus.Phase)).Infoln("phase updated") node.Phase = nodeStatus.Phase } node.Message = nodeStatus.Message - gc.gw.Status.Nodes[node.ID] = *node - gc.updated = true + gatewayContext.gateway.Status.Nodes[node.ID] = *node + gatewayContext.updated = true return node } // getNodeByName returns the node from this gateway for the nodeName -func (gc *GatewayConfig) getNodeByID(nodeID string) *v1alpha1.NodeStatus { - node, ok := gc.gw.Status.Nodes[nodeID] +func (gatewayContext *GatewayContext) getNodeByID(nodeID string) *v1alpha1.NodeStatus { + node, ok := gatewayContext.gateway.Status.Nodes[nodeID] if !ok { return nil } @@ -77,12 +76,14 @@ func (gc *GatewayConfig) getNodeByID(nodeID string) *v1alpha1.NodeStatus { } // create a new node -func (gc *GatewayConfig) initializeNode(nodeID string, nodeName string, messages string) v1alpha1.NodeStatus { - if gc.gw.Status.Nodes == nil { - gc.gw.Status.Nodes = make(map[string]v1alpha1.NodeStatus) +func (gatewayContext *GatewayContext) initializeNode(nodeID string, nodeName string, messages string) v1alpha1.NodeStatus { + if gatewayContext.gateway.Status.Nodes == nil { + gatewayContext.gateway.Status.Nodes = make(map[string]v1alpha1.NodeStatus) } - gc.Log.WithField(common.LabelNodeName, nodeName).Info("node") - node, ok := gc.gw.Status.Nodes[nodeID] + + gatewayContext.logger.WithField(common.LabelNodeName, nodeName).Infoln("node") + + node, ok := gatewayContext.gateway.Status.Nodes[nodeID] if !ok { node = v1alpha1.NodeStatus{ ID: nodeID, @@ -93,66 +94,68 @@ func (gc *GatewayConfig) initializeNode(nodeID string, nodeName string, messages } node.Phase = v1alpha1.NodePhaseRunning node.Message = messages - gc.gw.Status.Nodes[nodeID] = node - gc.Log.WithFields( + gatewayContext.gateway.Status.Nodes[nodeID] = node + + gatewayContext.logger.WithFields( map[string]interface{}{ common.LabelNodeName: nodeName, "node-message": node.Message, }, - ).Info("node is running") - gc.updated = true + ).Infoln("node is running") + + gatewayContext.updated = true return node } -// UpdateGatewayResourceState updates gateway resource nodes state -func (gc *GatewayConfig) UpdateGatewayResourceState(status *EventSourceStatus) { - log := gc.Log +// UpdateGatewayState updates gateway resource nodes state +func (gatewayContext *GatewayContext) UpdateGatewayState(status *EventSourceStatus) { + logger := gatewayContext.logger if status.Phase != v1alpha1.NodePhaseResourceUpdate { - log = log.WithField(common.LabelEventSource, status.Name).Logger + logger = logger.WithField(common.LabelEventSource, status.Name).Logger } - log.Info("received a gateway state update notification") + logger.Infoln("received a gateway state update notification") switch status.Phase { case v1alpha1.NodePhaseRunning: // init the node and mark it as running - gc.initializeNode(status.Id, status.Name, status.Message) + gatewayContext.initializeNode(status.Id, status.Name, status.Message) case v1alpha1.NodePhaseCompleted, v1alpha1.NodePhaseError: - gc.markGatewayNodePhase(status) + gatewayContext.markGatewayNodePhase(status) case v1alpha1.NodePhaseResourceUpdate: - gc.gw = status.Gw + gatewayContext.gateway = status.Gateway case v1alpha1.NodePhaseRemove: - delete(gc.gw.Status.Nodes, status.Id) - log.Info("event source is removed") - gc.updated = true + delete(gatewayContext.gateway.Status.Nodes, status.Id) + logger.Infoln("event source is removed") + gatewayContext.updated = true } - if gc.updated { + if gatewayContext.updated { // persist changes and create K8s event logging the change eventType := common.StateChangeEventType labels := map[string]string{ - common.LabelGatewayEventSourceName: status.Name, - common.LabelGatewayName: gc.Name, - common.LabelGatewayEventSourceID: status.Id, - common.LabelOperation: "persist_event_source_state", + common.LabelEventSourceName: status.Name, + common.LabelResourceName: gatewayContext.name, + common.LabelEventSourceID: status.Id, + common.LabelOperation: "persist_event_source_state", } - updatedGw, err := gtw.PersistUpdates(gc.gwcs, gc.gw, gc.Log) + updatedGw, err := gtw.PersistUpdates(gatewayContext.gatewayClient, gatewayContext.gateway, gatewayContext.logger) if err != nil { - log.WithError(err).Error("failed to persist gateway resource updates, reverting to old state") + logger.WithError(err).Errorln("failed to persist gateway resource updates, reverting to old state") eventType = common.EscalationEventType } // update gateway ref. in case of failure to persist updates, this is a deep copy of old gateway resource - gc.gw = updatedGw + gatewayContext.gateway = updatedGw labels[common.LabelEventType] = string(eventType) // generate a K8s event for persist event source state change - if err := common.GenerateK8sEvent(gc.Clientset, status.Message, eventType, "event source state update", gc.Name, gc.Namespace, gc.controllerInstanceID, gateway.Kind, labels); err != nil { - log.WithError(err).Error("failed to create K8s event to log event source state change") + if err := common.GenerateK8sEvent(gatewayContext.k8sClient, status.Message, eventType, "event source state update", gatewayContext.name, gatewayContext.namespace, gatewayContext.controllerInstanceID, gateway.Kind, labels); err != nil { + logger.WithError(err).Errorln("failed to create K8s event to log event source state change") } } - gc.updated = false + gatewayContext.updated = false } diff --git a/gateways/state_test.go b/gateways/client/state_test.go similarity index 75% rename from gateways/state_test.go rename to gateways/client/state_test.go index 82fb3545d5..d389582a8d 100644 --- a/gateways/state_test.go +++ b/gateways/client/state_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package main import ( "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" @@ -23,26 +23,26 @@ import ( ) func TestGatewayState(t *testing.T) { - gc := getGatewayConfig() + gc := getGatewayContext() convey.Convey("Given a gateway", t, func() { convey.Convey("Create the gateway", func() { var err error - gc.gw, err = gc.gwcs.ArgoprojV1alpha1().Gateways(gc.gw.Namespace).Create(gc.gw) + gc.gateway, err = gc.gatewayClient.ArgoprojV1alpha1().Gateways(gc.gateway.Namespace).Create(gc.gateway) convey.So(err, convey.ShouldBeNil) }) convey.Convey("Update gateway resource test-node node state to running", func() { - gc.UpdateGatewayResourceState(&EventSourceStatus{ + gc.UpdateGatewayState(&EventSourceStatus{ Phase: v1alpha1.NodePhaseRunning, Name: "test-node", Message: "node is marked as running", Id: "test-node", }) - convey.So(len(gc.gw.Status.Nodes), convey.ShouldEqual, 1) - convey.So(gc.gw.Status.Nodes["test-node"].Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) + convey.So(len(gc.gateway.Status.Nodes), convey.ShouldEqual, 1) + convey.So(gc.gateway.Status.Nodes["test-node"].Phase, convey.ShouldEqual, v1alpha1.NodePhaseRunning) }) - updatedGw := gc.gw + updatedGw := gc.gateway updatedGw.Spec.Watchers = &v1alpha1.NotificationWatchers{ Sensors: []v1alpha1.SensorNotificationWatcher{ { @@ -52,49 +52,49 @@ func TestGatewayState(t *testing.T) { } convey.Convey("Update gateway watchers", func() { - gc.UpdateGatewayResourceState(&EventSourceStatus{ + gc.UpdateGatewayState(&EventSourceStatus{ Phase: v1alpha1.NodePhaseResourceUpdate, Name: "test-node", Message: "gateway resource is updated", Id: "test-node", - Gw: updatedGw, + Gateway: updatedGw, }) - convey.So(len(gc.gw.Spec.Watchers.Sensors), convey.ShouldEqual, 1) + convey.So(len(gc.gateway.Spec.Watchers.Sensors), convey.ShouldEqual, 1) }) convey.Convey("Update gateway resource test-node node state to completed", func() { - gc.UpdateGatewayResourceState(&EventSourceStatus{ + gc.UpdateGatewayState(&EventSourceStatus{ Phase: v1alpha1.NodePhaseCompleted, Name: "test-node", Message: "node is marked completed", Id: "test-node", }) - convey.So(gc.gw.Status.Nodes["test-node"].Phase, convey.ShouldEqual, v1alpha1.NodePhaseCompleted) + convey.So(gc.gateway.Status.Nodes["test-node"].Phase, convey.ShouldEqual, v1alpha1.NodePhaseCompleted) }) convey.Convey("Remove gateway resource test-node node", func() { - gc.UpdateGatewayResourceState(&EventSourceStatus{ + gc.UpdateGatewayState(&EventSourceStatus{ Phase: v1alpha1.NodePhaseRemove, Name: "test-node", Message: "node is removed", Id: "test-node", }) - convey.So(len(gc.gw.Status.Nodes), convey.ShouldEqual, 0) + convey.So(len(gc.gateway.Status.Nodes), convey.ShouldEqual, 0) }) }) } func TestMarkGatewayNodePhase(t *testing.T) { convey.Convey("Given a node status, mark node state", t, func() { - gc := getGatewayConfig() + gc := getGatewayContext() nodeStatus := &EventSourceStatus{ Name: "fake", Id: "1234", Message: "running", Phase: v1alpha1.NodePhaseRunning, - Gw: gc.gw, + Gateway: gc.gateway, } - gc.gw.Status.Nodes = map[string]v1alpha1.NodeStatus{ + gc.gateway.Status.Nodes = map[string]v1alpha1.NodeStatus{ "1234": v1alpha1.NodeStatus{ Phase: v1alpha1.NodePhaseNew, Message: "init", @@ -107,7 +107,7 @@ func TestMarkGatewayNodePhase(t *testing.T) { convey.So(resultStatus, convey.ShouldNotBeNil) convey.So(resultStatus.Name, convey.ShouldEqual, nodeStatus.Name) - gc.gw.Status.Nodes = map[string]v1alpha1.NodeStatus{ + gc.gateway.Status.Nodes = map[string]v1alpha1.NodeStatus{ "4567": v1alpha1.NodeStatus{ Phase: v1alpha1.NodePhaseNew, Message: "init", @@ -123,8 +123,8 @@ func TestMarkGatewayNodePhase(t *testing.T) { func TestGetNodeByID(t *testing.T) { convey.Convey("Given a node id, retrieve the node", t, func() { - gc := getGatewayConfig() - gc.gw.Status.Nodes = map[string]v1alpha1.NodeStatus{ + gc := getGatewayContext() + gc.gateway.Status.Nodes = map[string]v1alpha1.NodeStatus{ "1234": v1alpha1.NodeStatus{ Phase: v1alpha1.NodePhaseNew, Message: "init", @@ -140,12 +140,12 @@ func TestGetNodeByID(t *testing.T) { func TestInitializeNode(t *testing.T) { convey.Convey("Given a node, initialize it", t, func() { - gc := getGatewayConfig() + gc := getGatewayContext() status := gc.initializeNode("1234", "fake", "init") convey.So(status, convey.ShouldNotBeNil) convey.So(status.ID, convey.ShouldEqual, "1234") convey.So(status.Name, convey.ShouldEqual, "fake") convey.So(status.Message, convey.ShouldEqual, "init") - convey.So(len(gc.gw.Status.Nodes), convey.ShouldEqual, 1) + convey.So(len(gc.gateway.Status.Nodes), convey.ShouldEqual, 1) }) } diff --git a/gateways/client/transformer.go b/gateways/client/transformer.go new file mode 100644 index 0000000000..6bb806f5d0 --- /dev/null +++ b/gateways/client/transformer.go @@ -0,0 +1,165 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "net/http" + "time" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + pc "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/google/uuid" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DispatchEvent dispatches event to gateway transformer for further processing +func (gatewayContext *GatewayContext) DispatchEvent(gatewayEvent *gateways.Event) error { + transformedEvent, err := gatewayContext.transformEvent(gatewayEvent) + if err != nil { + return err + } + + payload, err := json.Marshal(transformedEvent) + if err != nil { + return errors.Errorf("failed to dispatch event to watchers over http. marshalling failed. err: %+v", err) + } + + switch gatewayContext.gateway.Spec.EventProtocol.Type { + case pc.HTTP: + if err = gatewayContext.dispatchEventOverHttp(transformedEvent.Context.Source.Host, payload); err != nil { + return err + } + case pc.NATS: + if err = gatewayContext.dispatchEventOverNats(transformedEvent.Context.Source.Host, payload); err != nil { + return err + } + default: + return errors.Errorf("unknown dispatch mechanism %s", gatewayContext.gateway.Spec.EventProtocol.Type) + } + return nil +} + +// transformEvent transforms an event from event source into a CloudEvents specification compliant event +// See https://github.com/cloudevents/spec for more info. +func (gatewayContext *GatewayContext) transformEvent(gatewayEvent *gateways.Event) (*apicommon.Event, error) { + logger := gatewayContext.logger.WithField(common.LabelEventSource, gatewayEvent.Name) + + logger.Infoln("converting gateway event into cloudevents specification compliant event") + + // Create an CloudEvent + ce := &apicommon.Event{ + Context: apicommon.EventContext{ + CloudEventsVersion: common.CloudEventsVersion, + EventID: fmt.Sprintf("%x", uuid.New()), + ContentType: "application/json", + EventTime: metav1.MicroTime{Time: time.Now().UTC()}, + EventType: string(gatewayContext.gateway.Spec.Type), + Source: &apicommon.URI{ + Host: common.DefaultEventSourceName(gatewayContext.gateway.Name, gatewayEvent.Name), + }, + }, + Payload: gatewayEvent.Payload, + } + + logger.Infoln("event has been transformed into cloud event") + return ce, nil +} + +// dispatchEventOverHttp dispatches event to watchers over http. +func (gatewayContext *GatewayContext) dispatchEventOverHttp(source string, eventPayload []byte) error { + gatewayContext.logger.WithField(common.LabelEventSource, source).Infoln("dispatching event to watchers") + + completeSuccess := true + + for _, sensor := range gatewayContext.gateway.Spec.Watchers.Sensors { + namespace := gatewayContext.namespace + if sensor.Namespace != "" { + namespace = sensor.Namespace + } + if err := gatewayContext.postCloudEventToWatcher(common.ServiceDNSName(sensor.Name, namespace), gatewayContext.gateway.Spec.EventProtocol.Http.Port, common.SensorServiceEndpoint, eventPayload); err != nil { + gatewayContext.logger.WithField(common.LabelSensorName, sensor.Name).WithError(err).Warnln("failed to dispatch event to sensor watcher over http. communication error") + completeSuccess = false + } + } + for _, gateway := range gatewayContext.gateway.Spec.Watchers.Gateways { + namespace := gatewayContext.namespace + if gateway.Namespace != "" { + namespace = gateway.Namespace + } + if err := gatewayContext.postCloudEventToWatcher(common.ServiceDNSName(gateway.Name, namespace), gateway.Port, gateway.Endpoint, eventPayload); err != nil { + gatewayContext.logger.WithField(common.LabelResourceName, gateway.Name).WithError(err).Warnln("failed to dispatch event to gateway watcher over http. communication error") + completeSuccess = false + } + } + + response := "dispatched event to all watchers" + if !completeSuccess { + response = fmt.Sprintf("%s.%s", response, " although some of the dispatch operations failed, check logs for more info") + } + + gatewayContext.logger.Infoln(response) + return nil +} + +// dispatchEventOverNats dispatches event over nats +func (gatewayContext *GatewayContext) dispatchEventOverNats(source string, eventPayload []byte) error { + var err error + + switch gatewayContext.gateway.Spec.EventProtocol.Nats.Type { + case pc.Standard: + err = gatewayContext.natsConn.Publish(source, eventPayload) + case pc.Streaming: + err = gatewayContext.natsStreamingConn.Publish(source, eventPayload) + } + + if err != nil { + gatewayContext.logger.WithField(common.LabelEventSource, source).WithError(err).Errorln("failed to publish event") + return err + } + + gatewayContext.logger.WithField(common.LabelEventSource, source).Infoln("event published successfully") + return nil +} + +// postCloudEventToWatcher makes a HTTP POST call to watcher's service +func (gatewayContext *GatewayContext) postCloudEventToWatcher(host string, port string, endpoint string, payload []byte) error { + req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%s%s", host, port, endpoint), bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 20 * time.Second, + Transport: &http.Transport{ + Dial: (&net.Dialer{ + KeepAlive: 600 * time.Second, + }).Dial, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 50, + }, + } + _, err = client.Do(req) + return err +} diff --git a/gateways/transformer_test.go b/gateways/client/transformer_test.go similarity index 82% rename from gateways/transformer_test.go rename to gateways/client/transformer_test.go index e0bd2f761d..843dc0b37c 100644 --- a/gateways/transformer_test.go +++ b/gateways/client/transformer_test.go @@ -14,14 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package main import ( "fmt" + "testing" + + "github.com/argoproj/argo-events/gateways" "github.com/argoproj/argo-events/pkg/apis/common" "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/mock" - "testing" ) type MyMockedObject struct { @@ -38,8 +40,8 @@ func (m *MyMockedObject) dispatchEventOverNats(source string, eventPayload []byt func TestDispatchEvent(t *testing.T) { convey.Convey("Given an event, dispatch it to sensor", t, func() { - gc := getGatewayConfig() - event := &Event{ + gc := getGatewayContext() + event := &gateways.Event{ Name: "fake", Payload: []byte("fake"), } @@ -50,17 +52,17 @@ func TestDispatchEvent(t *testing.T) { err := gc.DispatchEvent(event) convey.So(err, convey.ShouldBeNil) - gc.gw.Spec.EventProtocol.Type = common.NATS + gc.gateway.Spec.EventProtocol.Type = common.NATS err = gc.DispatchEvent(event) convey.So(err, convey.ShouldBeNil) - gc.gw.Spec.EventProtocol.Type = common.NATS + gc.gateway.Spec.EventProtocol.Type = common.NATS err = gc.DispatchEvent(event) convey.So(err, convey.ShouldBeNil) - gc.gw.Spec.EventProtocol.Type = common.EventProtocolType("fake") + gc.gateway.Spec.EventProtocol.Type = common.EventProtocolType("fake") err = gc.DispatchEvent(event) convey.So(err, convey.ShouldNotBeNil) }) @@ -68,13 +70,13 @@ func TestDispatchEvent(t *testing.T) { func TestTransformEvent(t *testing.T) { convey.Convey("Given a gateway event, convert it into cloud event", t, func() { - gc := getGatewayConfig() - ce, err := gc.transformEvent(&Event{ + gc := getGatewayContext() + ce, err := gc.transformEvent(&gateways.Event{ Name: "fake", Payload: []byte("fake"), }) convey.So(err, convey.ShouldBeNil) convey.So(ce, convey.ShouldNotBeNil) - convey.So(ce.Context.Source.Host, convey.ShouldEqual, fmt.Sprintf("%s:%s", gc.gw.Name, "fake")) + convey.So(ce.Context.Source.Host, convey.ShouldEqual, fmt.Sprintf("%s:%s", gc.gateway.Name, "fake")) }) } diff --git a/gateways/watcher.go b/gateways/client/watcher.go similarity index 54% rename from gateways/watcher.go rename to gateways/client/watcher.go index 253c8c0b27..0b3bfba35a 100644 --- a/gateways/watcher.go +++ b/gateways/client/watcher.go @@ -14,16 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package main import ( "context" "fmt" - "github.com/argoproj/argo-events/common" - "strings" + "github.com/argoproj/argo-events/common" + eventSourceV1Alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -31,37 +30,29 @@ import ( "k8s.io/client-go/tools/cache" ) -// WatchGatewayEventSources watches change in configuration for the gateway -func (gc *GatewayConfig) WatchGatewayEventSources(ctx context.Context) (cache.Controller, error) { - source := gc.newConfigMapWatch(gc.configName) +// WatchGatewayEventSources watches change in event source for the gateway +func (gatewayContext *GatewayContext) WatchGatewayEventSources(ctx context.Context) (cache.Controller, error) { + source := gatewayContext.newEventSourceWatch(gatewayContext.eventSourceRef) _, controller := cache.NewInformer( source, - &corev1.ConfigMap{}, + &eventSourceV1Alpha1.EventSource{}, 0, cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { - if newCm, ok := obj.(*corev1.ConfigMap); ok { - if err := common.CheckEventSourceVersion(newCm); err != nil { - gc.Log.WithField("name", newCm.Name).Error(err) - } else { - gc.Log.WithField("name", newCm.Name).Info("detected configmap addition") - err := gc.manageEventSources(newCm) - if err != nil { - gc.Log.WithError(err).Error("add config failed") - } + if newEventSource, ok := obj.(*eventSourceV1Alpha1.EventSource); ok { + gatewayContext.logger.WithField(common.LabelEventSource, newEventSource.Name).Infoln("detected a new event-source...") + err := gatewayContext.syncEventSources(newEventSource) + if err != nil { + gatewayContext.logger.WithField(common.LabelEventSource, newEventSource.Name).WithError(err).Errorln("failed to process the event-source reference") } } }, UpdateFunc: func(old, new interface{}) { - if cm, ok := new.(*corev1.ConfigMap); ok { - if err := common.CheckEventSourceVersion(cm); err != nil { - gc.Log.WithField("name", cm.Name).Error(err) - } else { - gc.Log.Info("detected EventSource update. Updating the controller run config.") - err := gc.manageEventSources(cm) - if err != nil { - gc.Log.WithError(err).Error("update config failed") - } + if eventSource, ok := new.(*eventSourceV1Alpha1.EventSource); ok { + gatewayContext.logger.WithField(common.LabelEventSource, eventSource.Name).Info("detected event-source update...") + err := gatewayContext.syncEventSources(eventSource) + if err != nil { + gatewayContext.logger.WithField(common.LabelEventSource, eventSource.Name).WithError(err).Error("failed to process event source update") } } }, @@ -71,22 +62,21 @@ func (gc *GatewayConfig) WatchGatewayEventSources(ctx context.Context) (cache.Co return controller, nil } -// newConfigMapWatch creates a new configmap watcher -func (gc *GatewayConfig) newConfigMapWatch(name string) *cache.ListWatch { - x := gc.Clientset.CoreV1().RESTClient() - resource := "configmaps" - namespace := gc.Namespace - if strings.Contains(name, "/") { - parts := strings.SplitN(name, "/", 2) - namespace = parts[0] - name = parts[1] +// newEventSourceWatch creates a new event source watcher +func (gatewayContext *GatewayContext) newEventSourceWatch(eventSourceRef *v1alpha1.EventSourceRef) *cache.ListWatch { + client := gatewayContext.eventSourceClient.ArgoprojV1alpha1().RESTClient() + resource := "eventsources" + + if eventSourceRef.Namespace == "" { + eventSourceRef.Namespace = gatewayContext.namespace } - fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", name)) + + fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", eventSourceRef.Name)) listFunc := func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector.String() - req := x.Get(). - Namespace(namespace). + req := client.Get(). + Namespace(eventSourceRef.Namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec) return req.Do().Get() @@ -94,8 +84,8 @@ func (gc *GatewayConfig) newConfigMapWatch(name string) *cache.ListWatch { watchFunc := func(options metav1.ListOptions) (watch.Interface, error) { options.Watch = true options.FieldSelector = fieldSelector.String() - req := x.Get(). - Namespace(namespace). + req := client.Get(). + Namespace(eventSourceRef.Namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec) return req.Watch() @@ -103,10 +93,9 @@ func (gc *GatewayConfig) newConfigMapWatch(name string) *cache.ListWatch { return &cache.ListWatch{ListFunc: listFunc, WatchFunc: watchFunc} } -// WatchGateway watches for changes in the gateway resource -// This will act as replacement for old gateway-transformer-configmap. Changes to watchers, event version and event type will be reflected. -func (gc *GatewayConfig) WatchGateway(ctx context.Context) (cache.Controller, error) { - source := gc.newGatewayWatch(gc.Name) +// WatchGatewayUpdates watches for changes in the gateway resource +func (gatewayContext *GatewayContext) WatchGatewayUpdates(ctx context.Context) (cache.Controller, error) { + source := gatewayContext.newGatewayWatch(gatewayContext.name) _, controller := cache.NewInformer( source, &v1alpha1.Gateway{}, @@ -114,10 +103,10 @@ func (gc *GatewayConfig) WatchGateway(ctx context.Context) (cache.Controller, er cache.ResourceEventHandlerFuncs{ UpdateFunc: func(old, new interface{}) { if g, ok := new.(*v1alpha1.Gateway); ok { - gc.Log.Info("detected gateway update. updating gateway watchers") - gc.StatusCh <- EventSourceStatus{ + gatewayContext.logger.Info("detected gateway update. updating gateway watchers") + gatewayContext.statusCh <- EventSourceStatus{ Phase: v1alpha1.NodePhaseResourceUpdate, - Gw: g, + Gateway: g, Message: "gateway_resource_update", } } @@ -129,15 +118,15 @@ func (gc *GatewayConfig) WatchGateway(ctx context.Context) (cache.Controller, er } // newGatewayWatch creates a new gateway watcher -func (gc *GatewayConfig) newGatewayWatch(name string) *cache.ListWatch { - x := gc.gwcs.ArgoprojV1alpha1().RESTClient() +func (gatewayContext *GatewayContext) newGatewayWatch(name string) *cache.ListWatch { + x := gatewayContext.gatewayClient.ArgoprojV1alpha1().RESTClient() resource := "gateways" fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", name)) listFunc := func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector.String() req := x.Get(). - Namespace(gc.Namespace). + Namespace(gatewayContext.namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec) return req.Do().Get() @@ -146,7 +135,7 @@ func (gc *GatewayConfig) newGatewayWatch(name string) *cache.ListWatch { options.Watch = true options.FieldSelector = fieldSelector.String() req := x.Get(). - Namespace(gc.Namespace). + Namespace(gatewayContext.namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec) return req.Watch() diff --git a/gateways/common.go b/gateways/common.go new file mode 100644 index 0000000000..2b39dd84f4 --- /dev/null +++ b/gateways/common.go @@ -0,0 +1,14 @@ +package gateways + +// Gateway constants +const ( + // LabelEventSourceName is the label for a event source in gateway + LabelEventSourceName = "event-source-name" + // LabelEventSourceID is the label for gateway configuration ID + LabelEventSourceID = "event-source-id" + EnvVarGatewayServerPort = "GATEWAY_SERVER_PORT" + // Server Connection Timeout, 10 seconds + ServerConnTimeout = 10 + // EventSourceDir + EventSourceDir = "../../../examples/event-sources" +) diff --git a/gateways/common/fake.go b/gateways/common/fake.go deleted file mode 100644 index e0f0db6aba..0000000000 --- a/gateways/common/fake.go +++ /dev/null @@ -1,100 +0,0 @@ -package common - -import ( - "context" - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "google.golang.org/grpc/metadata" - "net/http" -) - -var Hook = &Webhook{ - Endpoint: "/fake", - Port: "12000", - URL: "test-url", -} - -type FakeHttpWriter struct { - HeaderStatus int - Payload []byte -} - -func (f *FakeHttpWriter) Header() http.Header { - return http.Header{} -} - -func (f *FakeHttpWriter) Write(body []byte) (int, error) { - f.Payload = body - return len(body), nil -} - -func (f *FakeHttpWriter) WriteHeader(status int) { - f.HeaderStatus = status -} - -type FakeRouteConfig struct { - route *Route -} - -func (f *FakeRouteConfig) GetRoute() *Route { - return f.route -} - -func (f *FakeRouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { -} - -func (f *FakeRouteConfig) PostStart() error { - return nil -} - -func (f *FakeRouteConfig) PostStop() error { - return nil -} - -func GetFakeRoute() *Route { - logger := common.NewArgoEventsLogger() - return &Route{ - Webhook: Hook, - EventSource: &gateways.EventSource{ - Name: "fake-event-source", - Data: "hello", - Id: "123", - }, - Logger: logger, - StartCh: make(chan struct{}), - } -} - -type FakeGRPCStream struct { - SentData *gateways.Event - Ctx context.Context -} - -func (f *FakeGRPCStream) Send(event *gateways.Event) error { - f.SentData = event - return nil -} - -func (f *FakeGRPCStream) SetHeader(metadata.MD) error { - return nil -} - -func (f *FakeGRPCStream) SendHeader(metadata.MD) error { - return nil -} - -func (f *FakeGRPCStream) SetTrailer(metadata.MD) { - return -} - -func (f *FakeGRPCStream) Context() context.Context { - return f.Ctx -} - -func (f *FakeGRPCStream) SendMsg(m interface{}) error { - return nil -} - -func (f *FakeGRPCStream) RecvMsg(m interface{}) error { - return nil -} diff --git a/gateways/common/validate.go b/gateways/common/validate.go deleted file mode 100644 index feb662c40a..0000000000 --- a/gateways/common/validate.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package common - -import ( - "fmt" - "github.com/argoproj/argo-events/gateways" -) - -const EventSourceDir = "../../../examples/event-sources" - -var ( - ErrNilEventSource = fmt.Errorf("event source can't be nil") -) - -func ValidateGatewayEventSource(eventSource *gateways.EventSource, version string, parseEventSource func(string) (interface{}, error), validateEventSource func(interface{}) error) (*gateways.ValidEventSource, error) { - v := &gateways.ValidEventSource{} - if eventSource.Version != version { - v.Reason = fmt.Sprintf("event source version mismatch. gateway expects %s version, and provided version is %s", version, eventSource.Version) - return v, nil - } - es, err := parseEventSource(eventSource.Data) - if err != nil { - v.Reason = fmt.Sprintf("failed to parse event source. err: %+v", err) - return v, nil - } - if err := validateEventSource(es); err != nil { - v.Reason = fmt.Sprintf("failed to validate event source. err: %+v", err) - return v, nil - } - v.IsValid = true - return v, nil -} diff --git a/gateways/common/webhook.go b/gateways/common/webhook.go deleted file mode 100644 index c59e26a752..0000000000 --- a/gateways/common/webhook.go +++ /dev/null @@ -1,314 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package common - -import ( - "fmt" - "github.com/argoproj/argo-events/common" - "github.com/sirupsen/logrus" - "net/http" - "strconv" - "strings" - "sync" - - "github.com/argoproj/argo-events/gateways" -) - -// Webhook is a general purpose REST API -type Webhook struct { - // REST API endpoint - Endpoint string `json:"endpoint" protobuf:"bytes,1,name=endpoint"` - // Method is HTTP request method that indicates the desired action to be performed for a given resource. - // See RFC7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content - Method string `json:"method" protobuf:"bytes,2,name=method"` - // Port on which HTTP server is listening for incoming events. - Port string `json:"port" protobuf:"bytes,3,name=port"` - // URL is the url of the server. - URL string `json:"url" protobuf:"bytes,4,name=url"` - // ServerCertPath refers the file that contains the cert. - ServerCertPath string `json:"serverCertPath,omitempty" protobuf:"bytes,4,opt,name=serverCertPath"` - // ServerKeyPath refers the file that contains private key - ServerKeyPath string `json:"serverKeyPath,omitempty" protobuf:"bytes,5,opt,name=serverKeyPath"` - - // srv holds reference to http server - srv *http.Server - mux *http.ServeMux -} - -// WebhookHelper is a helper struct -type WebhookHelper struct { - // Mutex synchronizes ActiveServers - Mutex sync.Mutex - // ActiveServers keeps track of currently running http servers. - ActiveServers map[string]*activeServer - // ActiveEndpoints keep track of endpoints that are already registered with server and their status active or inactive - ActiveEndpoints map[string]*Endpoint - // RouteActivateChan handles assigning new route to server. - RouteActivateChan chan RouteManager - // RouteDeactivateChan handles deactivating existing route - RouteDeactivateChan chan RouteManager -} - -// HTTP Muxer -type server struct { - mux *http.ServeMux -} - -// activeServer contains reference to server and an error channel that is shared across all functions registering endpoints for the server. -type activeServer struct { - srv *http.ServeMux - errChan chan error -} - -// Route contains common information for a route -type Route struct { - Webhook *Webhook - Logger *logrus.Logger - StartCh chan struct{} - EventSource *gateways.EventSource -} - -// RouteManager is an interface to manage the configuration for a route -type RouteManager interface { - GetRoute() *Route - RouteHandler(writer http.ResponseWriter, request *http.Request) - PostStart() error - PostStop() error -} - -// endpoint contains state of an http endpoint -type Endpoint struct { - // whether endpoint is active - Active bool - // data channel to receive data on this endpoint - DataCh chan []byte -} - -// NewWebhookHelper returns new Webhook helper -func NewWebhookHelper() *WebhookHelper { - return &WebhookHelper{ - ActiveEndpoints: make(map[string]*Endpoint), - ActiveServers: make(map[string]*activeServer), - Mutex: sync.Mutex{}, - RouteActivateChan: make(chan RouteManager), - RouteDeactivateChan: make(chan RouteManager), - } -} - -// InitRouteChannels initializes route channels so they can activate and deactivate routes. -func InitRouteChannels(helper *WebhookHelper) { - for { - select { - case config := <-helper.RouteActivateChan: - // start server if it has not been started on this port - startHttpServer(config, helper) - startCh := config.GetRoute().StartCh - startCh <- struct{}{} - - case config := <-helper.RouteDeactivateChan: - webhook := config.GetRoute().Webhook - _, ok := helper.ActiveServers[webhook.Port] - if ok { - helper.ActiveEndpoints[webhook.Endpoint].Active = false - } - } - } -} - -// ServeHTTP implementation -func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.mux.ServeHTTP(w, r) -} - -// starts a http server -func startHttpServer(routeManager RouteManager, helper *WebhookHelper) { - // start a http server only if no other configuration previously started the server on given port - helper.Mutex.Lock() - r := routeManager.GetRoute() - if _, ok := helper.ActiveServers[r.Webhook.Port]; !ok { - s := &server{ - mux: http.NewServeMux(), - } - r.Webhook.mux = s.mux - r.Webhook.srv = &http.Server{ - Addr: ":" + fmt.Sprintf("%s", r.Webhook.Port), - Handler: s, - } - errChan := make(chan error, 1) - helper.ActiveServers[r.Webhook.Port] = &activeServer{ - srv: s.mux, - errChan: errChan, - } - - // start http server - go func() { - var err error - if r.Webhook.ServerCertPath == "" || r.Webhook.ServerKeyPath == "" { - err = r.Webhook.srv.ListenAndServe() - } else { - err = r.Webhook.srv.ListenAndServeTLS(r.Webhook.ServerCertPath, r.Webhook.ServerKeyPath) - } - r.Logger.WithField(common.LabelEventSource, r.EventSource.Name).WithError(err).Error("http server stopped") - if err != nil { - errChan <- err - } - }() - } - helper.Mutex.Unlock() -} - -// activateRoute activates route -func activateRoute(routeManager RouteManager, helper *WebhookHelper) { - r := routeManager.GetRoute() - helper.RouteActivateChan <- routeManager - - <-r.StartCh - - if r.Webhook.mux == nil { - helper.Mutex.Lock() - r.Webhook.mux = helper.ActiveServers[r.Webhook.Port].srv - helper.Mutex.Unlock() - } - - log := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelPort: r.Webhook.Port, - common.LabelEndpoint: r.Webhook.Endpoint, - }) - - log.Info("adding route handler") - if _, ok := helper.ActiveEndpoints[r.Webhook.Endpoint]; !ok { - helper.ActiveEndpoints[r.Webhook.Endpoint] = &Endpoint{ - Active: true, - DataCh: make(chan []byte), - } - r.Webhook.mux.HandleFunc(r.Webhook.Endpoint, routeManager.RouteHandler) - } - helper.ActiveEndpoints[r.Webhook.Endpoint].Active = true - - log.Info("route handler added") -} - -func processChannels(routeManager RouteManager, helper *WebhookHelper, eventStream gateways.Eventing_StartEventSourceServer) error { - r := routeManager.GetRoute() - - for { - select { - case data := <-helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh: - r.Logger.WithField(common.LabelEventSource, r.EventSource.Name).Info("new event received, dispatching to gateway client") - err := eventStream.Send(&gateways.Event{ - Name: r.EventSource.Name, - Payload: data, - }) - if err != nil { - r.Logger.WithField(common.LabelEventSource, r.EventSource.Name).WithError(err).Error("failed to send event") - return err - } - - case <-eventStream.Context().Done(): - r.Logger.WithField(common.LabelEventSource, r.EventSource.Name).Info("connection is closed by client") - helper.RouteDeactivateChan <- routeManager - return nil - - // this error indicates that the server has stopped running - case err := <-helper.ActiveServers[r.Webhook.Port].errChan: - return err - } - } -} - -func ProcessRoute(routeManager RouteManager, helper *WebhookHelper, eventStream gateways.Eventing_StartEventSourceServer) error { - r := routeManager.GetRoute() - log := r.Logger.WithField(common.LabelEventSource, r.EventSource.Name) - - log.Info("validating the route") - if err := validateRoute(routeManager.GetRoute()); err != nil { - log.WithError(err).Error("error occurred validating route") - return err - } - - log.Info("activating the route") - activateRoute(routeManager, helper) - - log.Info("running post start") - if err := routeManager.PostStart(); err != nil { - log.WithError(err).Error("error occurred in post start") - return err - } - - log.Info("processing channels") - if err := processChannels(routeManager, helper, eventStream); err != nil { - log.WithError(err).Error("error occurred in process channel") - return err - } - - log.Info("running post stop") - if err := routeManager.PostStop(); err != nil { - log.WithError(err).Error("error occurred in post stop") - } - return nil -} - -func ValidateWebhook(w *Webhook) error { - if w == nil { - return fmt.Errorf("") - } - if w.Endpoint == "" { - return fmt.Errorf("endpoint can't be empty") - } - if w.Port == "" { - return fmt.Errorf("port can't be empty") - } - if w.Port != "" { - _, err := strconv.Atoi(w.Port) - if err != nil { - return fmt.Errorf("failed to parse server port %s. err: %+v", w.Port, err) - } - } - return nil -} - -func validateRoute(r *Route) error { - if r == nil { - return fmt.Errorf("route can't be nil") - } - if r.Webhook == nil { - return fmt.Errorf("webhook can't be nil") - } - if r.StartCh == nil { - return fmt.Errorf("start channel can't be nil") - } - if r.EventSource == nil { - return fmt.Errorf("event source can't be nil") - } - if r.Logger == nil { - return fmt.Errorf("logger can't be nil") - } - return nil -} - -func FormatWebhookEndpoint(endpoint string) string { - if !strings.HasPrefix(endpoint, "/") { - return fmt.Sprintf("/%s", endpoint) - } - return endpoint -} - -func GenerateFormattedURL(w *Webhook) string { - return fmt.Sprintf("%s%s", w.URL, FormatWebhookEndpoint(w.Endpoint)) -} diff --git a/gateways/common/webhook_test.go b/gateways/common/webhook_test.go deleted file mode 100644 index 931a0c86b6..0000000000 --- a/gateways/common/webhook_test.go +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package common - -import ( - "context" - "fmt" - "net/http" - "testing" - "time" - - "github.com/smartystreets/goconvey/convey" -) - -var rc = &FakeRouteConfig{ - route: GetFakeRoute(), -} - -func TestProcessRoute(t *testing.T) { - convey.Convey("Given a route configuration", t, func() { - convey.Convey("Activate the route configuration", func() { - - rc.route.Webhook.mux = http.NewServeMux() - - ctx, cancel := context.WithCancel(context.Background()) - fgs := &FakeGRPCStream{ - Ctx: ctx, - } - - helper := NewWebhookHelper() - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &Endpoint{ - DataCh: make(chan []byte), - } - helper.ActiveServers[rc.route.Webhook.Port] = &activeServer{ - errChan: make(chan error), - } - - errCh := make(chan error) - go func() { - <-helper.RouteDeactivateChan - }() - - go func() { - <-helper.RouteActivateChan - }() - go func() { - rc.route.StartCh <- struct{}{} - }() - go func() { - time.Sleep(3 * time.Second) - cancel() - }() - - go func() { - errCh <- ProcessRoute(rc, helper, fgs) - }() - - err := <-errCh - convey.So(err, convey.ShouldBeNil) - }) - }) -} - -func TestProcessRouteChannels(t *testing.T) { - convey.Convey("Given a route configuration", t, func() { - convey.Convey("Stop server stream", func() { - ctx, cancel := context.WithCancel(context.Background()) - fgs := &FakeGRPCStream{ - Ctx: ctx, - } - helper := NewWebhookHelper() - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &Endpoint{ - DataCh: make(chan []byte), - } - helper.ActiveServers[rc.route.Webhook.Port] = &activeServer{ - errChan: make(chan error), - } - errCh := make(chan error) - go func() { - <-helper.RouteDeactivateChan - }() - go func() { - errCh <- processChannels(rc, helper, fgs) - }() - cancel() - err := <-errCh - convey.So(err, convey.ShouldBeNil) - }) - convey.Convey("Handle error", func() { - fgs := &FakeGRPCStream{ - Ctx: context.Background(), - } - helper := NewWebhookHelper() - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &Endpoint{ - DataCh: make(chan []byte), - } - helper.ActiveServers[rc.route.Webhook.Port] = &activeServer{ - errChan: make(chan error), - } - errCh := make(chan error) - err := fmt.Errorf("error") - go func() { - helper.ActiveServers[rc.route.Webhook.Port].errChan <- err - }() - go func() { - errCh <- processChannels(rc, helper, fgs) - }() - newErr := <-errCh - convey.So(newErr.Error(), convey.ShouldEqual, err.Error()) - }) - }) -} - -func TestFormatWebhookEndpoint(t *testing.T) { - convey.Convey("Given a webhook endpoint, format it", t, func() { - convey.So(FormatWebhookEndpoint("hello"), convey.ShouldEqual, "/hello") - }) -} - -func TestValidateWebhook(t *testing.T) { - convey.Convey("Given a webhook, validate it", t, func() { - convey.So(ValidateWebhook(Hook), convey.ShouldBeNil) - }) -} - -func TestGenerateFormattedURL(t *testing.T) { - convey.Convey("Given a webhook, generate formatted URL", t, func() { - convey.So(GenerateFormattedURL(Hook), convey.ShouldEqual, "test-url/fake") - }) -} - -func TestNewWebhookHelper(t *testing.T) { - convey.Convey("Make sure webhook helper is not empty", t, func() { - helper := NewWebhookHelper() - convey.So(helper, convey.ShouldNotBeNil) - }) -} diff --git a/gateways/community/aws-sns/config_test.go b/gateways/community/aws-sns/config_test.go deleted file mode 100644 index 8e6a810b60..0000000000 --- a/gateways/community/aws-sns/config_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sns - -import ( - "testing" - - "github.com/smartystreets/goconvey/convey" -) - -var es = ` -hook: - endpoint: "/test" - port: "8080" - url: "myurl/test" -topicArn: "test-arn" -region: "us-east-1" -accessKey: - key: accesskey - name: sns -secretKey: - key: secretkey - name: sns -` - -var esWithoutCreds = ` -hook: - endpoint: "/test" - port: "8080" - url: "myurl/test" -topicArn: "test-arn" -region: "us-east-1" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a aws-sns event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*snsEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) - - convey.Convey("Given a aws-sns event source without credentials, parse it", t, func() { - ps, err := parseEventSource(esWithoutCreds) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*snsEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/community/aws-sns/start.go b/gateways/community/aws-sns/start.go deleted file mode 100644 index 5b15221901..0000000000 --- a/gateways/community/aws-sns/start.go +++ /dev/null @@ -1,188 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sns - -import ( - "fmt" - "io/ioutil" - "net/http" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/aws/aws-sdk-go/aws/session" - snslib "github.com/aws/aws-sdk-go/service/sns" - "github.com/ghodss/yaml" -) - -var ( - helper = gwcommon.NewWebhookHelper() -) - -func init() { - go gwcommon.InitRouteChannels(helper) -} - -// GetRoute returns the route -func (rc *RouteConfig) GetRoute() *gwcommon.Route { - return rc.Route -} - -// RouteHandler handles new routes -func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { - r := rc.Route - - logger := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - common.LabelHTTPMethod: r.Webhook.Method, - }) - - logger.Info("request received") - - if !helper.ActiveEndpoints[r.Webhook.Endpoint].Active { - logger.Info("endpoint is not active") - common.SendErrorResponse(writer, "") - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - logger.WithError(err).Error("failed to parse request body") - common.SendErrorResponse(writer, "") - return - } - - var snspayload *httpNotification - err = yaml.Unmarshal(body, &snspayload) - if err != nil { - logger.WithError(err).Error("failed to convert request payload into sns event source payload") - common.SendErrorResponse(writer, "") - return - } - - switch snspayload.Type { - case messageTypeSubscriptionConfirmation: - awsSession := rc.session - out, err := awsSession.ConfirmSubscription(&snslib.ConfirmSubscriptionInput{ - TopicArn: &rc.snses.TopicArn, - Token: &snspayload.Token, - }) - if err != nil { - logger.WithError(err).Error("failed to send confirmation response to amazon") - common.SendErrorResponse(writer, "") - return - } - rc.subscriptionArn = out.SubscriptionArn - - case messageTypeNotification: - helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh <- body - } - - logger.Info("request successfully processed") -} - -// PostStart subscribes to the sns topic -func (rc *RouteConfig) PostStart() error { - r := rc.Route - - logger := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - common.LabelHTTPMethod: r.Webhook.Method, - "topic-arn": rc.snses.TopicArn, - }) - - logger.Info("subscribing to sns topic") - - sc := rc.snses - var awsSession *session.Session - - if sc.AccessKey == nil && sc.SecretKey == nil { - awsSessionWithoutCreds, err := gwcommon.GetAWSSessionWithoutCreds(sc.Region) - if err != nil { - return fmt.Errorf("failed to create aws session. err: %+v", err) - } - - awsSession = awsSessionWithoutCreds - } else { - creds, err := gwcommon.GetAWSCreds(rc.clientset, rc.namespace, sc.AccessKey, sc.SecretKey) - if err != nil { - return fmt.Errorf("failed to create aws session. err: %+v", err) - } - - awsSessionWithCreds, err := gwcommon.GetAWSSession(creds, sc.Region) - if err != nil { - return fmt.Errorf("failed to create aws session. err: %+v", err) - } - - awsSession = awsSessionWithCreds - } - - rc.session = snslib.New(awsSession) - formattedUrl := gwcommon.GenerateFormattedURL(sc.Hook) - if _, err := rc.session.Subscribe(&snslib.SubscribeInput{ - Endpoint: &formattedUrl, - Protocol: &snsProtocol, - TopicArn: &sc.TopicArn, - }); err != nil { - return fmt.Errorf("failed to send subscribe request. err: %+v", err) - } - - return nil -} - -// PostStop unsubscribes from the sns topic -func (rc *RouteConfig) PostStop() error { - if _, err := rc.session.Unsubscribe(&snslib.UnsubscribeInput{ - SubscriptionArn: rc.subscriptionArn, - }); err != nil { - return fmt.Errorf("failed to unsubscribe. err: %+v", err) - } - return nil -} - -// StartEventSource starts an SNS event source -func (ese *SNSEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("operating on event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - sc := config.(*snsEventSource) - - return gwcommon.ProcessRoute(&RouteConfig{ - Route: &gwcommon.Route{ - Logger: ese.Log, - EventSource: eventSource, - StartCh: make(chan struct{}), - Webhook: sc.Hook, - }, - snses: sc, - namespace: ese.Namespace, - clientset: ese.Clientset, - }, helper, eventStream) -} diff --git a/gateways/community/aws-sns/start_test.go b/gateways/community/aws-sns/start_test.go deleted file mode 100644 index c6eb1b2bea..0000000000 --- a/gateways/community/aws-sns/start_test.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sns - -import ( - "bytes" - "io/ioutil" - "net/http" - "testing" - - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/aws/aws-sdk-go/aws/credentials" - snslib "github.com/aws/aws-sdk-go/service/sns" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - "k8s.io/client-go/kubernetes/fake" -) - -func TestAWSSNS(t *testing.T) { - convey.Convey("Given an route configuration", t, func() { - rc := &RouteConfig{ - Route: gwcommon.GetFakeRoute(), - namespace: "fake", - clientset: fake.NewSimpleClientset(), - } - r := rc.Route - - helper.ActiveEndpoints[r.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } - writer := &gwcommon.FakeHttpWriter{} - subscriptionArn := "arn://fake" - awsSession, err := gwcommon.GetAWSSession(credentials.NewStaticCredentialsFromCreds(credentials.Value{ - AccessKeyID: "access", - SecretAccessKey: "secret", - }), "mock-region") - - convey.So(err, convey.ShouldBeNil) - - snsSession := snslib.New(awsSession) - rc.session = snsSession - rc.subscriptionArn = &subscriptionArn - - convey.Convey("handle the inactive route", func() { - rc.RouteHandler(writer, &http.Request{}) - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) - }) - - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - - helper.ActiveEndpoints[r.Webhook.Endpoint].Active = true - rc.snses = ps.(*snsEventSource) - - convey.Convey("handle the active route", func() { - payload := httpNotification{ - TopicArn: "arn://fake", - Token: "faketoken", - Type: messageTypeSubscriptionConfirmation, - } - - payloadBytes, err := yaml.Marshal(payload) - convey.So(err, convey.ShouldBeNil) - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewBuffer(payloadBytes)), - }) - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) - - dataCh := make(chan []byte) - - go func() { - data := <-helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh - dataCh <- data - }() - - payload.Type = messageTypeNotification - payloadBytes, err = yaml.Marshal(payload) - convey.So(err, convey.ShouldBeNil) - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewBuffer(payloadBytes)), - }) - data := <-dataCh - convey.So(data, convey.ShouldNotBeNil) - }) - - convey.Convey("Run post activate", func() { - err := rc.PostStart() - convey.So(err, convey.ShouldNotBeNil) - }) - - convey.Convey("Run post stop", func() { - err = rc.PostStop() - convey.So(err, convey.ShouldNotBeNil) - }) - - psWithoutCreds, err2 := parseEventSource(esWithoutCreds) - convey.So(err2, convey.ShouldBeNil) - - rc.snses = psWithoutCreds.(*snsEventSource) - - convey.Convey("Run post activate on event source without credentials", func() { - err := rc.PostStart() - convey.So(err, convey.ShouldNotBeNil) - }) - }) -} diff --git a/gateways/community/aws-sns/validate.go b/gateways/community/aws-sns/validate.go deleted file mode 100644 index 5e70c66e19..0000000000 --- a/gateways/community/aws-sns/validate.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sns - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *SNSEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateSNSConfig) -} - -func validateSNSConfig(config interface{}) error { - sc := config.(*snsEventSource) - if sc == nil { - return gwcommon.ErrNilEventSource - } - if sc.TopicArn == "" { - return fmt.Errorf("must specify topic arn") - } - if sc.Region == "" { - return fmt.Errorf("must specify region") - } - return gwcommon.ValidateWebhook(sc.Hook) -} diff --git a/gateways/community/aws-sns/validate_test.go b/gateways/community/aws-sns/validate_test.go deleted file mode 100644 index 4809bb2b6d..0000000000 --- a/gateways/community/aws-sns/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sns - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestSNSEventSourceExecutor_ValidateEventSource(t *testing.T) { - convey.Convey("Given sns event source spec, parse it and make sure no error occurs", t, func() { - ese := &SNSEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "aws-sns.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/aws-sqs/config.go b/gateways/community/aws-sqs/config.go deleted file mode 100644 index 7d48ad6bc9..0000000000 --- a/gateways/community/aws-sqs/config.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sqs - -import ( - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// SQSEventSourceExecutor implements Eventing -type SQSEventSourceExecutor struct { - Log *logrus.Logger - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string -} - -// sqsEventSource contains information to listen to AWS SQS -type sqsEventSource struct { - // AccessKey refers K8 secret containing aws access key - AccessKey *corev1.SecretKeySelector `json:"accessKey"` - - // SecretKey refers K8 secret containing aws secret key - SecretKey *corev1.SecretKeySelector `json:"secretKey"` - - // Region is AWS region - Region string `json:"region"` - - // Queue is AWS SQS queue to listen to for messages - Queue string `json:"queue"` - - // WaitTimeSeconds is The duration (in seconds) for which the call waits for a message to arrive - // in the queue before returning. - WaitTimeSeconds int64 `json:"waitTimeSeconds"` -} - -func parseEventSource(es string) (interface{}, error) { - var n *sqsEventSource - err := yaml.Unmarshal([]byte(es), &n) - if err != nil { - return nil, err - } - return n, nil -} diff --git a/gateways/community/aws-sqs/config_test.go b/gateways/community/aws-sqs/config_test.go deleted file mode 100644 index 99bac49c6a..0000000000 --- a/gateways/community/aws-sqs/config_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sqs - -import ( - "testing" - - "github.com/smartystreets/goconvey/convey" -) - -var es = ` -region: "us-east-1" -accessKey: - key: accesskey - name: sns -secretKey: - key: secretkey - name: sns -queue: "test-queue" -waitTimeSeconds: 10 -` - -var esWithoutCreds = ` -region: "us-east-1" -queue: "test-queue" -waitTimeSeconds: 10 -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a aws-sqsEventSource event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*sqsEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) - - convey.Convey("Given a aws-sqsEventSource event source without AWS credentials, parse it", t, func() { - ps, err := parseEventSource(esWithoutCreds) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*sqsEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/community/aws-sqs/start.go b/gateways/community/aws-sqs/start.go deleted file mode 100644 index d2bb8015f9..0000000000 --- a/gateways/community/aws-sqs/start.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sqs - -import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - sqslib "github.com/aws/aws-sdk-go/service/sqs" -) - -// StartEventSource starts an event source -func (ese *SQSEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("activating event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*sqsEventSource), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -// listenEvents fires an event when interval completes and item is processed from queue. -func (ese *SQSEventSourceExecutor) listenEvents(s *sqsEventSource, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - var awsSession *session.Session - - if s.AccessKey == nil && s.SecretKey == nil { - awsSessionWithoutCreds, err := gwcommon.GetAWSSessionWithoutCreds(s.Region) - if err != nil { - errorCh <- err - return - } - - awsSession = awsSessionWithoutCreds - } else { - creds, err := gwcommon.GetAWSCreds(ese.Clientset, ese.Namespace, s.AccessKey, s.SecretKey) - if err != nil { - errorCh <- err - return - } - - awsSessionWithCreds, err := gwcommon.GetAWSSession(creds, s.Region) - if err != nil { - errorCh <- err - return - } - - awsSession = awsSessionWithCreds - } - - sqsClient := sqslib.New(awsSession) - - queueURL, err := sqsClient.GetQueueUrl(&sqslib.GetQueueUrlInput{ - QueueName: &s.Queue, - }) - if err != nil { - errorCh <- err - return - } - - for { - select { - case <-doneCh: - return - - default: - msg, err := sqsClient.ReceiveMessage(&sqslib.ReceiveMessageInput{ - QueueUrl: queueURL.QueueUrl, - MaxNumberOfMessages: aws.Int64(1), - WaitTimeSeconds: aws.Int64(s.WaitTimeSeconds), - }) - if err != nil { - ese.Log.WithField(common.LabelEventSource, eventSource.Name).WithError(err).Error("failed to process item from queue, waiting for next timeout") - continue - } - - if msg != nil && len(msg.Messages) > 0 { - dataCh <- []byte(*msg.Messages[0].Body) - - if _, err := sqsClient.DeleteMessage(&sqslib.DeleteMessageInput{ - QueueUrl: queueURL.QueueUrl, - ReceiptHandle: msg.Messages[0].ReceiptHandle, - }); err != nil { - errorCh <- err - return - } - } - } - } -} diff --git a/gateways/community/aws-sqs/start_test.go b/gateways/community/aws-sqs/start_test.go deleted file mode 100644 index 9d62dfbfbb..0000000000 --- a/gateways/community/aws-sqs/start_test.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sqs - -import ( - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/smartystreets/goconvey/convey" - "k8s.io/client-go/kubernetes/fake" -) - -func TestListenEvents(t *testing.T) { - convey.Convey("Given an event source, listen to events", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - - ese := &SQSEventSourceExecutor{ - Clientset: fake.NewSimpleClientset(), - Namespace: "fake", - Log: common.NewArgoEventsLogger(), - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - errorCh2 := make(chan error) - - go func() { - err := <-errorCh - errorCh2 <- err - }() - - ese.listenEvents(ps.(*sqsEventSource), &gateways.EventSource{ - Name: "fake", - Data: es, - Id: "1234", - }, dataCh, errorCh, doneCh) - - err = <-errorCh2 - convey.So(err, convey.ShouldNotBeNil) - }) - - convey.Convey("Given an event source without AWS credentials, listen to events", t, func() { - ps, err := parseEventSource(esWithoutCreds) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - - ese := &SQSEventSourceExecutor{ - Clientset: fake.NewSimpleClientset(), - Namespace: "fake", - Log: common.NewArgoEventsLogger(), - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - errorCh2 := make(chan error) - - go func() { - err := <-errorCh - errorCh2 <- err - }() - - ese.listenEvents(ps.(*sqsEventSource), &gateways.EventSource{ - Name: "fake", - Data: es, - Id: "1234", - }, dataCh, errorCh, doneCh) - - err = <-errorCh2 - convey.So(err, convey.ShouldNotBeNil) - }) -} diff --git a/gateways/community/aws-sqs/validate.go b/gateways/community/aws-sqs/validate.go deleted file mode 100644 index 3a4f0ab6ed..0000000000 --- a/gateways/community/aws-sqs/validate.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sqs - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *SQSEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateSQSConfig) -} - -func validateSQSConfig(config interface{}) error { - sc := config.(*sqsEventSource) - if sc == nil { - return gwcommon.ErrNilEventSource - } - if sc.WaitTimeSeconds == 0 { - return fmt.Errorf("must specify polling timeout") - } - if sc.Region == "" { - return fmt.Errorf("must specify region") - } - if sc.Queue == "" { - return fmt.Errorf("must specify queue name") - } - return nil -} diff --git a/gateways/community/aws-sqs/validate_test.go b/gateways/community/aws-sqs/validate_test.go deleted file mode 100644 index 2abeac0ea1..0000000000 --- a/gateways/community/aws-sqs/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws_sqs - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestSQSEventSourceExecutor_ValidateEventSource(t *testing.T) { - convey.Convey("Given a valid sqsEventSource event source spec, parse it and make sure no error occurs", t, func() { - ese := &SQSEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "aws-sqs.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/gcp-pubsub/config.go b/gateways/community/gcp-pubsub/config.go deleted file mode 100644 index ec24a06304..0000000000 --- a/gateways/community/gcp-pubsub/config.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package pubsub - -import ( - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// GcpPubSubEventSourceExecutor implements Eventing -type GcpPubSubEventSourceExecutor struct { - Log *logrus.Logger -} - -// pubSubEventSource contains configuration to subscribe to GCP PubSub topic -type pubSubEventSource struct { - // ProjectID is the unique identifier for your project on GCP - ProjectID string `json:"projectID"` - // TopicProjectID identifies the project where the topic should exist or be created - // (assumed to be the same as ProjectID by default) - TopicProjectID string `json:"topicProjectID"` - // Topic on which a subscription will be created - Topic string `json:"topic"` - // CredentialsFile is the file that contains credentials to authenticate for GCP - CredentialsFile string `json:"credentialsFile"` -} - -func parseEventSource(es string) (interface{}, error) { - var n *pubSubEventSource - err := yaml.Unmarshal([]byte(es), &n) - if err != nil { - return nil, err - } - return n, nil -} diff --git a/gateways/community/gcp-pubsub/start.go b/gateways/community/gcp-pubsub/start.go deleted file mode 100644 index dade593c8b..0000000000 --- a/gateways/community/gcp-pubsub/start.go +++ /dev/null @@ -1,128 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package pubsub - -import ( - "context" - "fmt" - - "cloud.google.com/go/pubsub" - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "google.golang.org/api/option" -) - -// StartEventSource starts the GCP PubSub Gateway -func (ese *GcpPubSubEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - ese.Log.Info("operating on event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Info("failed to parse event source") - return err - } - sc := config.(*pubSubEventSource) - - ctx := eventStream.Context() - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(ctx, sc, eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func (ese *GcpPubSubEventSourceExecutor) listenEvents(ctx context.Context, sc *pubSubEventSource, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - // Create a new topic with the given name if none exists - logger := ese.Log.WithField(common.LabelEventSource, eventSource.Name).WithField("topic", sc.Topic) - - client, err := pubsub.NewClient(ctx, sc.ProjectID, option.WithCredentialsFile(sc.CredentialsFile)) - if err != nil { - errorCh <- err - return - } - - topicClient := client // use same client for topic and subscription by default - if sc.TopicProjectID != "" && sc.TopicProjectID != sc.ProjectID { - topicClient, err = pubsub.NewClient(ctx, sc.TopicProjectID, option.WithCredentialsFile(sc.CredentialsFile)) - if err != nil { - errorCh <- err - return - } - } - - topic := topicClient.Topic(sc.Topic) - exists, err := topic.Exists(ctx) - if err != nil { - errorCh <- err - return - } - if !exists { - logger.Info("Creating GCP PubSub topic") - if _, err := topicClient.CreateTopic(ctx, sc.Topic); err != nil { - errorCh <- err - return - } - } - - logger.Info("Subscribing to GCP PubSub topic") - subscription_name := fmt.Sprintf("%s-%s", eventSource.Name, eventSource.Id) - subscription := client.Subscription(subscription_name) - exists, err = subscription.Exists(ctx) - - if err != nil { - errorCh <- err - return - } - if exists { - logger.Warn("Using an existing subscription") - } else { - logger.Info("Creating subscription") - if _, err := client.CreateSubscription(ctx, subscription_name, pubsub.SubscriptionConfig{Topic: topic}); err != nil { - errorCh <- err - return - } - } - - err = subscription.Receive(ctx, func(msgCtx context.Context, m *pubsub.Message) { - logger.Info("received GCP PubSub Message from topic") - dataCh <- m.Data - m.Ack() - }) - if err != nil { - errorCh <- err - return - } - - <-doneCh - - // after this point, panic on errors - logger.Info("deleting GCP PubSub subscription") - if err = subscription.Delete(context.Background()); err != nil { - panic(err) - } - - logger.Info("closing GCP PubSub client") - if err = client.Close(); err != nil { - panic(err) - } -} diff --git a/gateways/community/gcp-pubsub/start_test.go b/gateways/community/gcp-pubsub/start_test.go deleted file mode 100644 index 1781f5c3e8..0000000000 --- a/gateways/community/gcp-pubsub/start_test.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package pubsub - -import ( - "context" - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/smartystreets/goconvey/convey" - "testing" -) - -func TestListenEvents(t *testing.T) { - convey.Convey("Given a pubsub event source, listen to events", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - psc := ps.(*pubSubEventSource) - - ese := &GcpPubSubEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - errCh2 := make(chan error) - - go func() { - err := <-errorCh - errCh2 <- err - }() - - ese.listenEvents(context.Background(), psc, &gateways.EventSource{ - Name: "fake", - Data: es, - Id: "1234", - }, dataCh, errorCh, doneCh) - - err = <-errCh2 - convey.So(err, convey.ShouldNotBeNil) - }) -} diff --git a/gateways/community/gcp-pubsub/validate.go b/gateways/community/gcp-pubsub/validate.go deleted file mode 100644 index b8b57760d1..0000000000 --- a/gateways/community/gcp-pubsub/validate.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package pubsub - -import ( - "context" - "fmt" - "os" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *GcpPubSubEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validatePubSubConfig) -} - -func validatePubSubConfig(config interface{}) error { - sc := config.(*pubSubEventSource) - if sc == nil { - return gwcommon.ErrNilEventSource - } - if sc.ProjectID == "" { - return fmt.Errorf("must specify projectId") - } - if sc.Topic == "" { - return fmt.Errorf("must specify topic") - } - if sc.CredentialsFile != "" { - if _, err := os.Stat(sc.CredentialsFile); err != nil { - return err - } - } - return nil -} diff --git a/gateways/community/gcp-pubsub/validate_test.go b/gateways/community/gcp-pubsub/validate_test.go deleted file mode 100644 index 707b77752d..0000000000 --- a/gateways/community/gcp-pubsub/validate_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package pubsub - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestGcpPubSubEventSourceExecutor_ValidateEventSource(t *testing.T) { - convey.Convey("Given a valid gcp pub-sub event source spec, parse it and make sure no error occurs", t, func() { - ese := &GcpPubSubEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "gcp-pubsub.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.Println(valid.Reason) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/github/config.go b/gateways/community/github/config.go deleted file mode 100644 index 703a4da885..0000000000 --- a/gateways/community/github/config.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2018 KompiTech GmbH - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/google/go-github/github" - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// GithubEventSourceExecutor implements ConfigExecutor -type GithubEventSourceExecutor struct { - Log *logrus.Logger - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string -} - -// RouteConfig contains information about the route -type RouteConfig struct { - route *gwcommon.Route - ges *githubEventSource - client *github.Client - hook *github.Hook - clientset kubernetes.Interface - namespace string -} - -// githubEventSource contains information to setup a github project integration -type githubEventSource struct { - // Webhook ID - Id int64 `json:"id"` - // Webhook - Hook *gwcommon.Webhook `json:"hook"` - // GitHub owner name i.e. argoproj - Owner string `json:"owner"` - // GitHub repo name i.e. argo-events - Repository string `json:"repository"` - // Github events to subscribe to which the gateway will subscribe - Events []string `json:"events"` - // K8s secret containing github api token - APIToken *corev1.SecretKeySelector `json:"apiToken"` - // K8s secret containing WebHook Secret - WebHookSecret *corev1.SecretKeySelector `json:"webHookSecret"` - // Insecure tls verification - Insecure bool `json:"insecure"` - // Active - Active bool `json:"active"` - // ContentType json or form - ContentType string `json:"contentType"` - // GitHub base URL (for GitHub Enterprise) - GithubBaseURL string `json:"githubBaseURL"` - // GitHub upload URL (for GitHub Enterprise) - GithubUploadURL string `json:"githubUploadURL"` -} - -// cred stores the api access token or webhook secret -type cred struct { - secret string -} - -// parseEventSource parses a configuration of gateway -func parseEventSource(config string) (interface{}, error) { - var g *githubEventSource - err := yaml.Unmarshal([]byte(config), &g) - if err != nil { - return nil, err - } - return g, err -} diff --git a/gateways/community/github/config_test.go b/gateways/community/github/config_test.go deleted file mode 100644 index 08bb2e6c1f..0000000000 --- a/gateways/community/github/config_test.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2018 KompiTech GmbH - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -id: 1234 -hook: - endpoint: "/push" - port: "12000" - url: "http://webhook-gateway-svc" -owner: "asd" -repository: "dsa" -events: -- PushEvents -apiToken: - key: accesskey - name: githab-access -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a github event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*githubEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/community/github/start.go b/gateways/community/github/start.go deleted file mode 100644 index b31f3e0c52..0000000000 --- a/gateways/community/github/start.go +++ /dev/null @@ -1,247 +0,0 @@ -/* -Copyright 2018 KompiTech GmbH - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/argoproj/argo-events/store" - gh "github.com/google/go-github/github" - corev1 "k8s.io/api/core/v1" -) - -const ( - githubEventHeader = "X-GitHub-Event" - githubDeliveryHeader = "X-GitHub-Delivery" -) - -var ( - helper = gwcommon.NewWebhookHelper() -) - -func init() { - go gwcommon.InitRouteChannels(helper) -} - -// getCredentials for github -func (rc *RouteConfig) getCredentials(gs *corev1.SecretKeySelector) (*cred, error) { - token, err := store.GetSecrets(rc.clientset, rc.namespace, gs.Name, gs.Key) - if err != nil { - return nil, err - } - return &cred{ - secret: token, - }, nil -} - -func (rc *RouteConfig) GetRoute() *gwcommon.Route { - return rc.route -} - -func (rc *RouteConfig) PostStart() error { - gc := rc.ges - - c, err := rc.getCredentials(gc.APIToken) - if err != nil { - return fmt.Errorf("failed to rtrieve github credentials. err: %+v", err) - } - - PATTransport := TokenAuthTransport{ - Token: c.secret, - } - - formattedUrl := gwcommon.GenerateFormattedURL(gc.Hook) - hookConfig := map[string]interface{}{ - "url": &formattedUrl, - } - - if gc.ContentType != "" { - hookConfig["content_type"] = gc.ContentType - } - - if gc.Insecure { - hookConfig["insecure_ssl"] = "1" - } else { - hookConfig["insecure_ssl"] = "0" - } - - if gc.WebHookSecret != nil { - sc, err := rc.getCredentials(gc.WebHookSecret) - if err != nil { - return fmt.Errorf("failed to retrieve webhook secret. err: %+v", err) - } - hookConfig["secret"] = sc.secret - } - - rc.hook = &gh.Hook{ - Events: gc.Events, - Active: gh.Bool(gc.Active), - Config: hookConfig, - } - - rc.client = gh.NewClient(PATTransport.Client()) - if gc.GithubBaseURL != "" { - baseURL, err := url.Parse(gc.GithubBaseURL) - if err != nil { - return fmt.Errorf("failed to parse github base url. err: %s", err) - } - rc.client.BaseURL = baseURL - } - if gc.GithubUploadURL != "" { - uploadURL, err := url.Parse(gc.GithubUploadURL) - if err != nil { - return fmt.Errorf("failed to parse github upload url. err: %s", err) - } - rc.client.UploadURL = uploadURL - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - hook, _, err := rc.client.Repositories.CreateHook(ctx, gc.Owner, gc.Repository, rc.hook) - if err != nil { - // Continue if error is because hook already exists - er, ok := err.(*gh.ErrorResponse) - if !ok || er.Response.StatusCode != http.StatusUnprocessableEntity { - return fmt.Errorf("failed to create webhook. err: %+v", err) - } - } - - if hook == nil { - ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - hooks, _, err := rc.client.Repositories.ListHooks(ctx, gc.Owner, gc.Repository, nil) - if err != nil { - return fmt.Errorf("failed to list existing webhooks. err: %+v", err) - } - - hook = getHook(hooks, formattedUrl, gc.Events) - if hook == nil { - return fmt.Errorf("failed to find existing webhook.") - } - } - - if gc.WebHookSecret != nil { - // As secret in hook config is masked with asterisk (*), replace it with unmasked secret. - hook.Config["secret"] = hookConfig["secret"] - } - - rc.hook = hook - rc.route.Logger.WithField(common.LabelEventSource, rc.route.EventSource.Name).Info("github hook created") - return nil -} - -// PostStop runs after event source is stopped -func (rc *RouteConfig) PostStop() error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if _, err := rc.client.Repositories.DeleteHook(ctx, rc.ges.Owner, rc.ges.Repository, *rc.hook.ID); err != nil { - return fmt.Errorf("failed to delete hook. err: %+v", err) - } - rc.route.Logger.WithField(common.LabelEventSource, rc.route.EventSource.Name).Info("github hook deleted") - return nil -} - -// StartEventSource starts an event source -func (ese *GithubEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("operating on event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - gc := config.(*githubEventSource) - - return gwcommon.ProcessRoute(&RouteConfig{ - route: &gwcommon.Route{ - Logger: ese.Log, - EventSource: eventSource, - Webhook: gc.Hook, - StartCh: make(chan struct{}), - }, - clientset: ese.Clientset, - namespace: ese.Namespace, - ges: gc, - }, helper, eventStream) -} - -func parseValidateRequest(r *http.Request, secret []byte) ([]byte, error) { - body, err := gh.ValidatePayload(r, secret) - if err != nil { - return nil, err - } - - payload := make(map[string]interface{}) - if err := json.Unmarshal(body, &payload); err != nil { - return nil, err - } - for _, h := range []string{ - githubEventHeader, - githubDeliveryHeader, - } { - payload[h] = r.Header.Get(h) - } - return json.Marshal(payload) -} - -// routeActiveHandler handles new route -func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { - r := rc.route - - logger := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - "hi": "lol", - }) - - logger.Info("request received") - - if !helper.ActiveEndpoints[r.Webhook.Endpoint].Active { - logger.Info("endpoint is not active") - common.SendErrorResponse(writer, "") - return - } - - hook := rc.hook - secret := "" - if s, ok := hook.Config["secret"]; ok { - secret = s.(string) - } - body, err := parseValidateRequest(request, []byte(secret)) - if err != nil { - logger.WithError(err).Error("request is not valid event notification") - common.SendErrorResponse(writer, "") - return - } - - helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh <- body - logger.Info("request successfully processed") - common.SendSuccessResponse(writer, "") -} diff --git a/gateways/community/github/validate.go b/gateways/community/github/validate.go deleted file mode 100644 index f6f87aa575..0000000000 --- a/gateways/community/github/validate.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 KompiTech GmbH -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - "context" - "fmt" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// Validate validates github gateway configuration -func (ese *GithubEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateGithub) -} - -func validateGithub(config interface{}) error { - g := config.(*githubEventSource) - if g == nil { - return gwcommon.ErrNilEventSource - } - if g.Repository == "" { - return fmt.Errorf("repository cannot be empty") - } - if g.Owner == "" { - return fmt.Errorf("owner cannot be empty") - } - if g.APIToken == nil { - return fmt.Errorf("api token can't be empty") - } - if g.Events == nil || len(g.Events) < 1 { - return fmt.Errorf("events must be defined") - } - if g.ContentType != "" { - if !(g.ContentType == "json" || g.ContentType == "form") { - return fmt.Errorf("content type must be \"json\" or \"form\"") - } - } - return gwcommon.ValidateWebhook(g.Hook) -} diff --git a/gateways/community/github/validate_test.go b/gateways/community/github/validate_test.go deleted file mode 100644 index 5059b9c9e8..0000000000 --- a/gateways/community/github/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateGithubEventSource(t *testing.T) { - convey.Convey("Given github event source spec, parse it and make sure no error occurs", t, func() { - ese := &GithubEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "github.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/gitlab/cmd/main.go b/gateways/community/gitlab/cmd/main.go deleted file mode 100644 index b7b372d5c9..0000000000 --- a/gateways/community/gitlab/cmd/main.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "os" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/gitlab" - "k8s.io/client-go/kubernetes" -) - -func main() { - kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig) - restConfig, err := common.GetClientConfig(kubeConfig) - if err != nil { - panic(err) - } - clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) - if !ok { - panic("namespace is not provided") - } - gateways.StartGateway(&gitlab.GitlabEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - Namespace: namespace, - Clientset: clientset, - }) -} diff --git a/gateways/community/gitlab/config.go b/gateways/community/gitlab/config.go deleted file mode 100644 index 1184f1fcc7..0000000000 --- a/gateways/community/gitlab/config.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gitlab - -import ( - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" - "github.com/xanzy/go-gitlab" - "k8s.io/client-go/kubernetes" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// GitlabEventSourceExecutor implements ConfigExecutor -type GitlabEventSourceExecutor struct { - Log *logrus.Logger - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string -} - -// RouteConfig contains the configuration information for a route -type RouteConfig struct { - route *gwcommon.Route - clientset kubernetes.Interface - client *gitlab.Client - hook *gitlab.ProjectHook - namespace string - ges *gitlabEventSource -} - -// gitlabEventSource contains information to setup a gitlab project integration -type gitlabEventSource struct { - // Webhook - Hook *gwcommon.Webhook `json:"hook"` - // ProjectId is the id of project for which integration needs to setup - ProjectId string `json:"projectId"` - // Event is a gitlab event to listen to. - // Refer https://github.com/xanzy/go-gitlab/blob/bf34eca5d13a9f4c3f501d8a97b8ac226d55e4d9/projects.go#L794. - Event string `json:"event"` - // AccessToken is reference to k8 secret which holds the gitlab api access information - AccessToken *GitlabSecret `json:"accessToken"` - // EnableSSLVerification to enable ssl verification - EnableSSLVerification bool `json:"enableSSLVerification"` - // GitlabBaseURL is the base URL for API requests to a custom endpoint - GitlabBaseURL string `json:"gitlabBaseUrl"` -} - -// GitlabSecret contains information of k8 secret which holds the gitlab api access information -type GitlabSecret struct { - // Key within the K8 secret for access token - Key string - // Name of K8 secret containing access token info - Name string -} - -// cred stores the api access token -type cred struct { - // token is gitlab api access token - token string -} - -// parseEventSource parses an event sources of gateway -func parseEventSource(config string) (interface{}, error) { - var g *gitlabEventSource - err := yaml.Unmarshal([]byte(config), &g) - if err != nil { - return nil, err - } - return g, err -} diff --git a/gateways/community/gitlab/config_test.go b/gateways/community/gitlab/config_test.go deleted file mode 100644 index 8496bda0bd..0000000000 --- a/gateways/community/gitlab/config_test.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gitlab - -import ( - "testing" - - "github.com/smartystreets/goconvey/convey" -) - -var es = ` -id: 12 -hook: - endpoint: "/push" - port: "12000" - url: "http://webhook-gateway-gateway-svc/push" -projectId: "28" -event: "PushEvents" -accessToken: - key: accesskey - name: gitlab-access -enableSSLVerification: false -gitlabBaseUrl: "http://gitlab.com/" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a gitlab event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*gitlabEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/community/gitlab/start.go b/gateways/community/gitlab/start.go deleted file mode 100644 index 4020ffc535..0000000000 --- a/gateways/community/gitlab/start.go +++ /dev/null @@ -1,157 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gitlab - -import ( - "fmt" - "io/ioutil" - "net/http" - "reflect" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/argoproj/argo-events/store" - "github.com/xanzy/go-gitlab" -) - -var ( - helper = gwcommon.NewWebhookHelper() -) - -func init() { - go gwcommon.InitRouteChannels(helper) -} - -// getCredentials for gitlab -func (rc *RouteConfig) getCredentials(gs *GitlabSecret) (*cred, error) { - token, err := store.GetSecrets(rc.clientset, rc.namespace, gs.Name, gs.Key) - if err != nil { - return nil, err - } - return &cred{ - token: token, - }, nil -} - -func (rc *RouteConfig) GetRoute() *gwcommon.Route { - return rc.route -} - -func (rc *RouteConfig) PostStart() error { - c, err := rc.getCredentials(rc.ges.AccessToken) - if err != nil { - return fmt.Errorf("failed to get gitlab credentials. err: %+v", err) - } - - rc.client = gitlab.NewClient(nil, c.token) - if err = rc.client.SetBaseURL(rc.ges.GitlabBaseURL); err != nil { - return fmt.Errorf("failed to set gitlab base url, err: %+v", err) - } - - formattedUrl := gwcommon.GenerateFormattedURL(rc.ges.Hook) - - opt := &gitlab.AddProjectHookOptions{ - URL: &formattedUrl, - Token: &c.token, - EnableSSLVerification: &rc.ges.EnableSSLVerification, - } - - elem := reflect.ValueOf(opt).Elem().FieldByName(string(rc.ges.Event)) - if ok := elem.IsValid(); !ok { - return fmt.Errorf("unknown event %s", rc.ges.Event) - } - - iev := reflect.New(elem.Type().Elem()) - reflect.Indirect(iev).SetBool(true) - elem.Set(iev) - - hook, _, err := rc.client.Projects.AddProjectHook(rc.ges.ProjectId, opt) - if err != nil { - return fmt.Errorf("failed to add project hook. err: %+v", err) - } - - rc.hook = hook - rc.route.Logger.WithField(common.LabelEventSource, rc.route.EventSource.Name).Info("gitlab hook created") - return nil -} - -func (rc *RouteConfig) PostStop() error { - if _, err := rc.client.Projects.DeleteProjectHook(rc.ges.ProjectId, rc.hook.ID); err != nil { - return fmt.Errorf("failed to delete hook. err: %+v", err) - } - rc.route.Logger.WithField(common.LabelEventSource, rc.route.EventSource.Name).Info("gitlab hook deleted") - return nil -} - -// routeActiveHandler handles new route -func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { - r := rc.route - - log := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - }) - - log.Info("request received") - - if !helper.ActiveEndpoints[r.Webhook.Endpoint].Active { - log.Info("endpoint is not active") - common.SendErrorResponse(writer, "") - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.WithError(err).Error("failed to parse request body") - common.SendErrorResponse(writer, "") - return - } - - helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh <- body - log.Info("request successfully processed") - common.SendSuccessResponse(writer, "") -} - -// StartEventSource starts an event source -func (ese *GitlabEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - gl := config.(*gitlabEventSource) - - return gwcommon.ProcessRoute(&RouteConfig{ - route: &gwcommon.Route{ - EventSource: eventSource, - Logger: ese.Log, - Webhook: gl.Hook, - StartCh: make(chan struct{}), - }, - namespace: ese.Namespace, - clientset: ese.Clientset, - ges: gl, - }, helper, eventStream) -} diff --git a/gateways/community/gitlab/start_test.go b/gateways/community/gitlab/start_test.go deleted file mode 100644 index 4c27de9979..0000000000 --- a/gateways/community/gitlab/start_test.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gitlab - -import ( - "bytes" - "github.com/xanzy/go-gitlab" - "io/ioutil" - "net/http" - "testing" - - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" -) - -var ( - rc = &RouteConfig{ - route: gwcommon.GetFakeRoute(), - clientset: fake.NewSimpleClientset(), - namespace: "fake", - } - - secretName = "gitlab-access" - accessKey = "YWNjZXNz" - LabelAccessKey = "accesskey" -) - -func TestGetCredentials(t *testing.T) { - convey.Convey("Given a kubernetes secret, get credentials", t, func() { - secret, err := rc.clientset.CoreV1().Secrets(rc.namespace).Create(&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: rc.namespace, - }, - Data: map[string][]byte{ - LabelAccessKey: []byte(accessKey), - }, - }) - convey.So(err, convey.ShouldBeNil) - convey.So(secret, convey.ShouldNotBeNil) - - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - creds, err := rc.getCredentials(ps.(*gitlabEventSource).AccessToken) - convey.So(err, convey.ShouldBeNil) - convey.So(creds, convey.ShouldNotBeNil) - convey.So(creds.token, convey.ShouldEqual, "YWNjZXNz") - }) -} - -func TestRouteActiveHandler(t *testing.T) { - convey.Convey("Given a route configuration", t, func() { - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } - - convey.Convey("Inactive route should return error", func() { - writer := &gwcommon.FakeHttpWriter{} - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - pbytes, err := yaml.Marshal(ps.(*gitlabEventSource)) - convey.So(err, convey.ShouldBeNil) - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(pbytes)), - }) - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) - - convey.Convey("Active route should return success", func() { - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].Active = true - rc.hook = &gitlab.ProjectHook{ - URL: "fake", - PushEvents: true, - } - dataCh := make(chan []byte) - go func() { - resp := <-helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh - dataCh <- resp - }() - - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(pbytes)), - }) - - data := <-dataCh - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusOK) - convey.So(string(data), convey.ShouldEqual, string(pbytes)) - rc.ges = ps.(*gitlabEventSource) - err = rc.PostStart() - convey.So(err, convey.ShouldNotBeNil) - }) - }) - }) -} diff --git a/gateways/community/gitlab/validate.go b/gateways/community/gitlab/validate.go deleted file mode 100644 index b03eaf2dc8..0000000000 --- a/gateways/community/gitlab/validate.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gitlab - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gitlab gateway event source -func (ese *GitlabEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateGitlab) -} - -func validateGitlab(config interface{}) error { - g := config.(*gitlabEventSource) - if g == nil { - return gwcommon.ErrNilEventSource - } - if g.ProjectId == "" { - return fmt.Errorf("project id can't be empty") - } - if g.Event == "" { - return fmt.Errorf("event type can't be empty") - } - if g.GitlabBaseURL == "" { - return fmt.Errorf("gitlab base url can't be empty") - } - if g.AccessToken == nil { - return fmt.Errorf("access token can't be nil") - } - return gwcommon.ValidateWebhook(g.Hook) -} diff --git a/gateways/community/gitlab/validate_test.go b/gateways/community/gitlab/validate_test.go deleted file mode 100644 index c8ad5c89cf..0000000000 --- a/gateways/community/gitlab/validate_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gitlab - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateGitlabEventSource(t *testing.T) { - convey.Convey("Given a gitlab event source spec, parse it and make sure no error occurs", t, func() { - ese := &GitlabEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "gitlab.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.Println(valid.Reason) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/hdfs/cmd/main.go b/gateways/community/hdfs/cmd/main.go deleted file mode 100644 index d84824e0a7..0000000000 --- a/gateways/community/hdfs/cmd/main.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/hdfs" - "k8s.io/client-go/kubernetes" -) - -func main() { - kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig) - restConfig, err := common.GetClientConfig(kubeConfig) - if err != nil { - panic(err) - } - clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) - if !ok { - panic("namespace is not provided") - } - gateways.StartGateway(&hdfs.EventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - Namespace: namespace, - Clientset: clientset, - }) -} diff --git a/gateways/community/hdfs/config.go b/gateways/community/hdfs/config.go deleted file mode 100644 index de93b8daf1..0000000000 --- a/gateways/community/hdfs/config.go +++ /dev/null @@ -1,99 +0,0 @@ -package hdfs - -import ( - "errors" - "github.com/sirupsen/logrus" - - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// EventSourceExecutor implements Eventing -type EventSourceExecutor struct { - Log *logrus.Logger - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string -} - -// GatewayConfig contains information to setup a HDFS integration -type GatewayConfig struct { - gwcommon.WatchPathConfig `json:",inline"` - - // Type of file operations to watch - Type string `json:"type"` - // CheckInterval is a string that describes an interval duration to check the directory state, e.g. 1s, 30m, 2h... (defaults to 1m) - CheckInterval string `json:"checkInterval,omitempty"` - - GatewayClientConfig `json:",inline"` -} - -// GatewayClientConfig contains HDFS client configurations -type GatewayClientConfig struct { - // Addresses is accessible addresses of HDFS name nodes - Addresses []string `json:"addresses"` - - // HDFSUser is the user to access HDFS file system. - // It is ignored if either ccache or keytab is used. - HDFSUser string `json:"hdfsUser,omitempty"` - - // KrbCCacheSecret is the secret selector for Kerberos ccache - // Either ccache or keytab can be set to use Kerberos. - KrbCCacheSecret *corev1.SecretKeySelector `json:"krbCCacheSecret,omitempty"` - - // KrbKeytabSecret is the secret selector for Kerberos keytab - // Either ccache or keytab can be set to use Kerberos. - KrbKeytabSecret *corev1.SecretKeySelector `json:"krbKeytabSecret,omitempty"` - - // KrbUsername is the Kerberos username used with Kerberos keytab - // It must be set if keytab is used. - KrbUsername string `json:"krbUsername,omitempty"` - - // KrbRealm is the Kerberos realm used with Kerberos keytab - // It must be set if keytab is used. - KrbRealm string `json:"krbRealm,omitempty"` - - // KrbConfig is the configmap selector for Kerberos config as string - // It must be set if either ccache or keytab is used. - KrbConfigConfigMap *corev1.ConfigMapKeySelector `json:"krbConfigConfigMap,omitempty"` - - // KrbServicePrincipalName is the principal name of Kerberos service - // It must be set if either ccache or keytab is used. - KrbServicePrincipalName string `json:"krbServicePrincipalName,omitempty"` -} - -func parseEventSource(eventSource string) (interface{}, error) { - var f *GatewayConfig - err := yaml.Unmarshal([]byte(eventSource), &f) - if err != nil { - return nil, err - } - return f, err -} - -// Validate validates GatewayClientConfig -func (c *GatewayClientConfig) Validate() error { - if len(c.Addresses) == 0 { - return errors.New("addresses is required") - } - - hasKrbCCache := c.KrbCCacheSecret != nil - hasKrbKeytab := c.KrbKeytabSecret != nil - - if c.HDFSUser == "" && !hasKrbCCache && !hasKrbKeytab { - return errors.New("either hdfsUser, krbCCacheSecret or krbKeytabSecret is required") - } - if hasKrbKeytab && (c.KrbServicePrincipalName == "" || c.KrbConfigConfigMap == nil || c.KrbUsername == "" || c.KrbRealm == "") { - return errors.New("krbServicePrincipalName, krbConfigConfigMap, krbUsername and krbRealm are required with krbKeytabSecret") - } - if hasKrbCCache && (c.KrbServicePrincipalName == "" || c.KrbConfigConfigMap == nil) { - return errors.New("krbServicePrincipalName and krbConfigConfigMap are required with krbCCacheSecret") - } - - return nil -} diff --git a/gateways/community/hdfs/start.go b/gateways/community/hdfs/start.go deleted file mode 100644 index fb68c90df3..0000000000 --- a/gateways/community/hdfs/start.go +++ /dev/null @@ -1,154 +0,0 @@ -package hdfs - -import ( - "encoding/json" - "fmt" - "github.com/argoproj/argo-events/common" - "os" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/common/fsevent" - "github.com/argoproj/argo-events/gateways/common/naivewatcher" - "github.com/colinmarc/hdfs" -) - -// WatchableHDFS wraps hdfs.Client for naivewatcher -type WatchableHDFS struct { - hdfscli *hdfs.Client -} - -// Walk walks a directory -func (w *WatchableHDFS) Walk(root string, walkFn filepath.WalkFunc) error { - return w.hdfscli.Walk(root, walkFn) -} - -// GetFileID returns the file ID -func (w *WatchableHDFS) GetFileID(fi os.FileInfo) interface{} { - return fi.Name() - // FIXME: Use HDFS File ID once it's exposed - // https://github.com/colinmarc/hdfs/pull/171 - // return fi.Sys().(*hadoop_hdfs.HdfsFileStatusProto).GetFileID() -} - -// StartEventSource starts an event source -func (ese *EventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - ese.Log.WithField(common.LabelEventSource, eventSource.Name).Info("activating event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - return err - } - gwc := config.(*GatewayConfig) - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(gwc, eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func (ese *EventSourceExecutor) listenEvents(config *GatewayConfig, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - hdfsConfig, err := createHDFSConfig(ese.Clientset, ese.Namespace, &config.GatewayClientConfig) - if err != nil { - errorCh <- err - return - } - - hdfscli, err := createHDFSClient(hdfsConfig.Addresses, hdfsConfig.HDFSUser, hdfsConfig.KrbOptions) - if err != nil { - errorCh <- err - return - } - defer hdfscli.Close() - - // create new watcher - watcher, err := naivewatcher.NewWatcher(&WatchableHDFS{hdfscli: hdfscli}) - if err != nil { - errorCh <- err - return - } - defer watcher.Close() - - intervalDuration := 1 * time.Minute - if config.CheckInterval != "" { - d, err := time.ParseDuration(config.CheckInterval) - if err != nil { - errorCh <- err - return - } - intervalDuration = d - } - - err = watcher.Start(intervalDuration) - if err != nil { - errorCh <- err - return - } - - // directory to watch must be available in HDFS. You can't watch a directory that is not present. - err = watcher.Add(config.Directory) - if err != nil { - errorCh <- err - return - } - - op := fsevent.NewOp(config.Type) - var pathRegexp *regexp.Regexp - if config.PathRegexp != "" { - pathRegexp, err = regexp.Compile(config.PathRegexp) - if err != nil { - errorCh <- err - return - } - } - log.Info("starting to watch to HDFS notifications") - for { - select { - case event, ok := <-watcher.Events: - if !ok { - log.Info("HDFS watcher has stopped") - // watcher stopped watching file events - errorCh <- fmt.Errorf("HDFS watcher stopped") - return - } - matched := false - relPath := strings.TrimPrefix(event.Name, config.Directory) - if config.Path != "" && config.Path == relPath { - matched = true - } else if pathRegexp != nil && pathRegexp.MatchString(relPath) { - matched = true - } - if matched && (op&event.Op != 0) { - log.WithFields( - map[string]interface{}{ - "event-type": event.Op.String(), - "descriptor-name": event.Name, - }, - ).Debug("HDFS event") - - payload, err := json.Marshal(event) - if err != nil { - errorCh <- err - return - } - dataCh <- payload - } - case err := <-watcher.Errors: - errorCh <- err - return - case <-doneCh: - return - } - } -} diff --git a/gateways/community/hdfs/validate.go b/gateways/community/hdfs/validate.go deleted file mode 100644 index 068245b543..0000000000 --- a/gateways/community/hdfs/validate.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package hdfs - -import ( - "context" - "errors" - "time" - - "github.com/argoproj/argo-events/gateways/common/fsevent" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *EventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateGatewayConfig) -} - -func validateGatewayConfig(config interface{}) error { - gwc := config.(*GatewayConfig) - if gwc == nil { - return gwcommon.ErrNilEventSource - } - if gwc.Type == "" { - return errors.New("type is required") - } - op := fsevent.NewOp(gwc.Type) - if op == 0 { - return errors.New("type is invalid") - } - if gwc.CheckInterval != "" { - _, err := time.ParseDuration(gwc.CheckInterval) - if err != nil { - return errors.New("failed to parse interval") - } - } - err := gwc.WatchPathConfig.Validate() - if err != nil { - return err - } - err = gwc.GatewayClientConfig.Validate() - return err -} diff --git a/gateways/community/hdfs/validate_test.go b/gateways/community/hdfs/validate_test.go deleted file mode 100644 index 3c03f52eb0..0000000000 --- a/gateways/community/hdfs/validate_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package hdfs - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateEventSource(t *testing.T) { - convey.Convey("Given a hdfs event source spec, parse it and make sure no error occurs", t, func() { - ese := &EventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "hdfs.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/slack/config.go b/gateways/community/slack/config.go deleted file mode 100644 index 7455d4ec19..0000000000 --- a/gateways/community/slack/config.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package slack - -import ( - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// SlackEventSourceExecutor implements Eventing -type SlackEventSourceExecutor struct { - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string - Log *logrus.Logger -} - -type RouteConfig struct { - route *gwcommon.Route - ses *slackEventSource - token string - signingSecret string - clientset kubernetes.Interface - namespace string -} - -type slackEventSource struct { - // Slack App signing secret - SigningSecret *corev1.SecretKeySelector `json:"signingSecret,omitempty"` - // Token for URL verification handshake - Token *corev1.SecretKeySelector `json:"token"` - // Webhook - Hook *gwcommon.Webhook `json:"hook"` -} - -func parseEventSource(es string) (interface{}, error) { - var n *slackEventSource - err := yaml.Unmarshal([]byte(es), &n) - if err != nil { - return nil, err - } - return n, nil -} diff --git a/gateways/community/slack/validate.go b/gateways/community/slack/validate.go deleted file mode 100644 index 4c9637d82a..0000000000 --- a/gateways/community/slack/validate.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package slack - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *SlackEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateSlack) -} - -func validateSlack(config interface{}) error { - sc := config.(*slackEventSource) - if sc == nil { - return gwcommon.ErrNilEventSource - } - if sc.Token == nil { - return fmt.Errorf("token not provided") - } - return gwcommon.ValidateWebhook(sc.Hook) -} diff --git a/gateways/community/slack/validate_test.go b/gateways/community/slack/validate_test.go deleted file mode 100644 index f9b1927279..0000000000 --- a/gateways/community/slack/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package slack - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestSlackEventSource(t *testing.T) { - convey.Convey("Given a slack event source spec, parse it and make sure no error occurs", t, func() { - ese := &SlackEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "slack.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/community/storagegrid/cmd/main.go b/gateways/community/storagegrid/cmd/main.go deleted file mode 100644 index 8e066c88eb..0000000000 --- a/gateways/community/storagegrid/cmd/main.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/storagegrid" -) - -func main() { - gateways.StartGateway(&storagegrid.StorageGridEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - }) -} diff --git a/gateways/community/storagegrid/config_test.go b/gateways/community/storagegrid/config_test.go deleted file mode 100644 index dac18610eb..0000000000 --- a/gateways/community/storagegrid/config_test.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package storagegrid - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -hook: - endpoint: "/" - port: "8080" - url: "testurl" -events: - - "ObjectCreated:Put" -filter: - suffix: ".txt" - prefix: "hello-" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a storage grid event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*storageGridEventSource) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/community/storagegrid/start.go b/gateways/community/storagegrid/start.go deleted file mode 100644 index 0ae6320795..0000000000 --- a/gateways/community/storagegrid/start.go +++ /dev/null @@ -1,183 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package storagegrid - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "net/url" - "strings" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/joncalhoun/qson" - "github.com/google/uuid" -) - -var ( - helper = gwcommon.NewWebhookHelper() - - respBody = ` - - - ` + generateUUID().String() + ` - - - ` + generateUUID().String() + ` - -` + "\n" -) - -func init() { - go gwcommon.InitRouteChannels(helper) -} - -// generateUUID returns a new uuid -func generateUUID() uuid.UUID { - return uuid.New() -} - -// filterEvent filters notification based on event filter in a gateway configuration -func filterEvent(notification *storageGridNotification, sg *storageGridEventSource) bool { - if sg.Events == nil { - return true - } - for _, filterEvent := range sg.Events { - if notification.Message.Records[0].EventName == filterEvent { - return true - } - } - return false -} - -// filterName filters object key based on configured prefix and/or suffix -func filterName(notification *storageGridNotification, sg *storageGridEventSource) bool { - if sg.Filter == nil { - return true - } - if sg.Filter.Prefix != "" && sg.Filter.Suffix != "" { - return strings.HasPrefix(notification.Message.Records[0].S3.Object.Key, sg.Filter.Prefix) && strings.HasSuffix(notification.Message.Records[0].S3.Object.Key, sg.Filter.Suffix) - } - if sg.Filter.Prefix != "" { - return strings.HasPrefix(notification.Message.Records[0].S3.Object.Key, sg.Filter.Prefix) - } - if sg.Filter.Suffix != "" { - return strings.HasSuffix(notification.Message.Records[0].S3.Object.Key, sg.Filter.Suffix) - } - return true -} - -func (rc *RouteConfig) GetRoute() *gwcommon.Route { - return rc.route -} - -// StartConfig runs a configuration -func (ese *StorageGridEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - sges := config.(*storageGridEventSource) - - return gwcommon.ProcessRoute(&RouteConfig{ - route: &gwcommon.Route{ - Webhook: sges.Hook, - EventSource: eventSource, - Logger: ese.Log, - StartCh: make(chan struct{}), - }, - sges: sges, - }, helper, eventStream) -} - -func (rc *RouteConfig) PostStart() error { - return nil -} - -func (rc *RouteConfig) PostStop() error { - return nil -} - -// RouteHandler handles new route -func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { - r := rc.route - - log := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - common.LabelHTTPMethod: r.Webhook.Method, - }) - - if !helper.ActiveEndpoints[r.Webhook.Endpoint].Active { - log.Warn("inactive route") - common.SendErrorResponse(writer, "") - return - } - - log.Info("received a request") - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.WithError(err).Error("failed to parse request body") - common.SendErrorResponse(writer, "") - return - } - - switch request.Method { - case http.MethodHead: - respBody = "" - } - writer.WriteHeader(http.StatusOK) - writer.Header().Add("Content-Type", "text/plain") - writer.Write([]byte(respBody)) - - // notification received from storage grid is url encoded. - parsedURL, err := url.QueryUnescape(string(body)) - if err != nil { - log.WithError(err).Error("failed to unescape request body url") - return - } - b, err := qson.ToJSON(parsedURL) - if err != nil { - log.WithError(err).Error("failed to convert request body in JSON format") - return - } - - var notification *storageGridNotification - err = json.Unmarshal(b, ¬ification) - if err != nil { - log.WithError(err).Error("failed to unmarshal request body") - return - } - - if filterEvent(notification, rc.sges) && filterName(notification, rc.sges) { - log.WithError(err).Error("new event received, dispatching to gateway client") - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh <- b - return - } - - log.Warn("discarding notification since it did not pass all filters") -} diff --git a/gateways/community/storagegrid/validate.go b/gateways/community/storagegrid/validate.go deleted file mode 100644 index 45c1e2b380..0000000000 --- a/gateways/community/storagegrid/validate.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package storagegrid - -import ( - "context" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *StorageGridEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateStorageGrid) -} - -func validateStorageGrid(config interface{}) error { - sg := config.(*storageGridEventSource) - if sg == nil { - return gwcommon.ErrNilEventSource - } - return gwcommon.ValidateWebhook(sg.Hook) -} diff --git a/gateways/community/storagegrid/validate_test.go b/gateways/community/storagegrid/validate_test.go deleted file mode 100644 index 1cdc2a367e..0000000000 --- a/gateways/community/storagegrid/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package storagegrid - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateStorageGridEventSource(t *testing.T) { - convey.Convey("Given a storage grid event source spec, parse it and make sure no error occurs", t, func() { - ese := &StorageGridEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "storage-grid.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/config.go b/gateways/config.go deleted file mode 100644 index 5e20028d00..0000000000 --- a/gateways/config.go +++ /dev/null @@ -1,168 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateways - -import ( - "context" - "fmt" - "github.com/sirupsen/logrus" - "os" - - "github.com/nats-io/go-nats" - - "github.com/argoproj/argo-events/common" - pc "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - gwclientset "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" - snats "github.com/nats-io/go-nats-streaming" - "google.golang.org/grpc" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -// GatewayConfig provides a generic event source for a gateway -type GatewayConfig struct { - // Log provides fast and simple logger dedicated to JSON output - Log *logrus.Logger - // Clientset is client for kubernetes API - Clientset kubernetes.Interface - // Name is gateway name - Name string - // Namespace is namespace for the gateway to run inside - Namespace string - // KubeConfig rest client config - KubeConfig *rest.Config - // gateway holds Gateway custom resource - gw *v1alpha1.Gateway - // gwClientset is gateway clientset - gwcs gwclientset.Interface - // updated indicates whether gateway resource is updated - updated bool - // serverPort is gateway server port to listen events from - serverPort string - // registeredConfigs stores information about current event sources that are running in the gateway - registeredConfigs map[string]*EventSourceContext - // configName is name of configmap that contains run event source/s for the gateway - configName string - // controllerInstanceId is instance ID of the gateway controller - controllerInstanceID string - // StatusCh is used to communicate the status of an event source - StatusCh chan EventSourceStatus - // natsConn is the standard nats connection used to publish events to cluster. Only used if dispatch protocol is NATS - natsConn *nats.Conn - // natsStreamingConn is the nats connection used for streaming. - natsStreamingConn snats.Conn - // sensorHttpPort is the http server running in sensor that listens to event. Only used if dispatch protocol is HTTP - sensorHttpPort string -} - -// EventSourceContext contains information of a event source for gateway to run. -type EventSourceContext struct { - // Source holds the actual event source - Source *EventSource - // Ctx contains context for the connection - Ctx context.Context - // Cancel upon invocation cancels the connection context - Cancel context.CancelFunc - // Client is grpc client - Client EventingClient - // Conn is grpc connection - Conn *grpc.ClientConn -} - -// GatewayEvent is the internal representation of an event. -type GatewayEvent struct { - // Src is source of event - Src string `json:"src"` - // Payload contains event data - Payload []byte `json:"payload"` -} - -// NewGatewayConfiguration returns a new gateway configuration -func NewGatewayConfiguration() *GatewayConfig { - kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig) - restConfig, err := common.GetClientConfig(kubeConfig) - if err != nil { - panic(err) - } - name, ok := os.LookupEnv(common.EnvVarGatewayName) - if !ok { - panic("gateway name not provided") - } - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) - if !ok { - panic("no namespace provided") - } - configName, ok := os.LookupEnv(common.EnvVarGatewayEventSourceConfigMap) - if !ok { - panic("gateway processor configmap is not provided") - } - controllerInstanceID, ok := os.LookupEnv(common.EnvVarGatewayControllerInstanceID) - if !ok { - panic("gateway controller instance ID is not provided") - } - serverPort, ok := os.LookupEnv(common.EnvVarGatewayServerPort) - if !ok { - panic("server port is not provided") - } - - clientset := kubernetes.NewForConfigOrDie(restConfig) - gwcs := gwclientset.NewForConfigOrDie(restConfig) - gw, err := gwcs.ArgoprojV1alpha1().Gateways(namespace).Get(name, metav1.GetOptions{}) - if err != nil { - panic(err) - } - - gc := &GatewayConfig{ - Log: common.NewArgoEventsLogger().WithFields( - map[string]interface{}{ - common.LabelGatewayName: gw.Name, - common.LabelNamespace: gw.Namespace, - }).Logger, - Clientset: clientset, - Namespace: namespace, - Name: name, - KubeConfig: restConfig, - registeredConfigs: make(map[string]*EventSourceContext), - configName: configName, - gwcs: gwcs, - gw: gw, - controllerInstanceID: controllerInstanceID, - serverPort: serverPort, - StatusCh: make(chan EventSourceStatus), - } - - switch gw.Spec.EventProtocol.Type { - case pc.HTTP: - gc.sensorHttpPort = gw.Spec.EventProtocol.Http.Port - case pc.NATS: - if gc.natsConn, err = nats.Connect(gw.Spec.EventProtocol.Nats.URL); err != nil { - panic(fmt.Errorf("failed to obtain NATS standard connection. err: %+v", err)) - } - gc.Log.WithField(common.LabelURL, gw.Spec.EventProtocol.Nats.URL).Info("connected to nats service") - - if gc.gw.Spec.EventProtocol.Nats.Type == pc.Streaming { - gc.natsStreamingConn, err = snats.Connect(gc.gw.Spec.EventProtocol.Nats.ClusterId, gc.gw.Spec.EventProtocol.Nats.ClientId, snats.NatsConn(gc.natsConn)) - if err != nil { - panic(fmt.Errorf("failed to obtain NATS streaming connection. err: %+v", err)) - } - gc.Log.WithField(common.LabelURL, gw.Spec.EventProtocol.Nats.URL).Info("nats streaming connection successful") - } - } - return gc -} diff --git a/gateways/core/artifact/Dockerfile b/gateways/core/artifact/Dockerfile deleted file mode 100644 index 7e1d5661d1..0000000000 --- a/gateways/core/artifact/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM centos:7 -COPY dist/artifact-gateway /bin/ -ENTRYPOINT [ "/bin/artifact-gateway" ] \ No newline at end of file diff --git a/gateways/core/artifact/config.go b/gateways/core/artifact/config.go deleted file mode 100644 index db8b90c06d..0000000000 --- a/gateways/core/artifact/config.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package artifact - -import ( - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// S3EventSourceExecutor implements Eventing -type S3EventSourceExecutor struct { - Log *logrus.Logger - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string -} - -func parseEventSource(config string) (interface{}, error) { - var a *apicommon.S3Artifact - err := yaml.Unmarshal([]byte(config), &a) - if err != nil { - return nil, err - } - return a, err -} diff --git a/gateways/core/artifact/config_test.go b/gateways/core/artifact/config_test.go deleted file mode 100644 index ac73c722aa..0000000000 --- a/gateways/core/artifact/config_test.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package artifact - -import ( - "testing" - - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/smartystreets/goconvey/convey" -) - -var es = ` -bucket: - name: input -endpoint: minio-service.argo-events:9000 -event: s3:ObjectCreated:Put -filter: - prefix: "" - suffix: "" -insecure: true -accessKey: - key: accesskey - name: artifacts-minio -secretKey: - key: secretkey - name: artifacts-minio -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a artifact event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*apicommon.S3Artifact) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/core/artifact/start.go b/gateways/core/artifact/start.go deleted file mode 100644 index d163a6a812..0000000000 --- a/gateways/core/artifact/start.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package artifact - -import ( - "encoding/json" - "github.com/argoproj/argo-events/common" - - "github.com/argoproj/argo-events/gateways" - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/store" - "github.com/minio/minio-go" -) - -// StartEventSource activates an event source and streams back events -func (ese *S3EventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("activating event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*apicommon.S3Artifact), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -// listenEvents listens to minio bucket notifications -func (ese *S3EventSourceExecutor) listenEvents(a *apicommon.S3Artifact, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - - log.Info("retrieving access and secret key") - // retrieve access key id and secret access key - accessKey, err := store.GetSecrets(ese.Clientset, ese.Namespace, a.AccessKey.Name, a.AccessKey.Key) - if err != nil { - errorCh <- err - return - } - secretKey, err := store.GetSecrets(ese.Clientset, ese.Namespace, a.SecretKey.Name, a.SecretKey.Key) - if err != nil { - errorCh <- err - return - } - - minioClient, err := minio.New(a.Endpoint, accessKey, secretKey, !a.Insecure) - if err != nil { - errorCh <- err - return - } - - log.Info("starting to listen to bucket notifications") - for notification := range minioClient.ListenBucketNotification(a.Bucket.Name, a.Filter.Prefix, a.Filter.Suffix, a.Events, doneCh) { - if notification.Err != nil { - errorCh <- notification.Err - return - } - payload, err := json.Marshal(notification.Records[0]) - if err != nil { - errorCh <- err - return - } - dataCh <- payload - } -} diff --git a/gateways/core/artifact/validate.go b/gateways/core/artifact/validate.go deleted file mode 100644 index c73f24fd52..0000000000 --- a/gateways/core/artifact/validate.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package artifact - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/minio/minio-go" -) - -// ValidateEventSource validates a s3 event source -func (ese *S3EventSourceExecutor) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(eventSource, ArgoEventsEventSourceVersion, parseEventSource, validateArtifact) -} - -// validates an artifact -func validateArtifact(config interface{}) error { - a := config.(*apicommon.S3Artifact) - if a == nil { - return gwcommon.ErrNilEventSource - } - if a.AccessKey == nil { - return fmt.Errorf("access key can't be empty") - } - if a.SecretKey == nil { - return fmt.Errorf("secret key can't be empty") - } - if a.Endpoint == "" { - return fmt.Errorf("endpoint url can't be empty") - } - if a.Bucket != nil && a.Bucket.Name == "" { - return fmt.Errorf("bucket name can't be empty") - } - if a.Events != nil { - for _, event := range a.Events { - if minio.NotificationEventType(event) == "" { - return fmt.Errorf("unknown event %s", event) - } - } - } - return nil -} diff --git a/gateways/core/artifact/validate_test.go b/gateways/core/artifact/validate_test.go deleted file mode 100644 index ca7b7a3d90..0000000000 --- a/gateways/core/artifact/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package artifact - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateS3EventSource(t *testing.T) { - convey.Convey("Given a S3 artifact spec, parse the spec and make sure no error occurs", t, func() { - ese := &S3EventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "artifact.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/calendar/config.go b/gateways/core/calendar/config.go deleted file mode 100644 index dc9fff509b..0000000000 --- a/gateways/core/calendar/config.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package calendar - -import ( - "encoding/json" - "github.com/sirupsen/logrus" - "time" - - "github.com/ghodss/yaml" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// CalendarEventSourceExecutor implements Eventing -type CalendarEventSourceExecutor struct { - Log *logrus.Logger -} - -// calSchedule describes a time based dependency. One of the fields (schedule, interval, or recurrence) must be passed. -// Schedule takes precedence over interval; interval takes precedence over recurrence -// +k8s:openapi-gen=true -type calSchedule struct { - // Schedule is a cron-like expression. For reference, see: https://en.wikipedia.org/wiki/Cron - Schedule string `json:"schedule"` - - // Interval is a string that describes an interval duration, e.g. 1s, 30m, 2h... - Interval string `json:"interval"` - - // List of RRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545. - // RRULE is a recurrence rule which defines a repeating pattern for recurring events. - // RDATE defines the list of DATE-TIME values for recurring events. - // EXDATE defines the list of DATE-TIME exceptions for recurring events. - // the combination of these rules and dates combine to form a set of date times. - // NOTE: functionality currently only supports EXDATEs, but in the future could be expanded. - Recurrence []string `json:"recurrence,omitempty"` - - // Timezone in which to run the schedule - // +optional - Timezone string `json:"timezone,omitempty"` - - // UserPayload will be sent to sensor as extra data once the event is triggered - // +optional - UserPayload *json.RawMessage `json:"userPayload,omitempty"` -} - -// calResponse is the event payload that is sent as response to sensor -type calResponse struct { - // EventTime is time at which event occurred - EventTime time.Time `json:"eventTime"` - - // UserPayload if any - UserPayload *json.RawMessage `json:"userPayload"` -} - -func parseEventSource(eventSource string) (interface{}, error) { - var c *calSchedule - err := yaml.Unmarshal([]byte(eventSource), &c) - if err != nil { - return nil, err - } - return c, err -} diff --git a/gateways/core/calendar/config_test.go b/gateways/core/calendar/config_test.go deleted file mode 100644 index 0d94761cc5..0000000000 --- a/gateways/core/calendar/config_test.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package calendar - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -interval: 2s -userPayload: "{\r\n\"hello\": \"world\"\r\n}" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a calendar event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*calSchedule) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/core/calendar/start.go b/gateways/core/calendar/start.go deleted file mode 100644 index a90e088a8f..0000000000 --- a/gateways/core/calendar/start.go +++ /dev/null @@ -1,143 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package calendar - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - cronlib "github.com/robfig/cron" -) - -// Next is a function to compute the next event time from a given time -type Next func(time.Time) time.Time - -// StartEventSource starts an event source -func (ese *CalendarEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("activating event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*calSchedule), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func resolveSchedule(cal *calSchedule) (cronlib.Schedule, error) { - if cal.Schedule != "" { - // standard cron expression - specParser := cronlib.NewParser(cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow) - schedule, err := specParser.Parse(cal.Schedule) - if err != nil { - return nil, fmt.Errorf("failed to parse schedule %s from calendar event. Cause: %+v", cal.Schedule, err.Error()) - } - return schedule, nil - } else if cal.Interval != "" { - intervalDuration, err := time.ParseDuration(cal.Interval) - if err != nil { - return nil, fmt.Errorf("failed to parse interval %s from calendar event. Cause: %+v", cal.Interval, err.Error()) - } - schedule := cronlib.ConstantDelaySchedule{Delay: intervalDuration} - return schedule, nil - } else { - return nil, fmt.Errorf("calendar event must contain either a schedule or interval") - } -} - -// listenEvents fires an event when schedule is passed. -func (ese *CalendarEventSourceExecutor) listenEvents(cal *calSchedule, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - schedule, err := resolveSchedule(cal) - if err != nil { - errorCh <- err - return - } - - exDates, err := common.ParseExclusionDates(cal.Recurrence) - if err != nil { - errorCh <- err - return - } - - var next Next - next = func(last time.Time) time.Time { - nextT := schedule.Next(last) - nextYear := nextT.Year() - nextMonth := nextT.Month() - nextDay := nextT.Day() - for _, exDate := range exDates { - // if exDate == nextEvent, then we need to skip this and get the next - if exDate.Year() == nextYear && exDate.Month() == nextMonth && exDate.Day() == nextDay { - return next(nextT) - } - } - return nextT - } - - lastT := time.Now() - var location *time.Location - if cal.Timezone != "" { - location, err = time.LoadLocation(cal.Timezone) - if err != nil { - errorCh <- err - return - } - lastT = lastT.In(location) - } - - for { - t := next(lastT) - timer := time.After(time.Until(t)) - ese.Log.WithFields( - map[string]interface{}{ - common.LabelEventSource: eventSource.Name, - common.LabelTime: t.UTC().String(), - }).Info("expected next calendar event") - select { - case tx := <-timer: - lastT = tx - if location != nil { - lastT = lastT.In(location) - } - response := &calResponse{ - EventTime: tx, - UserPayload: cal.UserPayload, - } - payload, err := json.Marshal(response) - if err != nil { - errorCh <- err - return - } - dataCh <- payload - case <-doneCh: - return - } - } -} diff --git a/gateways/core/calendar/validate.go b/gateways/core/calendar/validate.go deleted file mode 100644 index 9d90e20fe5..0000000000 --- a/gateways/core/calendar/validate.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package calendar - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *CalendarEventSourceExecutor) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(eventSource, ArgoEventsEventSourceVersion, parseEventSource, validateSchedule) -} - -func validateSchedule(config interface{}) error { - cal := config.(*calSchedule) - if cal == nil { - return gwcommon.ErrNilEventSource - } - if cal.Schedule == "" && cal.Interval == "" { - return fmt.Errorf("must have either schedule or interval") - } - if _, err := resolveSchedule(cal); err != nil { - return err - } - return nil -} diff --git a/gateways/core/calendar/validate_test.go b/gateways/core/calendar/validate_test.go deleted file mode 100644 index 6f54d616b0..0000000000 --- a/gateways/core/calendar/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package calendar - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateCalendarEventSource(t *testing.T) { - convey.Convey("Given a calendar spec, parse it and make sure no error occurs", t, func() { - ese := &CalendarEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "calendar.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/file/config.go b/gateways/core/file/config.go deleted file mode 100644 index 3170923287..0000000000 --- a/gateways/core/file/config.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package file - -import ( - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// FileEventSourceExecutor implements Eventing -type FileEventSourceExecutor struct { - Log *logrus.Logger -} - -// fileWatcher contains configuration information for this gateway -// +k8s:openapi-gen=true -type fileWatcher struct { - gwcommon.WatchPathConfig `json:",inline"` - - // Type of file operations to watch - // Refer https://github.com/fsnotify/fsnotify/blob/master/fsnotify.go for more information - Type string `json:"type"` -} - -func parseEventSource(eventSource string) (interface{}, error) { - var f *fileWatcher - err := yaml.Unmarshal([]byte(eventSource), &f) - if err != nil { - return nil, err - } - return f, err -} diff --git a/gateways/core/file/config_test.go b/gateways/core/file/config_test.go deleted file mode 100644 index b4be30ff2c..0000000000 --- a/gateways/core/file/config_test.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package file - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -directory: "/bin/" -type: CREATE -path: x.txt -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a file event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*fileWatcher) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/core/file/start.go b/gateways/core/file/start.go deleted file mode 100644 index 6782949518..0000000000 --- a/gateways/core/file/start.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package file - -import ( - "encoding/json" - "fmt" - "github.com/argoproj/argo-events/common" - "regexp" - "strings" - - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/common/fsevent" - "github.com/fsnotify/fsnotify" -) - -// StartEventSource starts an event source -func (ese *FileEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("activating event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*fileWatcher), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func (ese *FileEventSourceExecutor) listenEvents(fwc *fileWatcher, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - // create new fs watcher - watcher, err := fsnotify.NewWatcher() - if err != nil { - errorCh <- err - return - } - defer watcher.Close() - - // file descriptor to watch must be available in file system. You can't watch an fs descriptor that is not present. - err = watcher.Add(fwc.Directory) - if err != nil { - errorCh <- err - return - } - - var pathRegexp *regexp.Regexp - if fwc.PathRegexp != "" { - pathRegexp, err = regexp.Compile(fwc.PathRegexp) - if err != nil { - errorCh <- err - return - } - } - - log.Info("starting to watch to file notifications") - for { - select { - case event, ok := <-watcher.Events: - if !ok { - log.Info("fs watcher has stopped") - // watcher stopped watching file events - errorCh <- fmt.Errorf("fs watcher stopped") - return - } - // fwc.Path == event.Name is required because we don't want to send event when .swp files are created - matched := false - relPath := strings.TrimPrefix(event.Name, fwc.Directory) - if fwc.Path != "" && fwc.Path == relPath { - matched = true - } else if pathRegexp != nil && pathRegexp.MatchString(relPath) { - matched = true - } - if matched && fwc.Type == event.Op.String() { - log.WithFields( - map[string]interface{}{ - "event-type": event.Op.String(), - "descriptor-name": event.Name, - }, - ).Debug("fs event") - - // Assume fsnotify event has the same Op spec of our file event - fileEvent := fsevent.Event{Name: event.Name, Op: fsevent.NewOp(event.Op.String())} - payload, err := json.Marshal(fileEvent) - if err != nil { - errorCh <- err - return - } - dataCh <- payload - } - case err := <-watcher.Errors: - errorCh <- err - return - case <-doneCh: - return - } - } -} diff --git a/gateways/core/file/validate.go b/gateways/core/file/validate.go deleted file mode 100644 index 4a1d27fa79..0000000000 --- a/gateways/core/file/validate.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package file - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *FileEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateFileWatcher) -} - -func validateFileWatcher(config interface{}) error { - fwc := config.(*fileWatcher) - if fwc == nil { - return gwcommon.ErrNilEventSource - } - if fwc.Type == "" { - return fmt.Errorf("type must be specified") - } - err := fwc.WatchPathConfig.Validate() - return err -} diff --git a/gateways/core/file/validate_test.go b/gateways/core/file/validate_test.go deleted file mode 100644 index f97a61420b..0000000000 --- a/gateways/core/file/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package file - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateFileEventSource(t *testing.T) { - convey.Convey("Given a file event source spec, parse it and make sure no error occurs", t, func() { - ese := &FileEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "file.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/resource/config.go b/gateways/core/resource/config.go deleted file mode 100644 index 27dc40afc4..0000000000 --- a/gateways/core/resource/config.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resource - -import ( - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/rest" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -type EventType string - -const ( - ADD EventType = "ADD" - UPDATE EventType = "UPDATE" - DELETE EventType = "DELETE" -) - -// InformerEvent holds event generated from resource state change -type InformerEvent struct { - Obj interface{} - OldObj interface{} - Type EventType -} - -// ResourceEventSourceExecutor implements Eventing -type ResourceEventSourceExecutor struct { - Log *logrus.Logger - // K8RestConfig is kubernetes cluster config - K8RestConfig *rest.Config -} - -// resource refers to a dependency on a k8s resource. -type resource struct { - // Namespace where resource is deployed - Namespace string `json:"namespace"` - // Filter is applied on the metadata of the resource - Filter *ResourceFilter `json:"filter,omitempty"` - // Group of the resource - metav1.GroupVersionResource `json:",inline"` - // Type is the event type. - // If not provided, the gateway will watch all events for a resource. - Type EventType `json:"type,omitempty"` -} - -// ResourceFilter contains K8 ObjectMeta information to further filter resource event objects -type ResourceFilter struct { - Prefix string `json:"prefix,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - Annotations map[string]string `json:"annotations,omitempty"` - Fields map[string]string `json:"fields,omitempty"` - CreatedBy metav1.Time `json:"createdBy,omitempty"` -} - -func parseEventSource(es string) (interface{}, error) { - var r *resource - err := yaml.Unmarshal([]byte(es), &r) - if err != nil { - return nil, err - } - return r, err -} diff --git a/gateways/core/resource/config_test.go b/gateways/core/resource/config_test.go deleted file mode 100644 index 34a615799e..0000000000 --- a/gateways/core/resource/config_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resource - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -namespace: "argo-events" -group: "" -version: "v1" -resource: "pods" -filter: - labels: - workflows.argoproj.io/phase: Succeeded - name: "my-workflow" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a resource event source, parse it", t, func() { - ps, err := parseEventSource(es) - - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - - resource, ok := ps.(*resource) - convey.So(ok, convey.ShouldEqual, true) - - convey.So(resource.Group, convey.ShouldEqual, "") - convey.So(resource.Version, convey.ShouldEqual, "v1") - convey.So(resource.Resource, convey.ShouldEqual, "pods") - }) -} diff --git a/gateways/core/resource/validate.go b/gateways/core/resource/validate.go deleted file mode 100644 index 2b0a51f31b..0000000000 --- a/gateways/core/resource/validate.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resource - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (executor *ResourceEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateResource) -} - -func validateResource(config interface{}) error { - res := config.(*resource) - if res == nil { - return gwcommon.ErrNilEventSource - } - if res.Version == "" { - return fmt.Errorf("version must be specified") - } - if res.Resource == "" { - return fmt.Errorf("resource must be specified") - } - return nil -} diff --git a/gateways/core/resource/validate_test.go b/gateways/core/resource/validate_test.go deleted file mode 100644 index 120855a43a..0000000000 --- a/gateways/core/resource/validate_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resource - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateResourceEventSource(t *testing.T) { - convey.Convey("Given a resource event source spec, parse it and make sure no error occurs", t, func() { - ese := &ResourceEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "resource.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.Println(valid.Reason) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/stream/amqp/config.go b/gateways/core/stream/amqp/config.go deleted file mode 100644 index cd5555ad16..0000000000 --- a/gateways/core/stream/amqp/config.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package amqp - -import ( - "github.com/argoproj/argo-events/common" - "github.com/ghodss/yaml" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - amqplib "github.com/streadway/amqp" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// AMQPEventSourceExecutor implements Eventing -type AMQPEventSourceExecutor struct { - Log *logrus.Logger -} - -// amqp contains configuration required to connect to rabbitmq service and process messages -type amqp struct { - // URL for rabbitmq service - URL string `json:"url"` - // ExchangeName is the exchange name - // For more information, visit https://www.rabbitmq.com/tutorials/amqp-concepts.html - ExchangeName string `json:"exchangeName"` - // ExchangeType is rabbitmq exchange type - ExchangeType string `json:"exchangeType"` - // Routing key for bindings - RoutingKey string `json:"routingKey"` - // Backoff holds parameters applied to connection. - Backoff *common.Backoff `json:"backoff,omitempty"` - // Connection manages the serialization and deserialization of frames from IO - // and dispatches the frames to the appropriate channel. - conn *amqplib.Connection - // Maximum number of events consumed from the queue per RatePeriod. - RateLimit uint32 `json:"rateLimit,omitempty"` - // Number of seconds between two consumptions. - RatePeriod uint32 `json:"ratePeriod,omitempty"` -} - -func parseEventSource(eventSource string) (interface{}, error) { - var a *amqp - err := yaml.Unmarshal([]byte(eventSource), &a) - if err != nil { - return nil, err - } - return a, nil -} - -// Validate validates amqp -func (a *amqp) Validate() error { - if (a.RateLimit == 0) != (a.RatePeriod == 0) { - return errors.New("RateLimit and RatePeriod must be either set or omitted") - } - return nil -} diff --git a/gateways/core/stream/amqp/config_test.go b/gateways/core/stream/amqp/config_test.go deleted file mode 100644 index 890604fd75..0000000000 --- a/gateways/core/stream/amqp/config_test.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package amqp - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -url: amqp://amqp.argo-events:5672 -exchangeName: fooExchangeName -exchangeType: fanout -routingKey: fooRoutingKey -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a amqp event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*amqp) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/core/stream/amqp/start.go b/gateways/core/stream/amqp/start.go deleted file mode 100644 index ce383e84aa..0000000000 --- a/gateways/core/stream/amqp/start.go +++ /dev/null @@ -1,148 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package amqp - -import ( - "fmt" - "time" - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - amqplib "github.com/streadway/amqp" - "k8s.io/apimachinery/pkg/util/wait" -) - -// StartEventSource starts an event source -func (ese *AMQPEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*amqp), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func getLimitedDelivery(ch *amqplib.Channel, a *amqp, delivery chan amqplib.Delivery, queue string) { - period := time.Duration(a.RatePeriod) * time.Second - for { - startTime := time.Now() - - for i := uint32(0); i < a.RateLimit; i++ { - msg, ok, err := ch.Get(queue, true) - - if err != nil || ok == false { - break - } - delivery <- msg - - if time.Now().After(startTime.Add(period)) { - startTime = time.Now() - i = 0 - } - } - - remainingTime := startTime.Add(period).Sub(time.Now()) - time.Sleep(remainingTime) - } -} - -func getDelivery(ch *amqplib.Channel, a *amqp) (<-chan amqplib.Delivery, error) { - err := ch.ExchangeDeclare(a.ExchangeName, a.ExchangeType, true, false, false, false, nil) - if err != nil { - return nil, fmt.Errorf("failed to declare exchange with name %s and type %s. err: %+v", a.ExchangeName, a.ExchangeType, err) - } - - q, err := ch.QueueDeclare("", false, false, true, false, nil) - if err != nil { - return nil, fmt.Errorf("failed to declare queue: %s", err) - } - - err = ch.QueueBind(q.Name, a.RoutingKey, a.ExchangeName, false, nil) - if err != nil { - return nil, fmt.Errorf("failed to bind %s exchange '%s' to queue with routingKey: %s: %s", a.ExchangeType, a.ExchangeName, a.RoutingKey, err) - } - - if a.RateLimit != 0 { - delivery := make(chan amqplib.Delivery) - go getLimitedDelivery(ch, a, delivery, q.Name) - return delivery, nil - } - - delivery, err := ch.Consume(q.Name, "", true, false, false, false, nil) - if err != nil { - return nil, fmt.Errorf("failed to begin consuming messages: %s", err) - } - return delivery, nil -} - -func (ese *AMQPEventSourceExecutor) listenEvents(a *amqp, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - if err := gateways.Connect(&wait.Backoff{ - Steps: a.Backoff.Steps, - Factor: a.Backoff.Factor, - Duration: a.Backoff.Duration, - Jitter: a.Backoff.Jitter, - }, func() error { - var err error - a.conn, err = amqplib.Dial(a.URL) - if err != nil { - return err - } - return nil - }); err != nil { - errorCh <- err - return - } - - ch, err := a.conn.Channel() - if err != nil { - errorCh <- err - return - } - - delivery, err := getDelivery(ch, a) - if err != nil { - errorCh <- err - return - } - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("starting to subscribe to messages") - for { - select { - case msg := <-delivery: - dataCh <- msg.Body - case <-doneCh: - err = a.conn.Close() - if err != nil { - log.WithError(err).Info("failed to close connection") - } - return - } - } -} diff --git a/gateways/core/stream/amqp/validate.go b/gateways/core/stream/amqp/validate.go deleted file mode 100644 index a1d018ad2e..0000000000 --- a/gateways/core/stream/amqp/validate.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package amqp - -import ( - "context" - "fmt" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *AMQPEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateAMQP) -} - -func validateAMQP(config interface{}) error { - a := config.(*amqp) - if a == nil { - return gwcommon.ErrNilEventSource - } - if a.URL == "" { - return fmt.Errorf("url must be specified") - } - if a.RoutingKey == "" { - return fmt.Errorf("routing key must be specified") - } - if a.ExchangeName == "" { - return fmt.Errorf("exchange name must be specified") - } - if a.ExchangeType == "" { - return fmt.Errorf("exchange type must be specified") - } - return nil -} diff --git a/gateways/core/stream/amqp/validate_test.go b/gateways/core/stream/amqp/validate_test.go deleted file mode 100644 index f8e91b8d90..0000000000 --- a/gateways/core/stream/amqp/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package amqp - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateAMQPEventSource(t *testing.T) { - convey.Convey("Given a amqp event source spec, parse it and make sure no error occurs", t, func() { - ese := &AMQPEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("../%s/%s", gwcommon.EventSourceDir, "amqp.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/stream/kafka/config.go b/gateways/core/stream/kafka/config.go deleted file mode 100644 index 4ed5a1006c..0000000000 --- a/gateways/core/stream/kafka/config.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kafka - -import ( - "github.com/Shopify/sarama" - "github.com/argoproj/argo-events/common" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// KafkaEventSourceExecutor implements Eventing -type KafkaEventSourceExecutor struct { - Log *logrus.Logger -} - -// kafka defines configuration required to connect to kafka cluster -type kafka struct { - // URL to kafka cluster - URL string `json:"url"` - // Partition name - Partition string `json:"partition"` - // Topic name - Topic string `json:"topic"` - // Backoff holds parameters applied to connection. - Backoff *common.Backoff `json:"backoff,omitempty"` - // Consumer manages PartitionConsumers which process Kafka messages from brokers. - consumer sarama.Consumer -} - -func parseEventSource(eventSource string) (interface{}, error) { - var n *kafka - err := yaml.Unmarshal([]byte(eventSource), &n) - if err != nil { - return nil, err - } - return n, nil -} diff --git a/gateways/core/stream/kafka/config_test.go b/gateways/core/stream/kafka/config_test.go deleted file mode 100644 index c8d6a26a43..0000000000 --- a/gateways/core/stream/kafka/config_test.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kafka - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -url: kafka.argo-events:9092 -topic: foo -partition: "0" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a kafka event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*kafka) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/core/stream/kafka/start.go b/gateways/core/stream/kafka/start.go deleted file mode 100644 index 2a678e0540..0000000000 --- a/gateways/core/stream/kafka/start.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kafka - -import ( - "fmt" - "github.com/Shopify/sarama" - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "k8s.io/apimachinery/pkg/util/wait" - "strconv" -) - -func verifyPartitionAvailable(part int32, partitions []int32) bool { - for _, p := range partitions { - if part == p { - return true - } - } - return false -} - -// StartEventSource starts an event source -func (ese *KafkaEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*kafka), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func (ese *KafkaEventSourceExecutor) listenEvents(k *kafka, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - if err := gateways.Connect(&wait.Backoff{ - Steps: k.Backoff.Steps, - Jitter: k.Backoff.Jitter, - Duration: k.Backoff.Duration, - Factor: k.Backoff.Factor, - }, func() error { - var err error - k.consumer, err = sarama.NewConsumer([]string{k.URL}, nil) - if err != nil { - return err - } - return nil - }); err != nil { - log.WithError(err).WithField(common.LabelURL, k.URL).Error("failed to connect") - errorCh <- err - return - } - - pInt, err := strconv.ParseInt(k.Partition, 10, 32) - if err != nil { - errorCh <- err - return - } - partition := int32(pInt) - - availablePartitions, err := k.consumer.Partitions(k.Topic) - if err != nil { - errorCh <- err - return - } - if ok := verifyPartitionAvailable(partition, availablePartitions); !ok { - errorCh <- fmt.Errorf("partition %d is not available", partition) - return - } - - partitionConsumer, err := k.consumer.ConsumePartition(k.Topic, partition, sarama.OffsetNewest) - if err != nil { - errorCh <- err - return - } - - log.Info("starting to subscribe to messages") - for { - select { - case msg := <-partitionConsumer.Messages(): - dataCh <- msg.Value - - case err := <-partitionConsumer.Errors(): - errorCh <- err - return - - case <-doneCh: - err = partitionConsumer.Close() - if err != nil { - log.WithError(err).Error("failed to close consumer") - } - return - } - } -} diff --git a/gateways/core/stream/kafka/validate.go b/gateways/core/stream/kafka/validate.go deleted file mode 100644 index 2cd8c138f1..0000000000 --- a/gateways/core/stream/kafka/validate.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kafka - -import ( - "context" - "fmt" - - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates the gateway event source -func (ese *KafkaEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateKafka) -} - -func validateKafka(config interface{}) error { - k := config.(*kafka) - if k == nil { - return gwcommon.ErrNilEventSource - } - if k.URL == "" { - return fmt.Errorf("url must be specified") - } - if k.Topic == "" { - return fmt.Errorf("topic must be specified") - } - if k.Partition == "" { - return fmt.Errorf("partition must be specified") - } - return nil -} diff --git a/gateways/core/stream/kafka/validate_test.go b/gateways/core/stream/kafka/validate_test.go deleted file mode 100644 index b3678f73fa..0000000000 --- a/gateways/core/stream/kafka/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kafka - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateKafkaEventSource(t *testing.T) { - convey.Convey("Given a kafka event source spec, parse it and make sure no error occurs", t, func() { - ese := &KafkaEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("../%s/%s", gwcommon.EventSourceDir, "kafka.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/stream/mqtt/config.go b/gateways/core/stream/mqtt/config.go deleted file mode 100644 index 9528ee31bd..0000000000 --- a/gateways/core/stream/mqtt/config.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package mqtt - -import ( - "github.com/argoproj/argo-events/common" - mqttlib "github.com/eclipse/paho.mqtt.golang" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// MqttEventSourceExecutor implements Eventing -type MqttEventSourceExecutor struct { - Log *logrus.Logger -} - -// mqtt contains information to connect to MQTT broker -type mqtt struct { - // URL to connect to broker - URL string `json:"url"` - // Topic name - Topic string `json:"topic"` - // Client ID - ClientId string `json:"clientId"` - // Backoff holds parameters applied to connection. - Backoff *common.Backoff `json:"backoff,omitempty"` - // It is an MQTT client for communicating with an MQTT server - client mqttlib.Client -} - -func parseEventSource(eventSource string) (interface{}, error) { - var m *mqtt - err := yaml.Unmarshal([]byte(eventSource), &m) - if err != nil { - return nil, err - } - return m, nil -} diff --git a/gateways/core/stream/mqtt/start.go b/gateways/core/stream/mqtt/start.go deleted file mode 100644 index eec63dacf6..0000000000 --- a/gateways/core/stream/mqtt/start.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package mqtt - -import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - mqttlib "github.com/eclipse/paho.mqtt.golang" - "k8s.io/apimachinery/pkg/util/wait" -) - -// StartEventSource starts an event source -func (ese *MqttEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*mqtt), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func (ese *MqttEventSourceExecutor) listenEvents(m *mqtt, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithFields( - map[string]interface{}{ - common.LabelEventSource: eventSource.Name, - common.LabelURL: m.URL, - common.LabelClientID: m.ClientId, - }, - ) - - handler := func(c mqttlib.Client, msg mqttlib.Message) { - dataCh <- msg.Payload() - } - opts := mqttlib.NewClientOptions().AddBroker(m.URL).SetClientID(m.ClientId) - - if err := gateways.Connect(&wait.Backoff{ - Factor: m.Backoff.Factor, - Duration: m.Backoff.Duration, - Jitter: m.Backoff.Jitter, - Steps: m.Backoff.Steps, - }, func() error { - client := mqttlib.NewClient(opts) - if token := client.Connect(); token.Wait() && token.Error() != nil { - return token.Error() - } - return nil - }); err != nil { - log.Info("failed to connect") - errorCh <- err - return - } - - log.Info("subscribing to topic") - if token := m.client.Subscribe(m.Topic, 0, handler); token.Wait() && token.Error() != nil { - log.WithError(token.Error()).Error("failed to subscribe") - errorCh <- token.Error() - return - } - - <-doneCh - token := m.client.Unsubscribe(m.Topic) - if token.Error() != nil { - log.WithError(token.Error()).Error("failed to unsubscribe client") - } -} diff --git a/gateways/core/stream/mqtt/validate.go b/gateways/core/stream/mqtt/validate.go deleted file mode 100644 index f3a3a4d8b5..0000000000 --- a/gateways/core/stream/mqtt/validate.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package mqtt - -import ( - "context" - "fmt" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *MqttEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateMQTT) -} - -func validateMQTT(config interface{}) error { - m := config.(*mqtt) - if m == nil { - return gwcommon.ErrNilEventSource - } - if m.URL == "" { - return fmt.Errorf("url must be specified") - } - if m.Topic == "" { - return fmt.Errorf("topic must be specified") - } - if m.ClientId == "" { - return fmt.Errorf("client id must be specified") - } - return nil -} diff --git a/gateways/core/stream/mqtt/validate_test.go b/gateways/core/stream/mqtt/validate_test.go deleted file mode 100644 index d700b9d40b..0000000000 --- a/gateways/core/stream/mqtt/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package mqtt - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateMqttEventSource(t *testing.T) { - convey.Convey("Given a mqtt event source spec, parse it and make sure no error occurs", t, func() { - ese := &MqttEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("../%s/%s", gwcommon.EventSourceDir, "mqtt.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/stream/nats/config.go b/gateways/core/stream/nats/config.go deleted file mode 100644 index cc710ffc10..0000000000 --- a/gateways/core/stream/nats/config.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package nats - -import ( - "github.com/argoproj/argo-events/common" - "github.com/ghodss/yaml" - natslib "github.com/nats-io/go-nats" - "github.com/sirupsen/logrus" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// NatsEventSourceExecutor implements Eventing -type NatsEventSourceExecutor struct { - Log *logrus.Logger -} - -// Nats contains configuration to connect to NATS cluster -type natsConfig struct { - // URL to connect to natsConfig cluster - URL string `json:"url"` - // Subject name - Subject string `json:"subject"` - // Backoff holds parameters applied to connection. - Backoff *common.Backoff `json:"backoff,omitempty"` - // conn represents a bare connection to a nats-server. - conn *natslib.Conn -} - -func parseEventSource(es string) (interface{}, error) { - var n *natsConfig - err := yaml.Unmarshal([]byte(es), &n) - if err != nil { - return nil, err - } - return n, nil -} diff --git a/gateways/core/stream/nats/config_test.go b/gateways/core/stream/nats/config_test.go deleted file mode 100644 index 907edb4adf..0000000000 --- a/gateways/core/stream/nats/config_test.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package nats - -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -var es = ` -url: natsConfig://natsConfig.argo-events:4222 -subject: foo -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a nats event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*natsConfig) - convey.So(ok, convey.ShouldEqual, true) - }) -} diff --git a/gateways/core/stream/nats/start.go b/gateways/core/stream/nats/start.go deleted file mode 100644 index 4f6de230d4..0000000000 --- a/gateways/core/stream/nats/start.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package nats - -import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - natslib "github.com/nats-io/go-nats" - "k8s.io/apimachinery/pkg/util/wait" -) - -// StartEventSource starts an event source -func (ese *NatsEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("operating on event source") - - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - - dataCh := make(chan []byte) - errorCh := make(chan error) - doneCh := make(chan struct{}, 1) - - go ese.listenEvents(config.(*natsConfig), eventSource, dataCh, errorCh, doneCh) - - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, ese.Log) -} - -func (ese *NatsEventSourceExecutor) listenEvents(n *natsConfig, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithFields( - map[string]interface{}{ - common.LabelEventSource: eventSource.Name, - common.LabelURL: n.URL, - "subject": n.Subject, - }, - ) - - if err := gateways.Connect(&wait.Backoff{ - Steps: n.Backoff.Steps, - Jitter: n.Backoff.Jitter, - Duration: n.Backoff.Duration, - Factor: n.Backoff.Factor, - }, func() error { - var err error - if n.conn, err = natslib.Connect(n.URL); err != nil { - return err - } - return nil - }); err != nil { - log.WithError(err).Error("connection failed") - errorCh <- err - return - } - - log.Info("subscribing to messages") - _, err := n.conn.Subscribe(n.Subject, func(msg *natslib.Msg) { - dataCh <- msg.Data - }) - if err != nil { - log.WithError(err).Error("failed to subscribe") - errorCh <- err - return - } - n.conn.Flush() - if err := n.conn.LastError(); err != nil { - errorCh <- err - return - } - - <-doneCh -} diff --git a/gateways/core/stream/nats/validate.go b/gateways/core/stream/nats/validate.go deleted file mode 100644 index 395ccb87b7..0000000000 --- a/gateways/core/stream/nats/validate.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package nats - -import ( - "context" - "fmt" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" -) - -// ValidateEventSource validates gateway event source -func (ese *NatsEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateNATS) -} - -func validateNATS(config interface{}) error { - n := config.(*natsConfig) - if n == nil { - return fmt.Errorf("configuration must be non empty") - } - if n.URL == "" { - return fmt.Errorf("url must be specified") - } - if n.Subject == "" { - return fmt.Errorf("subject must be specified") - } - return nil -} diff --git a/gateways/core/stream/nats/validate_test.go b/gateways/core/stream/nats/validate_test.go deleted file mode 100644 index 951294a5c5..0000000000 --- a/gateways/core/stream/nats/validate_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package nats - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateNatsEventSource(t *testing.T) { - convey.Convey("Given a nats event source spec, parse it and make sure no error occurs", t, func() { - ese := &NatsEventSourceExecutor{} - content, err := ioutil.ReadFile(fmt.Sprintf("../%s/%s", gwcommon.EventSourceDir, "nats.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} diff --git a/gateways/core/webhook/cmd/main.go b/gateways/core/webhook/cmd/main.go deleted file mode 100644 index 6941eaf043..0000000000 --- a/gateways/core/webhook/cmd/main.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/webhook" -) - -func main() { - gateways.StartGateway(&webhook.WebhookEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - }) -} diff --git a/gateways/core/webhook/config.go b/gateways/core/webhook/config.go deleted file mode 100644 index ae37d02620..0000000000 --- a/gateways/core/webhook/config.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" -) - -const ArgoEventsEventSourceVersion = "v0.11" - -// WebhookEventSourceExecutor implements Eventing -type WebhookEventSourceExecutor struct { - Log *logrus.Logger -} - -type RouteConfig struct { - Route *gwcommon.Route -} - -func parseEventSource(es string) (interface{}, error) { - var n *gwcommon.Webhook - err := yaml.Unmarshal([]byte(es), &n) - if err != nil { - return nil, err - } - return n, nil -} diff --git a/gateways/core/webhook/start.go b/gateways/core/webhook/start.go deleted file mode 100644 index 615f19ac96..0000000000 --- a/gateways/core/webhook/start.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - "fmt" - "github.com/argoproj/argo-events/common" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "io/ioutil" - "net/http" - - "github.com/argoproj/argo-events/gateways" -) - -var ( - helper = gwcommon.NewWebhookHelper() -) - -func init() { - go gwcommon.InitRouteChannels(helper) -} - -func (rc *RouteConfig) GetRoute() *gwcommon.Route { - return rc.Route -} - -// RouteHandler handles new route -func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { - var response string - - r := rc.Route - - log := r.Logger.WithFields( - map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - common.LabelHTTPMethod: r.Webhook.Method, - }) - - log.Info("request received") - - if !helper.ActiveEndpoints[r.Webhook.Endpoint].Active { - response = fmt.Sprintf("the route: endpoint %s and method %s is deactived", r.Webhook.Endpoint, r.Webhook.Method) - log.Info("endpoint is not active") - common.SendErrorResponse(writer, response) - return - } - - if r.Webhook.Method != request.Method { - log.WithFields( - map[string]interface{}{ - "expected": r.Webhook.Method, - "actual": request.Method, - }, - ).Warn("method mismatch") - - common.SendErrorResponse(writer, fmt.Sprintf("the method %s is not defined for endpoint %s", r.Webhook.Method, r.Webhook.Endpoint)) - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.WithError(err).Error("failed to parse request body") - common.SendErrorResponse(writer, fmt.Sprintf("failed to parse request. err: %+v", err)) - return - } - - helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh <- body - response = "request successfully processed" - log.Info(response) - common.SendSuccessResponse(writer, response) -} - -func (rc *RouteConfig) PostStart() error { - return nil -} - -func (rc *RouteConfig) PostStop() error { - return nil -} - -// StartEventSource starts a event source -func (ese *WebhookEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) - - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - - log.Info("operating on event source") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } - h := config.(*gwcommon.Webhook) - h.Endpoint = gwcommon.FormatWebhookEndpoint(h.Endpoint) - - return gwcommon.ProcessRoute(&RouteConfig{ - Route: &gwcommon.Route{ - Logger: ese.Log, - EventSource: eventSource, - StartCh: make(chan struct{}), - Webhook: h, - }, - }, helper, eventStream) -} diff --git a/gateways/core/webhook/start_test.go b/gateways/core/webhook/start_test.go deleted file mode 100644 index 3c1025b692..0000000000 --- a/gateways/core/webhook/start_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - "bytes" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/smartystreets/goconvey/convey" - "io/ioutil" - "net/http" - "testing" -) - -func TestRouteActiveHandler(t *testing.T) { - convey.Convey("Given a route configuration", t, func() { - rc := &RouteConfig{ - Route: gwcommon.GetFakeRoute(), - } - r := rc.Route - r.Webhook.Method = http.MethodGet - helper.ActiveEndpoints[r.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } - - writer := &gwcommon.FakeHttpWriter{} - - convey.Convey("Inactive route should return error", func() { - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("hello"))), - }) - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) - }) - - helper.ActiveEndpoints[r.Webhook.Endpoint].Active = true - - convey.Convey("Active route with correct method should return success", func() { - dataCh := make(chan []byte) - go func() { - resp := <-helper.ActiveEndpoints[r.Webhook.Endpoint].DataCh - dataCh <- resp - }() - - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("fake notification"))), - Method: http.MethodGet, - }) - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusOK) - data := <-dataCh - convey.So(string(data), convey.ShouldEqual, "fake notification") - }) - - convey.Convey("Active route with incorrect method should return failure", func() { - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("fake notification"))), - Method: http.MethodHead, - }) - convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) - }) - }) -} diff --git a/gateways/core/webhook/validate.go b/gateways/core/webhook/validate.go deleted file mode 100644 index 65bc6eb5f1..0000000000 --- a/gateways/core/webhook/validate.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - "context" - "fmt" - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "net/http" -) - -// ValidateEventSource validates webhook event source -func (ese *WebhookEventSourceExecutor) ValidateEventSource(ctx context.Context, es *gateways.EventSource) (*gateways.ValidEventSource, error) { - ese.Log.WithFields( - map[string]interface{}{ - common.LabelEventSource: es.Name, - common.LabelVersion: es.Version, - }).Info("validating event source") - return gwcommon.ValidateGatewayEventSource(es, ArgoEventsEventSourceVersion, parseEventSource, validateWebhook) -} - -func validateWebhook(config interface{}) error { - w := config.(*gwcommon.Webhook) - if w == nil { - return gwcommon.ErrNilEventSource - } - - switch w.Method { - case http.MethodHead, http.MethodPut, http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodOptions, http.MethodPatch, http.MethodPost, http.MethodTrace: - default: - return fmt.Errorf("unknown HTTP method %s", w.Method) - } - - return gwcommon.ValidateWebhook(w) -} diff --git a/gateways/core/webhook/validate_test.go b/gateways/core/webhook/validate_test.go deleted file mode 100644 index e79c000bb4..0000000000 --- a/gateways/core/webhook/validate_test.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - "context" - "fmt" - "io/ioutil" - "testing" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" -) - -func TestValidateEventSource(t *testing.T) { - convey.Convey("Given a valid webhook event source spec, parse it and make sure no error occurs", t, func() { - ese := &WebhookEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - } - content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gwcommon.EventSourceDir, "webhook.yaml")) - convey.So(err, convey.ShouldBeNil) - - var cm *corev1.ConfigMap - err = yaml.Unmarshal(content, &cm) - convey.So(err, convey.ShouldBeNil) - convey.So(cm, convey.ShouldNotBeNil) - - err = common.CheckEventSourceVersion(cm) - convey.So(err, convey.ShouldBeNil) - - for key, value := range cm.Data { - valid, _ := ese.ValidateEventSource(context.Background(), &gateways.EventSource{ - Name: key, - Id: common.Hasher(key), - Data: value, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }) - convey.So(valid, convey.ShouldNotBeNil) - convey.So(valid.IsValid, convey.ShouldBeTrue) - } - }) -} - -func TestValidate(t *testing.T) { - convey.Convey("Given a webhook, validate it", t, func() { - w := &gwcommon.Webhook{ - Port: "12000", - Endpoint: "/", - Method: "POST", - } - err := validateWebhook(w) - convey.So(err, convey.ShouldBeNil) - }) -} diff --git a/gateways/event-source_test.go b/gateways/event-source_test.go deleted file mode 100644 index 688c420d5e..0000000000 --- a/gateways/event-source_test.go +++ /dev/null @@ -1,170 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateways - -import ( - "context" - "fmt" - "os" - "sync" - "testing" - - "github.com/argoproj/argo-events/common" - pc "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - gwfake "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/fake" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" -) - -func getGatewayConfig() *GatewayConfig { - return &GatewayConfig{ - Log: common.NewArgoEventsLogger(), - serverPort: "1234", - StatusCh: make(chan EventSourceStatus), - gw: &v1alpha1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-agteway", - Namespace: "test-nm", - }, - Spec: v1alpha1.GatewaySpec{ - Watchers: &v1alpha1.NotificationWatchers{ - Sensors: []v1alpha1.SensorNotificationWatcher{}, - }, - EventProtocol: &pc.EventProtocol{ - Type: pc.HTTP, - Http: pc.Http{ - Port: "9000", - }, - }, - }, - }, - Clientset: fake.NewSimpleClientset(), - gwcs: gwfake.NewSimpleClientset(), - } -} - -type testEventSourceExecutor struct{} - -func (ese *testEventSourceExecutor) StartEventSource(eventSource *EventSource, eventStream Eventing_StartEventSourceServer) error { - defer func() { - if r := recover(); r != nil { - fmt.Println(r) - } - }() - _ = eventStream.Send(&Event{ - Name: eventSource.Name, - Payload: []byte("test payload"), - }) - - <-eventStream.Context().Done() - - return nil -} - -func (ese *testEventSourceExecutor) ValidateEventSource(ctx context.Context, eventSource *EventSource) (*ValidEventSource, error) { - return &ValidEventSource{ - IsValid: true, - }, nil -} - -func TestEventSources(t *testing.T) { - _ = os.Setenv(common.EnvVarGatewayServerPort, "1234") - go StartGateway(&testEventSourceExecutor{}) - gc := getGatewayConfig() - - var eventSrcCtxMap map[string]*EventSourceContext - var eventSourceKeys []string - - convey.Convey("Given a gateway configmap, create event sources", t, func() { - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-configmap", - Namespace: "test-namespace", - Labels: map[string]string{ - common.LabelArgoEventsEventSourceVersion: "v0.11", - }, - }, - Data: map[string]string{ - "event-source-1": ` -testKey: testValue -`, - }, - } - fakeclientset := fake.NewSimpleClientset() - _, err := fakeclientset.CoreV1().ConfigMaps(cm.Namespace).Create(cm) - convey.So(err, convey.ShouldBeNil) - - eventSrcCtxMap, err = gc.createInternalEventSources(cm) - convey.So(err, convey.ShouldBeNil) - convey.So(eventSrcCtxMap, convey.ShouldNotBeNil) - convey.So(len(eventSrcCtxMap), convey.ShouldEqual, 1) - for _, data := range eventSrcCtxMap { - convey.So(data.Source.Data, convey.ShouldEqual, ` -testKey: testValue -`) - convey.So(data.Source.Version, convey.ShouldEqual, "v0.11") - } - }) - - convey.Convey("Given old and new event sources, return diff", t, func() { - gc.registeredConfigs = make(map[string]*EventSourceContext) - staleEventSources, newEventSources := gc.diffEventSources(eventSrcCtxMap) - convey.So(staleEventSources, convey.ShouldBeEmpty) - convey.So(newEventSources, convey.ShouldNotBeEmpty) - convey.So(len(newEventSources), convey.ShouldEqual, 1) - eventSourceKeys = newEventSources - }) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - i := 0 - for event := range gc.StatusCh { - switch event.Phase { - case v1alpha1.NodePhaseRunning: - convey.Convey("Event source is running", t, func() { - convey.So(i, convey.ShouldEqual, 0) - convey.So(event.Message, convey.ShouldEqual, "event_source_is_running") - i++ - go gc.stopEventSources(eventSourceKeys) - }) - case v1alpha1.NodePhaseError: - convey.Convey("Event source is in error", t, func() { - convey.So(i, convey.ShouldNotEqual, 0) - convey.So(event.Message, convey.ShouldEqual, "failed_to_receive_event_from_event_source_stream") - }) - - case v1alpha1.NodePhaseRemove: - convey.Convey("Event source should be removed", t, func() { - convey.So(i, convey.ShouldNotEqual, 0) - convey.So(event.Message, convey.ShouldEqual, "event_source_is_removed") - }) - goto end - } - } - end: - wg.Done() - }() - - convey.Convey("Given new event sources, start consuming events", t, func() { - gc.startEventSources(eventSrcCtxMap, eventSourceKeys) - wg.Wait() - }) -} diff --git a/gateways/event-sources.go b/gateways/event-sources.go deleted file mode 100644 index 1842820287..0000000000 --- a/gateways/event-sources.go +++ /dev/null @@ -1,275 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateways - -import ( - "context" - "fmt" - "io" - "time" - - "github.com/argoproj/argo-events/pkg/apis/gateway" - - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - "google.golang.org/grpc" - "google.golang.org/grpc/connectivity" - corev1 "k8s.io/api/core/v1" -) - -// createInternalEventSources creates an internal representation of event source declared in the gateway configmap. -// returned event sources are map of hash of event source and event source itself. -// Creating a hash of event source makes it easy to check equality of two event sources. -func (gc *GatewayConfig) createInternalEventSources(cm *corev1.ConfigMap) (map[string]*EventSourceContext, error) { - configs := make(map[string]*EventSourceContext) - for configKey, configValue := range cm.Data { - hashKey := common.Hasher(configKey + configValue) - gc.Log.WithFields( - map[string]interface{}{ - "config-key": configKey, - "config-value": configValue, - "hash": string(hashKey), - }, - ).Info("event source") - - // create a connection to gateway server - ctx, cancel := context.WithCancel(context.Background()) - conn, err := grpc.Dial( - fmt.Sprintf("localhost:%s", gc.serverPort), - grpc.WithBlock(), - grpc.WithInsecure(), - grpc.WithTimeout(common.ServerConnTimeout*time.Second)) - if err != nil { - gc.Log.WithError(err).Panic("failed to connect to gateway server") - cancel() - return nil, err - } - - gc.Log.WithField("state", conn.GetState().String()).Info("state of the connection") - - configs[hashKey] = &EventSourceContext{ - Source: &EventSource{ - Id: hashKey, - Name: configKey, - Data: configValue, - Version: cm.Labels[common.LabelArgoEventsEventSourceVersion], - }, - Cancel: cancel, - Ctx: ctx, - Client: NewEventingClient(conn), - Conn: conn, - } - } - return configs, nil -} - -// diffConfig diffs currently registered event sources and the event sources in the gateway configmap -// It simply matches the event source strings. So, if event source string differs through some sequence of definition -// and although the event sources are actually same, this method will treat them as different event sources. -// retunrs staleConfig - event sources to be removed from gateway -// newConfig - new event sources to run -func (gc *GatewayConfig) diffEventSources(newConfigs map[string]*EventSourceContext) (staleConfigKeys []string, newConfigKeys []string) { - var currentConfigKeys []string - var updatedConfigKeys []string - - for currentConfigKey := range gc.registeredConfigs { - currentConfigKeys = append(currentConfigKeys, currentConfigKey) - } - for updatedConfigKey := range newConfigs { - updatedConfigKeys = append(updatedConfigKeys, updatedConfigKey) - } - - gc.Log.WithField("current-event-sources-keys", currentConfigKeys).Debug("event sources hashes") - gc.Log.WithField("updated-event-sources-keys", updatedConfigKeys).Debug("event sources hashes") - - swapped := false - // iterates over current event sources and updated event sources - // and creates two arrays, first one containing event sources that need to removed - // and second containing new event sources that need to be added and run. - for i := 0; i < 2; i++ { - for _, cc := range currentConfigKeys { - found := false - for _, uc := range updatedConfigKeys { - if cc == uc { - found = true - break - } - } - if !found { - if swapped { - newConfigKeys = append(newConfigKeys, cc) - } else { - staleConfigKeys = append(staleConfigKeys, cc) - } - } - } - if i == 0 { - currentConfigKeys, updatedConfigKeys = updatedConfigKeys, currentConfigKeys - swapped = true - } - } - return -} - -// startEventSources starts new event sources added to gateway -func (gc *GatewayConfig) startEventSources(eventSources map[string]*EventSourceContext, keys []string) { - for _, key := range keys { - eventSource := eventSources[key] - // register the event source - gc.registeredConfigs[key] = eventSource - - log := gc.Log.WithField(common.LabelEventSource, eventSource.Source.Name) - - log.Info("activating new event source") - - go func() { - // conn should be in READY state - if eventSource.Conn.GetState() != connectivity.Ready { - gc.Log.Error("connection is not in ready state.") - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseError, - Id: eventSource.Source.Id, - Message: "connection_is_not_in_ready_state", - Name: eventSource.Source.Name, - } - return - } - - // validate event source - if valid, _ := eventSource.Client.ValidateEventSource(eventSource.Ctx, eventSource.Source); !valid.IsValid { - gc.Log.WithFields( - map[string]interface{}{ - "validation-failure": valid.Reason, - }, - ).Error("event source is not valid") - if err := eventSource.Conn.Close(); err != nil { - gc.Log.WithError(err).Error("failed to close client connection") - } - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseError, - Id: eventSource.Source.Id, - Message: "event_source_is_not_valid", - Name: eventSource.Source.Name, - } - return - } - - gc.Log.Info("event source is valid") - - // mark event source as running - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseRunning, - Message: "event_source_is_running", - Id: eventSource.Source.Id, - Name: eventSource.Source.Name, - } - - // listen to events from gateway server - eventStream, err := eventSource.Client.StartEventSource(eventSource.Ctx, eventSource.Source) - if err != nil { - gc.Log.WithError(err).Error("error occurred while starting event source") - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseError, - Message: "failed_to_receive_event_stream", - Name: eventSource.Source.Name, - Id: eventSource.Source.Id, - } - return - } - - gc.Log.Info("started listening to events from gateway server") - for { - event, err := eventStream.Recv() - if err != nil { - if err == io.EOF { - gc.Log.Info("event source has stopped") - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseCompleted, - Message: "event_source_has_been_stopped", - Name: eventSource.Source.Name, - Id: eventSource.Source.Id, - } - return - } - - gc.Log.WithError(err).Error("failed to receive event from stream") - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseError, - Message: "failed_to_receive_event_from_event_source_stream", - Name: eventSource.Source.Name, - Id: eventSource.Source.Id, - } - return - } - err = gc.DispatchEvent(event) - if err != nil { - // escalate error through a K8s event - labels := map[string]string{ - common.LabelEventType: string(common.EscalationEventType), - common.LabelGatewayEventSourceName: eventSource.Source.Name, - common.LabelGatewayName: gc.Name, - common.LabelGatewayEventSourceID: eventSource.Source.Id, - common.LabelOperation: "dispatch_event_to_watchers", - } - if err := common.GenerateK8sEvent(gc.Clientset, fmt.Sprintf("failed to dispatch event to watchers"), common.EscalationEventType, "event dispatch failed", gc.Name, gc.Namespace, gc.controllerInstanceID, gateway.Kind, labels); err != nil { - gc.Log.WithError(err).Error("failed to create K8s event to escalate event dispatch failure") - } - gc.Log.WithError(err).Error("failed to dispatch event to watchers") - } - } - }() - } -} - -// stopEventSources stops an existing event sources -func (gc *GatewayConfig) stopEventSources(configs []string) { - for _, configKey := range configs { - eventSource := gc.registeredConfigs[configKey] - delete(gc.registeredConfigs, configKey) - gc.Log.WithField(common.LabelEventSource, eventSource.Source.Name).Info("removing the event source") - gc.StatusCh <- EventSourceStatus{ - Phase: v1alpha1.NodePhaseRemove, - Id: eventSource.Source.Id, - Message: "event_source_is_removed", - Name: eventSource.Source.Name, - } - eventSource.Cancel() - if err := eventSource.Conn.Close(); err != nil { - gc.Log.WithField(common.LabelEventSource, eventSource.Source.Name).WithError(err).Error("failed to close client connection") - } - } -} - -// manageEventSources syncs registered event sources and updated gateway configmap -func (gc *GatewayConfig) manageEventSources(cm *corev1.ConfigMap) error { - eventSources, err := gc.createInternalEventSources(cm) - if err != nil { - return err - } - - staleEventSources, newEventSources := gc.diffEventSources(eventSources) - gc.Log.WithField(common.LabelEventSource, staleEventSources).Info("stale event sources") - gc.Log.WithField(common.LabelEventSource, newEventSources).Info("new event sources") - - // stop existing event sources - gc.stopEventSources(staleEventSources) - - // start new event sources - gc.startEventSources(eventSources, newEventSources) - - return nil -} diff --git a/gateways/eventing.pb.go b/gateways/eventing.pb.go index 8ce0f0f223..fb1e1e9dff 100644 --- a/gateways/eventing.pb.go +++ b/gateways/eventing.pb.go @@ -8,8 +8,6 @@ import ( fmt "fmt" proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" math "math" ) @@ -32,9 +30,9 @@ type EventSource struct { // The event source name. Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // The event source configuration value. - Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` - // Version of the event source - Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"` + Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + // Type of the event source + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -79,16 +77,16 @@ func (m *EventSource) GetName() string { return "" } -func (m *EventSource) GetData() string { +func (m *EventSource) GetValue() []byte { if m != nil { - return m.Data + return m.Value } - return "" + return nil } -func (m *EventSource) GetVersion() string { +func (m *EventSource) GetType() string { if m != nil { - return m.Version + return m.Type } return "" } @@ -204,22 +202,23 @@ func init() { func init() { proto.RegisterFile("eventing.proto", fileDescriptor_2abcc01b0da84106) } var fileDescriptor_2abcc01b0da84106 = []byte{ - // 237 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x91, 0xc1, 0x4a, 0xc3, 0x40, - 0x10, 0x86, 0xd9, 0x58, 0x9b, 0x38, 0x4a, 0x2d, 0x23, 0xca, 0xd2, 0x93, 0xe4, 0xe4, 0x29, 0x88, - 0xe2, 0xcd, 0xa3, 0x05, 0xcf, 0x29, 0x78, 0x95, 0xd1, 0x1d, 0xca, 0x42, 0xdd, 0x2d, 0x9b, 0xb5, - 0xd2, 0xd7, 0xf0, 0x89, 0x25, 0x63, 0x56, 0x97, 0x9c, 0x7a, 0x9b, 0xff, 0x0b, 0x7c, 0xff, 0x4c, - 0x16, 0x66, 0xbc, 0x63, 0x17, 0xad, 0x5b, 0x37, 0xdb, 0xe0, 0xa3, 0xc7, 0x6a, 0x4d, 0x91, 0xbf, - 0x68, 0xdf, 0xd5, 0xaf, 0x70, 0xba, 0xec, 0xbf, 0xad, 0xfc, 0x67, 0x78, 0x67, 0x9c, 0x41, 0x61, - 0x8d, 0x56, 0xd7, 0xea, 0xe6, 0xa4, 0x2d, 0xac, 0x41, 0x84, 0x89, 0xa3, 0x0f, 0xd6, 0x85, 0x10, - 0x99, 0x7b, 0x66, 0x28, 0x92, 0x3e, 0xfa, 0x65, 0xfd, 0x8c, 0x1a, 0xca, 0x1d, 0x87, 0xce, 0x7a, - 0xa7, 0x27, 0x82, 0x53, 0xac, 0x1f, 0xe0, 0x58, 0x0a, 0xfe, 0x54, 0x2a, 0x53, 0x69, 0x28, 0xb7, - 0xb4, 0xdf, 0x78, 0x32, 0xd2, 0x70, 0xd6, 0xa6, 0x58, 0x3f, 0xc1, 0xfc, 0x85, 0x36, 0xd6, 0xe4, - 0xcb, 0x69, 0x28, 0x6d, 0x27, 0x54, 0x24, 0x55, 0x9b, 0x22, 0x5e, 0xc1, 0x34, 0x30, 0x75, 0xde, - 0x0d, 0x8b, 0x0e, 0xe9, 0xee, 0x5b, 0x41, 0xb5, 0x1c, 0x4e, 0xc7, 0x47, 0x98, 0xaf, 0x22, 0x85, - 0x98, 0x2b, 0x2f, 0x9b, 0xf4, 0x27, 0x9a, 0x0c, 0x2f, 0xce, 0x47, 0xf8, 0x56, 0xe1, 0x33, 0x5c, - 0x48, 0x17, 0x45, 0x3e, 0x40, 0xb0, 0xf8, 0xc7, 0xe3, 0x33, 0xde, 0xa6, 0xf2, 0x06, 0xf7, 0x3f, - 0x01, 0x00, 0x00, 0xff, 0xff, 0xf3, 0x93, 0xbe, 0x7e, 0x95, 0x01, 0x00, 0x00, + // 241 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0x4d, 0x4b, 0xc3, 0x40, + 0x10, 0x86, 0xd9, 0xd8, 0x8f, 0x38, 0x96, 0x5a, 0xc6, 0x0f, 0x96, 0x9e, 0x4a, 0x4e, 0x3d, 0x05, + 0x51, 0xbc, 0x79, 0xb4, 0xe0, 0x39, 0x05, 0x2f, 0x9e, 0x46, 0x33, 0x94, 0x85, 0xb8, 0x1b, 0x36, + 0xdb, 0x4a, 0xfe, 0x86, 0xbf, 0x58, 0x32, 0x4d, 0x74, 0xe9, 0xc9, 0xdb, 0xbc, 0xcf, 0x0e, 0xcf, + 0xce, 0x0c, 0xcc, 0xf9, 0xc0, 0x36, 0x18, 0xbb, 0xcb, 0x6b, 0xef, 0x82, 0xc3, 0x74, 0x47, 0x81, + 0xbf, 0xa8, 0x6d, 0xb2, 0x37, 0xb8, 0xd8, 0x74, 0x6f, 0x5b, 0xb7, 0xf7, 0x1f, 0x8c, 0x73, 0x48, + 0x4c, 0xa9, 0xd5, 0x4a, 0xad, 0xcf, 0x8b, 0xc4, 0x94, 0x88, 0x30, 0xb2, 0xf4, 0xc9, 0x3a, 0x11, + 0x22, 0x35, 0x5e, 0xc3, 0xf8, 0x40, 0xd5, 0x9e, 0xf5, 0xd9, 0x4a, 0xad, 0x67, 0xc5, 0x31, 0x74, + 0x9d, 0xa1, 0xad, 0x59, 0x8f, 0x8e, 0x9d, 0x5d, 0x9d, 0x3d, 0xc2, 0x58, 0xe4, 0xbf, 0x1a, 0x15, + 0x69, 0x34, 0x4c, 0x6b, 0x6a, 0x2b, 0x47, 0xa5, 0xd8, 0x67, 0xc5, 0x10, 0xb3, 0x67, 0x58, 0xbc, + 0x52, 0x65, 0xca, 0x78, 0x30, 0x0d, 0x53, 0xd3, 0x08, 0x15, 0x49, 0x5a, 0x0c, 0x11, 0x6f, 0x61, + 0xe2, 0x99, 0x1a, 0x67, 0xfb, 0x21, 0xfb, 0x74, 0xff, 0xad, 0x20, 0xdd, 0xf4, 0x6b, 0xe3, 0x13, + 0x2c, 0xb6, 0x81, 0x7c, 0x88, 0x95, 0x37, 0xf9, 0x70, 0x85, 0x3c, 0xc2, 0xcb, 0xcb, 0x13, 0x7c, + 0xa7, 0xf0, 0x05, 0xae, 0xe4, 0x2f, 0x0a, 0xfc, 0x0f, 0xc1, 0xf2, 0x0f, 0x9f, 0xae, 0xf1, 0x3e, + 0x91, 0xfb, 0x3f, 0xfc, 0x04, 0x00, 0x00, 0xff, 0xff, 0x1a, 0xc0, 0xda, 0xf1, 0x91, 0x01, 0x00, + 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -297,17 +296,6 @@ type EventingServer interface { ValidateEventSource(context.Context, *EventSource) (*ValidEventSource, error) } -// UnimplementedEventingServer can be embedded to have forward compatible implementations. -type UnimplementedEventingServer struct { -} - -func (*UnimplementedEventingServer) StartEventSource(req *EventSource, srv Eventing_StartEventSourceServer) error { - return status.Errorf(codes.Unimplemented, "method StartEventSource not implemented") -} -func (*UnimplementedEventingServer) ValidateEventSource(ctx context.Context, req *EventSource) (*ValidEventSource, error) { - return nil, status.Errorf(codes.Unimplemented, "method ValidateEventSource not implemented") -} - func RegisterEventingServer(s *grpc.Server, srv EventingServer) { s.RegisterService(&_Eventing_serviceDesc, srv) } diff --git a/gateways/eventing.proto b/gateways/eventing.proto index d45e592d0e..aca646cf90 100644 --- a/gateways/eventing.proto +++ b/gateways/eventing.proto @@ -11,9 +11,9 @@ package gateways; */ service Eventing { // StartEventSource starts an event source and returns stream of events. - rpc StartEventSource(EventSource) returns (stream Event); + rpc StartEventSource (EventSource) returns (stream Event); // ValidateEventSource validates an event source. - rpc ValidateEventSource(EventSource) returns (ValidEventSource); + rpc ValidateEventSource (EventSource) returns (ValidEventSource); } /** @@ -25,9 +25,9 @@ message EventSource { // The event source name. string name = 2; // The event source configuration value. - string data = 3; - // Version of the event source - string version = 4; + bytes value = 3; + // Type of the event source + string type = 4; } /** diff --git a/gateways/core/stream/amqp/Dockerfile b/gateways/server/amqp/Dockerfile similarity index 100% rename from gateways/core/stream/amqp/Dockerfile rename to gateways/server/amqp/Dockerfile diff --git a/gateways/core/file/cmd/main.go b/gateways/server/amqp/cmd/main.go similarity index 77% rename from gateways/core/file/cmd/main.go rename to gateways/server/amqp/cmd/main.go index 223ccfdb24..55b914bbaa 100644 --- a/gateways/core/file/cmd/main.go +++ b/gateways/server/amqp/cmd/main.go @@ -18,12 +18,12 @@ package main import ( "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/file" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/amqp" ) func main() { - gateways.StartGateway(&file.FileEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&amqp.EventListener{ + Logger: common.NewArgoEventsLogger(), }) } diff --git a/gateways/server/amqp/start.go b/gateways/server/amqp/start.go new file mode 100644 index 0000000000..2e41349e60 --- /dev/null +++ b/gateways/server/amqp/start.go @@ -0,0 +1,135 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package amqp + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + amqplib "github.com/streadway/amqp" + "k8s.io/apimachinery/pkg/util/wait" +) + +// EventListener implements Eventing for amqp event source +type EventListener struct { + // Logger logs stuff + Logger *logrus.Logger +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listens to events from amqp server +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing the event source...") + var amqpEventSource *v1alpha1.AMQPEventSource + if err := yaml.Unmarshal(eventSource.Value, &amqpEventSource); err != nil { + errorCh <- err + return + } + + var conn *amqplib.Connection + + logger.Infoln("dialing connection...") + if err := server.Connect(&wait.Backoff{ + Steps: amqpEventSource.ConnectionBackoff.Steps, + Factor: amqpEventSource.ConnectionBackoff.Factor, + Duration: amqpEventSource.ConnectionBackoff.Duration, + Jitter: amqpEventSource.ConnectionBackoff.Jitter, + }, func() error { + var err error + conn, err = amqplib.Dial(amqpEventSource.URL) + if err != nil { + return err + } + return nil + }); err != nil { + errorCh <- err + return + } + + logger.Infoln("opening the server channel...") + ch, err := conn.Channel() + if err != nil { + errorCh <- err + return + } + + logger.Infoln("setting up the delivery channel...") + delivery, err := getDelivery(ch, amqpEventSource) + if err != nil { + errorCh <- err + return + } + + logger.Info("listening to messages on channel...") + for { + select { + case msg := <-delivery: + logger.Infoln("dispatching event on data channel...") + dataCh <- msg.Body + case <-doneCh: + err = conn.Close() + if err != nil { + logger.WithError(err).Info("failed to close connection") + } + return + } + } +} + +// getDelivery sets up a channel for message deliveries +func getDelivery(ch *amqplib.Channel, eventSource *v1alpha1.AMQPEventSource) (<-chan amqplib.Delivery, error) { + err := ch.ExchangeDeclare(eventSource.ExchangeName, eventSource.ExchangeType, true, false, false, false, nil) + if err != nil { + return nil, errors.Errorf("failed to declare exchange with name %s and type %s. err: %+v", eventSource.ExchangeName, eventSource.ExchangeType, err) + } + + q, err := ch.QueueDeclare("", false, false, true, false, nil) + if err != nil { + return nil, errors.Errorf("failed to declare queue: %s", err) + } + + err = ch.QueueBind(q.Name, eventSource.RoutingKey, eventSource.ExchangeName, false, nil) + if err != nil { + return nil, errors.Errorf("failed to bind %s exchange '%s' to queue with routingKey: %s: %s", eventSource.ExchangeType, eventSource.ExchangeName, eventSource.RoutingKey, err) + } + + delivery, err := ch.Consume(q.Name, "", true, false, false, false, nil) + if err != nil { + return nil, errors.Errorf("failed to begin consuming messages: %s", err) + } + return delivery, nil +} diff --git a/gateways/server/amqp/validate.go b/gateways/server/amqp/validate.go new file mode 100644 index 0000000000..fd251f1b40 --- /dev/null +++ b/gateways/server/amqp/validate.go @@ -0,0 +1,78 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package amqp + +import ( + "context" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/pkg/errors" +) + +// ValidateEventSource validates gateway event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.AMQPEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.AMQPEvent)), + }, nil + } + + var amqpEventSource *v1alpha1.AMQPEventSource + if err := yaml.Unmarshal(eventSource.Value, &amqpEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(amqpEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate amqp event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.AMQPEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.URL == "" { + return errors.New("url must be specified") + } + if eventSource.RoutingKey == "" { + return errors.New("routing key must be specified") + } + if eventSource.ExchangeName == "" { + return errors.New("exchange name must be specified") + } + if eventSource.ExchangeType == "" { + return errors.New("exchange type must be specified") + } + return nil +} diff --git a/gateways/server/amqp/validate_test.go b/gateways/server/amqp/validate_test.go new file mode 100644 index 0000000000..1c1209b291 --- /dev/null +++ b/gateways/server/amqp/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package amqp + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateAMQPEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "amqp", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("amqp"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "amqp.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.AMQP { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "amqp", + Value: content, + Type: "amqp", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/aws-sns/Dockerfile b/gateways/server/aws-sns/Dockerfile similarity index 100% rename from gateways/community/aws-sns/Dockerfile rename to gateways/server/aws-sns/Dockerfile diff --git a/gateways/community/aws-sns/cmd/main.go b/gateways/server/aws-sns/cmd/main.go similarity index 70% rename from gateways/community/aws-sns/cmd/main.go rename to gateways/server/aws-sns/cmd/main.go index 01bbbf1e1b..88a5ce54b2 100644 --- a/gateways/community/aws-sns/cmd/main.go +++ b/gateways/server/aws-sns/cmd/main.go @@ -20,8 +20,8 @@ import ( "os" "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/aws-sns" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/aws-sns" "k8s.io/client-go/kubernetes" ) @@ -32,13 +32,9 @@ func main() { panic(err) } clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) - if !ok { - panic("namespace is not provided") - } - gateways.StartGateway(&aws_sns.SNSEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - Clientset: clientset, - Namespace: namespace, + + server.StartGateway(&aws_sns.EventListener{ + Logger: common.NewArgoEventsLogger(), + K8sClient: clientset, }) } diff --git a/gateways/server/aws-sns/start.go b/gateways/server/aws-sns/start.go new file mode 100644 index 0000000000..6484435660 --- /dev/null +++ b/gateways/server/aws-sns/start.go @@ -0,0 +1,187 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_sns + +import ( + "io/ioutil" + "net/http" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + commonaws "github.com/argoproj/argo-events/gateways/server/common/aws" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + snslib "github.com/aws/aws-sdk-go/service/sns" + "github.com/ghodss/yaml" +) + +var ( + // controller controls the webhook operations + controller = webhook.NewController() +) + +// set up route activation and deactivation channels +func init() { + go webhook.ProcessRouteStatus(controller) +} + +// Implement Router +// 1. GetRoute +// 2. HandleRoute +// 3. PostActivate +// 4. PostDeactivate + +// GetRoute returns the route +func (router *Router) GetRoute() *webhook.Route { + return router.Route +} + +// HandleRoute handles new routes +func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { + route := router.Route + + logger := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelPort: route.Context.Port, + common.LabelHTTPMethod: route.Context.Method, + }) + + logger.Info("request received from event source") + + if !route.Active { + logger.Info("endpoint is not active, won't process the request") + common.SendErrorResponse(writer, "inactive endpoint") + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + logger.WithError(err).Error("failed to parse the request body") + common.SendErrorResponse(writer, err.Error()) + return + } + + logger.WithField("body", string(body)).Debugln("request body") + + var notification *httpNotification + err = yaml.Unmarshal(body, ¬ification) + if err != nil { + logger.WithError(err).Error("failed to convert request payload into sns notification") + common.SendErrorResponse(writer, err.Error()) + return + } + + switch notification.Type { + case messageTypeSubscriptionConfirmation: + awsSession := router.session + response, err := awsSession.ConfirmSubscription(&snslib.ConfirmSubscriptionInput{ + TopicArn: &router.eventSource.TopicArn, + Token: ¬ification.Token, + }) + if err != nil { + logger.WithError(err).Error("failed to send confirmation response to aws sns") + common.SendErrorResponse(writer, err.Error()) + return + } + logger.Infoln("subscription successfully confirmed to aws sns") + router.subscriptionArn = response.SubscriptionArn + + case messageTypeNotification: + logger.Infoln("dispatching notification on route's data channel") + route.DataCh <- body + } + + logger.Info("request has been successfully processed") +} + +// PostActivate refers to operations performed after a route is successfully activated +func (router *Router) PostActivate() error { + route := router.Route + + logger := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelPort: route.Context.Port, + common.LabelHTTPMethod: route.Context.Method, + "topic-arn": router.eventSource.TopicArn, + }) + + // In order to successfully subscribe to sns topic, + // 1. Fetch credentials if configured explicitly. Users can use something like https://github.com/jtblin/kube2iam + // which will help not configure creds explicitly. + // 2. Get AWS session + // 3. Subscribe to a topic + + logger.Info("subscribing to sns topic...") + + snsEventSource := router.eventSource + + awsSession, err := commonaws.CreateAWSSession(router.k8sClient, snsEventSource.Namespace, snsEventSource.Region, snsEventSource.AccessKey, snsEventSource.SecretKey) + if err != nil { + return err + } + + router.session = snslib.New(awsSession) + formattedUrl := common.FormattedURL(snsEventSource.Webhook.URL, snsEventSource.Webhook.Endpoint) + if _, err := router.session.Subscribe(&snslib.SubscribeInput{ + Endpoint: &formattedUrl, + Protocol: &snsProtocol, + TopicArn: &snsEventSource.TopicArn, + }); err != nil { + return err + } + + return nil +} + +// PostInactive refers to operations performed after a route is successfully inactivated +func (router *Router) PostInactivate() error { + // After event source is removed, the subscription is cancelled. + if _, err := router.session.Unsubscribe(&snslib.UnsubscribeInput{ + SubscriptionArn: router.subscriptionArn, + }); err != nil { + return err + } + return nil +} + +// StartEventSource starts an SNS event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + logger.Info("started processing the event source...") + + var snsEventSource *v1alpha1.SNSEventSource + if err := yaml.Unmarshal(eventSource.Value, &snsEventSource); err != nil { + logger.WithError(err).Error("failed to parse event source") + return err + } + + route := webhook.NewRoute(snsEventSource.Webhook, listener.Logger, eventSource) + + logger.Infoln("operating on the route...") + return webhook.ManageRoute(&Router{ + Route: route, + eventSource: snsEventSource, + k8sClient: listener.K8sClient, + }, controller, eventStream) +} diff --git a/gateways/community/aws-sns/config.go b/gateways/server/aws-sns/types.go similarity index 61% rename from gateways/community/aws-sns/config.go rename to gateways/server/aws-sns/types.go index b6d74d4500..a44d338b4a 100644 --- a/gateways/community/aws-sns/config.go +++ b/gateways/server/aws-sns/types.go @@ -17,18 +17,15 @@ limitations under the License. package aws_sns import ( - "github.com/sirupsen/logrus" "time" - gwcommon "github.com/argoproj/argo-events/gateways/common" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" snslib "github.com/aws/aws-sdk-go/service/sns" - "github.com/ghodss/yaml" - corev1 "k8s.io/api/core/v1" + "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" ) -const ArgoEventsEventSourceVersion = "v0.11" - const ( messageTypeSubscriptionConfirmation = "SubscriptionConfirmation" messageTypeNotification = "Notification" @@ -38,23 +35,26 @@ var ( snsProtocol = "http" ) -// SNSEventSourceExecutor implements Eventing -type SNSEventSourceExecutor struct { - Log *logrus.Logger - // Clientset is kubernetes client - Clientset kubernetes.Interface - // Namespace where gateway is deployed - Namespace string +// EventListener implements Eventing for aws sns event source +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger + // K8sClient is kubernetes client + K8sClient kubernetes.Interface } -// RouteConfig contains information for a route -type RouteConfig struct { - Route *gwcommon.Route - snses *snsEventSource - session *snslib.SNS +// Router contains information for a route +type Router struct { + // Route contains webhook context and configuration related to api route + Route *webhook.Route + // eventSource refers to sns event source configuration + eventSource *v1alpha1.SNSEventSource + // session refers to aws session + session *snslib.SNS + // subscriptionArn is sns arn subscriptionArn *string - clientset kubernetes.Interface - namespace string + // k8sClient is Kubernetes client + k8sClient kubernetes.Interface } // Json http notifications @@ -76,22 +76,3 @@ type httpNotification struct { SigningCertURL string `json:"SigningCertURL"` UnsubscribeURL string `json:"UnsubscribeURL,omitempty"` // Only for notifications } - -// snsEventSource contains configuration to subscribe to SNS topic -type snsEventSource struct { - // Hook defines a webhook. - Hook *gwcommon.Webhook `json:"hook"` - TopicArn string `json:"topicArn"` - AccessKey *corev1.SecretKeySelector `json:"accessKey" protobuf:"bytes,5,opt,name=accessKey"` - SecretKey *corev1.SecretKeySelector `json:"secretKey" protobuf:"bytes,6,opt,name=secretKey"` - Region string `json:"region"` -} - -func parseEventSource(es string) (interface{}, error) { - var ses *snsEventSource - err := yaml.Unmarshal([]byte(es), &ses) - if err != nil { - return nil, err - } - return ses, nil -} diff --git a/gateways/server/aws-sns/validate.go b/gateways/server/aws-sns/validate.go new file mode 100644 index 0000000000..0d0d00bca3 --- /dev/null +++ b/gateways/server/aws-sns/validate.go @@ -0,0 +1,71 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_sns + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates sns event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.SNSEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.SNSEvent)), + }, nil + } + + var snsEventSource *v1alpha1.SNSEventSource + if err := yaml.Unmarshal(eventSource.Value, &snsEventSource); err != nil { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(snsEventSource); err != nil { + return &gateways.ValidEventSource{ + Reason: err.Error(), + IsValid: false, + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(snsEventSource *v1alpha1.SNSEventSource) error { + if snsEventSource == nil { + return common.ErrNilEventSource + } + if snsEventSource.TopicArn == "" { + return fmt.Errorf("must specify topic arn") + } + if snsEventSource.Region == "" { + return fmt.Errorf("must specify region") + } + return webhook.ValidateWebhookContext(snsEventSource.Webhook) +} diff --git a/gateways/server/aws-sns/validate_test.go b/gateways/server/aws-sns/validate_test.go new file mode 100644 index 0000000000..84b346a733 --- /dev/null +++ b/gateways/server/aws-sns/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_sns + +import ( + "context" + "fmt" + "github.com/argoproj/argo-events/common" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/gateways" + esv1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestSNSEventSourceExecutor_ValidateEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "sns", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("sns"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "aws-sns.yaml")) + assert.Nil(t, err) + + var eventSource *esv1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.SNS { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "sns", + Value: content, + Type: "sns", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/aws-sqs/Dockerfile b/gateways/server/aws-sqs/Dockerfile similarity index 100% rename from gateways/community/aws-sqs/Dockerfile rename to gateways/server/aws-sqs/Dockerfile diff --git a/gateways/core/artifact/cmd/main.go b/gateways/server/aws-sqs/cmd/main.go similarity index 76% rename from gateways/core/artifact/cmd/main.go rename to gateways/server/aws-sqs/cmd/main.go index 2e1688301f..52a498d568 100644 --- a/gateways/core/artifact/cmd/main.go +++ b/gateways/server/aws-sqs/cmd/main.go @@ -20,8 +20,8 @@ import ( "os" "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/artifact" + "github.com/argoproj/argo-events/gateways/server" + aws_sqs "github.com/argoproj/argo-events/gateways/server/aws-sqs" "k8s.io/client-go/kubernetes" ) @@ -32,13 +32,15 @@ func main() { panic(err) } clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) + + namespace, ok := os.LookupEnv(common.EnvVarNamespace) if !ok { panic("namespace is not provided") } - gateways.StartGateway(&artifact.S3EventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - Clientset: clientset, + + server.StartGateway(&aws_sqs.EventListener{ + Logger: common.NewArgoEventsLogger(), + K8sClient: clientset, Namespace: namespace, }) } diff --git a/gateways/server/aws-sqs/start.go b/gateways/server/aws-sqs/start.go new file mode 100644 index 0000000000..972bb737fa --- /dev/null +++ b/gateways/server/aws-sqs/start.go @@ -0,0 +1,121 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_sqs + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + commonaws "github.com/argoproj/argo-events/gateways/server/common/aws" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + sqslib "github.com/aws/aws-sdk-go/service/sqs" + "github.com/ghodss/yaml" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" +) + +// EventListener implements Eventing for aws sqs event source +type EventListener struct { + Logger *logrus.Logger + // k8sClient is kubernetes client + K8sClient kubernetes.Interface + // Namespace where gateway is deployed + Namespace string +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + log := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + log.Info("started processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents fires an event when interval completes and item is processed from queue. +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + var sqsEventSource *v1alpha1.SQSEventSource + if err := yaml.Unmarshal(eventSource.Value, &sqsEventSource); err != nil { + errorCh <- err + return + } + + var awsSession *session.Session + + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("setting up aws session...") + awsSession, err := commonaws.CreateAWSSession(listener.K8sClient, sqsEventSource.Namespace, sqsEventSource.Region, sqsEventSource.AccessKey, sqsEventSource.SecretKey) + if err != nil { + errorCh <- err + return + } + + sqsClient := sqslib.New(awsSession) + + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("fetching queue url...") + queueURL, err := sqsClient.GetQueueUrl(&sqslib.GetQueueUrlInput{ + QueueName: &sqsEventSource.Queue, + }) + if err != nil { + errorCh <- err + return + } + + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("listening for messages on the queue...") + for { + select { + case <-doneCh: + return + + default: + msg, err := sqsClient.ReceiveMessage(&sqslib.ReceiveMessageInput{ + QueueUrl: queueURL.QueueUrl, + MaxNumberOfMessages: aws.Int64(1), + WaitTimeSeconds: aws.Int64(sqsEventSource.WaitTimeSeconds), + }) + if err != nil { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).WithError(err).Error("failed to process item from queue, waiting for next timeout") + continue + } + + if msg != nil && len(msg.Messages) > 0 { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("dispatching message from queue on data channel") + listener.Logger.WithFields(map[string]interface{}{ + common.LabelEventSource: eventSource.Name, + "message": *msg.Messages[0].Body, + }).Debugln("message from queue") + + dataCh <- []byte(*msg.Messages[0].Body) + + if _, err := sqsClient.DeleteMessage(&sqslib.DeleteMessageInput{ + QueueUrl: queueURL.QueueUrl, + ReceiptHandle: msg.Messages[0].ReceiptHandle, + }); err != nil { + errorCh <- err + return + } + } + } + } +} diff --git a/gateways/server/aws-sqs/validate.go b/gateways/server/aws-sqs/validate.go new file mode 100644 index 0000000000..bf699f6d53 --- /dev/null +++ b/gateways/server/aws-sqs/validate.go @@ -0,0 +1,73 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_sqs + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates sqs event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.SQSEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.SQSEvent)), + }, nil + } + + var sqsEventSource *v1alpha1.SQSEventSource + if err := yaml.Unmarshal(eventSource.Value, &sqsEventSource); err != nil { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(sqsEventSource); err != nil { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.SQSEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.WaitTimeSeconds == 0 { + return fmt.Errorf("must specify polling timeout") + } + if eventSource.Region == "" { + return fmt.Errorf("must specify region") + } + if eventSource.Queue == "" { + return fmt.Errorf("must specify queue name") + } + return nil +} diff --git a/gateways/server/aws-sqs/validate_test.go b/gateways/server/aws-sqs/validate_test.go new file mode 100644 index 0000000000..0cbcff1c23 --- /dev/null +++ b/gateways/server/aws-sqs/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_sqs + +import ( + "context" + "fmt" + "github.com/argoproj/argo-events/common" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "sns", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("sqs"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "aws-sqs.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.SQS { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "sqs", + Value: content, + Type: "sqs", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/core/calendar/Dockerfile b/gateways/server/calendar/Dockerfile similarity index 100% rename from gateways/core/calendar/Dockerfile rename to gateways/server/calendar/Dockerfile diff --git a/gateways/server/calendar/cmd/main.go b/gateways/server/calendar/cmd/main.go new file mode 100644 index 0000000000..dd6c673bab --- /dev/null +++ b/gateways/server/calendar/cmd/main.go @@ -0,0 +1,29 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/calendar" +) + +func main() { + server.StartGateway(&calendar.EventListener{ + Logger: common.NewArgoEventsLogger(), + }) +} diff --git a/gateways/server/calendar/start.go b/gateways/server/calendar/start.go new file mode 100644 index 0000000000..9bf2a1ff31 --- /dev/null +++ b/gateways/server/calendar/start.go @@ -0,0 +1,164 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package calendar + +import ( + "encoding/json" + "time" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/pkg/errors" + cronlib "github.com/robfig/cron" + "github.com/sirupsen/logrus" +) + +// EventListener implements Eventing for calendar based events +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger +} + +// response is the event payload that is sent as response to sensor +type response struct { + // EventTime is time at which event occurred + EventTime time.Time `json:"eventTime"` + // UserPayload if any + UserPayload *json.RawMessage `json:"userPayload"` +} + +// Next is a function to compute the next event time from a given time +type Next func(time.Time) time.Time + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents fires an event when schedule completes. +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing calendar event source...") + var calendarEventSource *v1alpha1.CalendarEventSource + if err := yaml.Unmarshal(eventSource.Value, &calendarEventSource); err != nil { + errorCh <- err + return + } + + logger.Infoln("resolving calendar schedule...") + schedule, err := resolveSchedule(calendarEventSource) + if err != nil { + errorCh <- err + return + } + + logger.Infoln("parsing exclusion dates if any...") + exDates, err := common.ParseExclusionDates(calendarEventSource.ExclusionDates) + if err != nil { + errorCh <- err + return + } + + var next Next + next = func(last time.Time) time.Time { + nextT := schedule.Next(last) + nextYear := nextT.Year() + nextMonth := nextT.Month() + nextDay := nextT.Day() + for _, exDate := range exDates { + // if exDate == nextEvent, then we need to skip this and get the next + if exDate.Year() == nextYear && exDate.Month() == nextMonth && exDate.Day() == nextDay { + return next(nextT) + } + } + return nextT + } + + lastT := time.Now() + var location *time.Location + if calendarEventSource.Timezone != "" { + logger.WithField("location", calendarEventSource.Timezone).Infoln("loading location for the schedule...") + location, err = time.LoadLocation(calendarEventSource.Timezone) + if err != nil { + errorCh <- err + return + } + lastT = lastT.In(location) + } + + for { + t := next(lastT) + timer := time.After(time.Until(t)) + logger.WithField(common.LabelTime, t.UTC().String()).Info("expected next calendar event") + select { + case tx := <-timer: + lastT = tx + if location != nil { + lastT = lastT.In(location) + } + response := &response{ + EventTime: tx, + UserPayload: calendarEventSource.UserPayload, + } + payload, err := json.Marshal(response) + if err != nil { + errorCh <- err + return + } + logger.Infoln("event dispatched on data channel") + dataCh <- payload + case <-doneCh: + return + } + } +} + +// resolveSchedule parses the schedule and returns a valid cron schedule +func resolveSchedule(cal *v1alpha1.CalendarEventSource) (cronlib.Schedule, error) { + if cal.Schedule != "" { + // standard cron expression + specParser := cronlib.NewParser(cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow) + schedule, err := specParser.Parse(cal.Schedule) + if err != nil { + return nil, errors.Errorf("failed to parse schedule %s from calendar event. Cause: %+v", cal.Schedule, err.Error()) + } + return schedule, nil + } else if cal.Interval != "" { + intervalDuration, err := time.ParseDuration(cal.Interval) + if err != nil { + return nil, errors.Errorf("failed to parse interval %s from calendar event. Cause: %+v", cal.Interval, err.Error()) + } + schedule := cronlib.ConstantDelaySchedule{Delay: intervalDuration} + return schedule, nil + } else { + return nil, errors.New("calendar event must contain either a schedule or interval") + } +} diff --git a/gateways/core/calendar/start_test.go b/gateways/server/calendar/start_test.go similarity index 66% rename from gateways/core/calendar/start_test.go rename to gateways/server/calendar/start_test.go index e2ffb3982f..29d22879cc 100644 --- a/gateways/core/calendar/start_test.go +++ b/gateways/server/calendar/start_test.go @@ -17,20 +17,22 @@ limitations under the License. package calendar import ( + "encoding/json" + "testing" + "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/ghodss/yaml" "github.com/smartystreets/goconvey/convey" - "testing" ) func TestResolveSchedule(t *testing.T) { convey.Convey("Given a calendar schedule, resolve it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - - schedule, err := resolveSchedule(ps.(*calSchedule)) + schedule, err := resolveSchedule(&v1alpha1.CalendarEventSource{ + Schedule: "* * * * *", + }) convey.So(err, convey.ShouldBeNil) convey.So(schedule, convey.ShouldNotBeNil) }) @@ -38,13 +40,10 @@ func TestResolveSchedule(t *testing.T) { func TestListenEvents(t *testing.T) { convey.Convey("Given a calendar schedule, listen events", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - - ese := &CalendarEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + listener := &EventListener{ + Logger: common.NewArgoEventsLogger(), } + dataCh := make(chan []byte) errorCh := make(chan error) doneCh := make(chan struct{}, 1) @@ -55,20 +54,32 @@ func TestListenEvents(t *testing.T) { dataCh2 <- data }() - go ese.listenEvents(ps.(*calSchedule), &gateways.EventSource{ - Name: "fake", - Data: es, - Id: "1234", + payload := []byte(`"{\r\n\"hello\": \"world\"\r\n}"`) + raw := json.RawMessage(payload) + + calendarEventSource := &v1alpha1.CalendarEventSource{ + Schedule: "* * * * *", + UserPayload: &raw, + } + + body, err := yaml.Marshal(calendarEventSource) + convey.So(err, convey.ShouldBeNil) + + go listener.listenEvents(&gateways.EventSource{ + Name: "fake", + Value: body, + Id: "1234", + Type: string(apicommon.CalendarEvent), }, dataCh, errorCh, doneCh) data := <-dataCh2 doneCh <- struct{}{} - var cal *calResponse + var cal *response err = yaml.Unmarshal(data, &cal) convey.So(err, convey.ShouldBeNil) - payload, err := cal.UserPayload.MarshalJSON() + payload, err = cal.UserPayload.MarshalJSON() convey.So(err, convey.ShouldBeNil) convey.So(string(payload), convey.ShouldEqual, `"{\r\n\"hello\": \"world\"\r\n}"`) diff --git a/gateways/server/calendar/validate.go b/gateways/server/calendar/validate.go new file mode 100644 index 0000000000..602094c885 --- /dev/null +++ b/gateways/server/calendar/validate.go @@ -0,0 +1,74 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package calendar + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +const () + +// ValidateEventSource validates calendar event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.CalendarEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.CalendarEvent)), + }, nil + } + + var calendarEventSource *v1alpha1.CalendarEventSource + if err := yaml.Unmarshal(eventSource.Value, &calendarEventSource); err != nil { + listener.Logger.WithError(err).WithField(common.LabelEventSource, eventSource.Name).Errorln("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(calendarEventSource); err != nil { + listener.Logger.WithError(err).WithField(common.LabelEventSource, eventSource.Name).Errorln("failed to validate the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(calendarEventSource *v1alpha1.CalendarEventSource) error { + if calendarEventSource == nil { + return common.ErrNilEventSource + } + if calendarEventSource.Schedule == "" && calendarEventSource.Interval == "" { + return fmt.Errorf("must have either schedule or interval") + } + if _, err := resolveSchedule(calendarEventSource); err != nil { + return err + } + return nil +} diff --git a/gateways/server/calendar/validate_test.go b/gateways/server/calendar/validate_test.go new file mode 100644 index 0000000000..002e56c257 --- /dev/null +++ b/gateways/server/calendar/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package calendar + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestEventSourceListener_ValidateEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "calendar", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("calendar"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "calendar.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Calendar { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "calendar", + Value: content, + Type: "calendar", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/common/aws.go b/gateways/server/common/aws/aws.go similarity index 77% rename from gateways/common/aws.go rename to gateways/server/common/aws/aws.go index 7f0c908050..aab8acd08f 100644 --- a/gateways/common/aws.go +++ b/gateways/server/common/aws/aws.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package common +package aws import ( "github.com/argoproj/argo-events/store" @@ -54,3 +54,17 @@ func GetAWSSessionWithoutCreds(region string) (*session.Session, error) { Region: ®ion, }) } + +// CreateAWSSession based on credentials settings return a aws session +func CreateAWSSession(client kubernetes.Interface, namespace, region string, accessKey *corev1.SecretKeySelector, secretKey *corev1.SecretKeySelector) (*session.Session, error) { + if accessKey == nil && secretKey == nil { + return GetAWSSessionWithoutCreds(region) + } + + creds, err := GetAWSCreds(client, namespace, accessKey, secretKey) + if err != nil { + return nil, err + } + + return GetAWSSession(creds, region) +} diff --git a/gateways/common/aws_test.go b/gateways/server/common/aws/aws_test.go similarity index 99% rename from gateways/common/aws_test.go rename to gateways/server/common/aws/aws_test.go index 9b08b578df..289f615f11 100644 --- a/gateways/common/aws_test.go +++ b/gateways/server/common/aws/aws_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package common +package aws import ( "testing" diff --git a/gateways/server/common/fake.go b/gateways/server/common/fake.go new file mode 100644 index 0000000000..24527a62c2 --- /dev/null +++ b/gateways/server/common/fake.go @@ -0,0 +1,41 @@ +package common + +import ( + "context" + "github.com/argoproj/argo-events/gateways" + "google.golang.org/grpc/metadata" +) + +type FakeGRPCStream struct { + SentData *gateways.Event + Ctx context.Context +} + +func (f *FakeGRPCStream) Send(event *gateways.Event) error { + f.SentData = event + return nil +} + +func (f *FakeGRPCStream) SetHeader(metadata.MD) error { + return nil +} + +func (f *FakeGRPCStream) SendHeader(metadata.MD) error { + return nil +} + +func (f *FakeGRPCStream) SetTrailer(metadata.MD) { + return +} + +func (f *FakeGRPCStream) Context() context.Context { + return f.Ctx +} + +func (f *FakeGRPCStream) SendMsg(m interface{}) error { + return nil +} + +func (f *FakeGRPCStream) RecvMsg(m interface{}) error { + return nil +} diff --git a/gateways/common/config.go b/gateways/server/common/fsevent/config.go similarity index 98% rename from gateways/common/config.go rename to gateways/server/common/fsevent/config.go index 212e7aa06f..9a802fc045 100644 --- a/gateways/common/config.go +++ b/gateways/server/common/fsevent/config.go @@ -1,4 +1,4 @@ -package common +package fsevent import ( "errors" diff --git a/gateways/common/config_test.go b/gateways/server/common/fsevent/config_test.go similarity index 98% rename from gateways/common/config_test.go rename to gateways/server/common/fsevent/config_test.go index 78d75d3a5b..eeef933cff 100644 --- a/gateways/common/config_test.go +++ b/gateways/server/common/fsevent/config_test.go @@ -1,4 +1,4 @@ -package common +package fsevent import ( "testing" diff --git a/gateways/common/fsevent/fileevent.go b/gateways/server/common/fsevent/fileevent.go similarity index 100% rename from gateways/common/fsevent/fileevent.go rename to gateways/server/common/fsevent/fileevent.go diff --git a/gateways/common/naivewatcher/mutex.go b/gateways/server/common/naivewatcher/mutex.go similarity index 100% rename from gateways/common/naivewatcher/mutex.go rename to gateways/server/common/naivewatcher/mutex.go diff --git a/gateways/common/naivewatcher/watcher.go b/gateways/server/common/naivewatcher/watcher.go similarity index 98% rename from gateways/common/naivewatcher/watcher.go rename to gateways/server/common/naivewatcher/watcher.go index e615e259ad..8affdc2719 100644 --- a/gateways/common/naivewatcher/watcher.go +++ b/gateways/server/common/naivewatcher/watcher.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/argoproj/argo-events/gateways/common/fsevent" + "github.com/argoproj/argo-events/gateways/server/common/fsevent" ) const ( diff --git a/gateways/common/naivewatcher/watcher_test.go b/gateways/server/common/naivewatcher/watcher_test.go similarity index 98% rename from gateways/common/naivewatcher/watcher_test.go rename to gateways/server/common/naivewatcher/watcher_test.go index f79f1a639c..2ed2e02c18 100644 --- a/gateways/common/naivewatcher/watcher_test.go +++ b/gateways/server/common/naivewatcher/watcher_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/argoproj/argo-events/gateways/common/fsevent" + "github.com/argoproj/argo-events/gateways/server/common/fsevent" "github.com/stretchr/testify/assert" ) diff --git a/gateways/server/common/webhook/fake.go b/gateways/server/common/webhook/fake.go new file mode 100644 index 0000000000..3ef30c999e --- /dev/null +++ b/gateways/server/common/webhook/fake.go @@ -0,0 +1,76 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "net/http" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" +) + +var Hook = &Context{ + Endpoint: "/fake", + Port: "12000", + URL: "test-url", +} + +type FakeHttpWriter struct { + HeaderStatus int + Payload []byte +} + +func (f *FakeHttpWriter) Header() http.Header { + return http.Header{} +} + +func (f *FakeHttpWriter) Write(body []byte) (int, error) { + f.Payload = body + return len(body), nil +} + +func (f *FakeHttpWriter) WriteHeader(status int) { + f.HeaderStatus = status +} + +type FakeRouter struct { + route *Route +} + +func (f *FakeRouter) GetRoute() *Route { + return f.route +} + +func (f *FakeRouter) HandleRoute(writer http.ResponseWriter, request *http.Request) { +} + +func (f *FakeRouter) PostActivate() error { + return nil +} + +func (f *FakeRouter) PostInactivate() error { + return nil +} + +func GetFakeRoute() *Route { + logger := common.NewArgoEventsLogger() + return NewRoute(Hook, logger, &gateways.EventSource{ + Name: "fake-event-source", + Value: []byte("hello"), + Id: "123", + }) +} diff --git a/gateways/server/common/webhook/types.go b/gateways/server/common/webhook/types.go new file mode 100644 index 0000000000..e0d5348136 --- /dev/null +++ b/gateways/server/common/webhook/types.go @@ -0,0 +1,92 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "net/http" + "sync" + + "github.com/argoproj/argo-events/gateways" + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +var ( + // Mutex synchronizes ActiveServerHandlers + Lock sync.Mutex +) + +// Router is an interface to manage the route +type Router interface { + // GetRoute returns the route + GetRoute() *Route + // HandleRoute processes the incoming requests on the route + HandleRoute(writer http.ResponseWriter, request *http.Request) + // PostActivate captures the operations if any after route being activated and ready to process requests. + PostActivate() error + // PostInactivate captures cleanup operations if any after route is inactivated + PostInactivate() error +} + +// Route contains general information about a route +type Route struct { + // Context refers to the webhook context + Context *Context + // Logger to log stuff + Logger *logrus.Logger + // StartCh controls the + StartCh chan struct{} + // EventSource refers to gateway event source + EventSource *gateways.EventSource + // active determines whether the route is active and ready to process incoming requets + // or it is an inactive route + Active bool + // data channel to receive data on this endpoint + DataCh chan []byte + // initialized indicates whether the route has been initialized and there exist a http router + // to process incoming requests + initialized bool +} + +// Controller controls the active servers and endpoints +type Controller struct { + // ActiveServerHandlers keeps track of currently active mux/router for the http servers. + ActiveServerHandlers map[string]*mux.Router + // ActiveRoutes keep track of routes that are already registered with server and their status active or inactive + ActiveRoutes map[string]*Route + // RouteActivateChan handles activation of routes + RouteActivateChan chan Router + // RouteDeactivateChan handles inactivation of routes + RouteDeactivateChan chan Router +} + +// Context holds a general purpose REST API context +type Context struct { + // REST API endpoint + Endpoint string `json:"endpoint" protobuf:"bytes,1,name=endpoint"` + // Method is HTTP request method that indicates the desired action to be performed for a given resource. + // See RFC7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content + Method string `json:"method" protobuf:"bytes,2,name=method"` + // Port on which HTTP server is listening for incoming events. + Port string `json:"port" protobuf:"bytes,3,name=port"` + // URL is the url of the server. + URL string `json:"url" protobuf:"bytes,4,name=url"` + // ServerCertPath refers the file that contains the cert. + ServerCertPath string `json:"serverCertPath,omitempty" protobuf:"bytes,4,opt,name=serverCertPath"` + // ServerKeyPath refers the file that contains private key + ServerKeyPath string `json:"serverKeyPath,omitempty" protobuf:"bytes,5,opt,name=serverKeyPath"` +} diff --git a/gateways/server/common/webhook/validate.go b/gateways/server/common/webhook/validate.go new file mode 100644 index 0000000000..63f7b8f84c --- /dev/null +++ b/gateways/server/common/webhook/validate.go @@ -0,0 +1,62 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "fmt" + "strconv" +) + +// ValidateWebhookContext validates a webhook context +func ValidateWebhookContext(context *Context) error { + if context == nil { + return fmt.Errorf("") + } + if context.Endpoint == "" { + return fmt.Errorf("endpoint can't be empty") + } + if context.Port == "" { + return fmt.Errorf("port can't be empty") + } + if context.Port != "" { + _, err := strconv.Atoi(context.Port) + if err != nil { + return fmt.Errorf("failed to parse server port %s. err: %+v", context.Port, err) + } + } + return nil +} + +// validateRoute validates a route +func validateRoute(r *Route) error { + if r == nil { + return fmt.Errorf("route can't be nil") + } + if r.Context == nil { + return fmt.Errorf("webhook can't be nil") + } + if r.StartCh == nil { + return fmt.Errorf("start channel can't be nil") + } + if r.EventSource == nil { + return fmt.Errorf("event source can't be nil") + } + if r.Logger == nil { + return fmt.Errorf("logger can't be nil") + } + return nil +} diff --git a/gateways/server/common/webhook/webhook.go b/gateways/server/common/webhook/webhook.go new file mode 100644 index 0000000000..4a8100b1e8 --- /dev/null +++ b/gateways/server/common/webhook/webhook.go @@ -0,0 +1,193 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "fmt" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "net/http" +) + +// NewController returns a webhook controller +func NewController() *Controller { + return &Controller{ + ActiveRoutes: make(map[string]*Route), + ActiveServerHandlers: make(map[string]*mux.Router), + RouteActivateChan: make(chan Router), + RouteDeactivateChan: make(chan Router), + } +} + +// NewRoute returns a vanilla route +func NewRoute(hookContext *Context, logger *logrus.Logger, eventSource *gateways.EventSource) *Route { + return &Route{ + Context: hookContext, + Logger: logger, + EventSource: eventSource, + Active: false, + DataCh: make(chan []byte), + StartCh: make(chan struct{}), + } +} + +// ProcessRouteStatus processes route status as active and inactive. +func ProcessRouteStatus(ctrl *Controller) { + for { + select { + case router := <-ctrl.RouteActivateChan: + // start server if it has not been started on this port + startServer(router, ctrl) + // to allow route process incoming requests + router.GetRoute().StartCh <- struct{}{} + + case router := <-ctrl.RouteDeactivateChan: + router.GetRoute().Active = false + } + } +} + +// starts a http server +func startServer(router Router, controller *Controller) { + // start a http server only if no other configuration previously started the server on given port + Lock.Lock() + route := router.GetRoute() + if _, ok := controller.ActiveServerHandlers[route.Context.Port]; !ok { + handler := mux.NewRouter() + server := &http.Server{ + Addr: fmt.Sprintf(":%s", route.Context.Port), + Handler: handler, + } + + controller.ActiveServerHandlers[route.Context.Port] = handler + + // start http server + go func() { + var err error + if route.Context.ServerCertPath == "" || route.Context.ServerKeyPath == "" { + err = server.ListenAndServe() + } else { + err = server.ListenAndServeTLS(route.Context.ServerCertPath, route.Context.ServerKeyPath) + } + route.Logger.WithField(common.LabelEventSource, route.EventSource.Name).WithError(err).Error("http server stopped") + if err != nil { + route.Logger.WithError(err).WithField("port", route.Context.Port).Errorln("failed to listen and serve") + } + }() + } + + // if route is not previously initialized, then assign a router against it + if !route.initialized { + handler := controller.ActiveServerHandlers[route.Context.Port] + handler.HandleFunc(route.Context.Endpoint, router.HandleRoute).Methods(route.Context.Method) + } + + Lock.Unlock() +} + +// activateRoute activates a route to process incoming requests +func activateRoute(router Router, controller *Controller) { + route := router.GetRoute() + endpoint := route.Context.Endpoint + // change status of route as a active route + controller.RouteActivateChan <- router + + // wait for any route to become ready + // if this is the first route that is added for a server, then controller will + // start a http server before marking the route as ready + <-route.StartCh + + log := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelPort: route.Context.Port, + common.LabelEndpoint: endpoint, + }) + + log.Info("activating the route...") + route.Active = true + log.Info("route is activated") +} + +// manageRouteStream consumes data from route's data channel and stops the processing when the event source is stopped/removed +func manageRouteStream(router Router, controller *Controller, eventStream gateways.Eventing_StartEventSourceServer) error { + route := router.GetRoute() + + for { + select { + case data := <-route.DataCh: + route.Logger.WithField(common.LabelEventSource, route.EventSource.Name).Info("new event received, dispatching to gateway client") + err := eventStream.Send(&gateways.Event{ + Name: route.EventSource.Name, + Payload: data, + }) + if err != nil { + route.Logger.WithField(common.LabelEventSource, route.EventSource.Name).WithError(err).Error("failed to send event") + return err + } + + case <-eventStream.Context().Done(): + route.Logger.WithField(common.LabelEventSource, route.EventSource.Name).Info("connection is closed by client") + controller.RouteDeactivateChan <- router + return nil + } + } +} + +// ManagerRoute manages the lifecycle of a route +func ManageRoute(router Router, controller *Controller, eventStream gateways.Eventing_StartEventSourceServer) error { + route := router.GetRoute() + + logger := route.Logger.WithField(common.LabelEventSource, route.EventSource.Name) + + // in order to process a route, it needs to go through + // 1. validation - basic configuration checks + // 2. activation - associate http handler if not done previously + // 3. post start operations - operations that must be performed after route has been activated and ready to process requests + // 4. consume data from route's data channel + // 5. post stop operations - operations that must be performed after route is inactivated + + logger.Info("validating the route...") + if err := validateRoute(router.GetRoute()); err != nil { + logger.WithError(err).Error("route is invalid, won't initialize it") + return err + } + + logger.Info("activating the route...") + activateRoute(router, controller) + + logger.Info("running operations post route activation...") + if err := router.PostActivate(); err != nil { + logger.WithError(err).Error("error occurred while performing post route activation operations") + return err + } + + logger.Info("listening to payloads for the route...") + if err := manageRouteStream(router, controller, eventStream); err != nil { + logger.WithError(err).Error("error occurred in consuming payload from the route") + return err + } + + logger.Info("running operations post route inactivation...") + if err := router.PostInactivate(); err != nil { + logger.WithError(err).Error("error occurred while running operations post route inactivation") + } + + return nil +} diff --git a/gateways/core/webhook/config_test.go b/gateways/server/common/webhook/webhook_test.go similarity index 61% rename from gateways/core/webhook/config_test.go rename to gateways/server/common/webhook/webhook_test.go index 044285ae78..688ab9c3e9 100644 --- a/gateways/core/webhook/config_test.go +++ b/gateways/server/common/webhook/webhook_test.go @@ -19,22 +19,22 @@ package webhook import ( "testing" - "github.com/argoproj/argo-events/gateways/common" "github.com/smartystreets/goconvey/convey" ) -var es = ` -endpoint: "/bar" -port: "10000" -method: "POST" -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a webhook event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*common.Webhook) - convey.So(ok, convey.ShouldEqual, true) +var rc = &FakeRouter{ + route: GetFakeRoute(), +} + +func TestValidateWebhook(t *testing.T) { + convey.Convey("Given a webhook, validate it", t, func() { + convey.So(ValidateWebhookContext(Hook), convey.ShouldBeNil) + }) +} + +func TestNewWebhookHelper(t *testing.T) { + convey.Convey("Make sure webhook helper is not empty", t, func() { + controller := NewController() + convey.So(controller, convey.ShouldNotBeNil) }) } diff --git a/gateways/core/file/Dockerfile b/gateways/server/file/Dockerfile similarity index 100% rename from gateways/core/file/Dockerfile rename to gateways/server/file/Dockerfile diff --git a/gateways/core/stream/nats/cmd/main.go b/gateways/server/file/cmd/main.go similarity index 76% rename from gateways/core/stream/nats/cmd/main.go rename to gateways/server/file/cmd/main.go index 82bbb71ae2..14ab2ea09a 100644 --- a/gateways/core/stream/nats/cmd/main.go +++ b/gateways/server/file/cmd/main.go @@ -18,12 +18,12 @@ package main import ( "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/stream/nats" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/file" ) func main() { - gateways.StartGateway(&nats.NatsEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&file.EventListener{ + Logger: common.NewArgoEventsLogger(), }) } diff --git a/gateways/server/file/start.go b/gateways/server/file/start.go new file mode 100644 index 0000000000..ed0d982100 --- /dev/null +++ b/gateways/server/file/start.go @@ -0,0 +1,141 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package file + +import ( + "encoding/json" + "regexp" + "strings" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/common/fsevent" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/fsnotify/fsnotify" + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// EventListener implements Eventing for file event source +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + log := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + log.Info("started processing event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listen to file related events +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + var fileEventSource *v1alpha1.FileEventSource + if err := yaml.Unmarshal(eventSource.Value, &fileEventSource); err != nil { + errorCh <- err + return + } + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + // create new fs watcher + logger.Infoln("setting up a new file watcher...") + watcher, err := fsnotify.NewWatcher() + if err != nil { + errorCh <- err + return + } + defer watcher.Close() + + // file descriptor to watch must be available in file system. You can't watch an fs descriptor that is not present. + logger.Infoln("adding directory to monitor for the watcher...") + err = watcher.Add(fileEventSource.WatchPathConfig.Directory) + if err != nil { + errorCh <- err + return + } + + var pathRegexp *regexp.Regexp + if fileEventSource.WatchPathConfig.PathRegexp != "" { + logger.WithField("regex", fileEventSource.WatchPathConfig.PathRegexp).Infoln("matching file path with configured regex...") + pathRegexp, err = regexp.Compile(fileEventSource.WatchPathConfig.PathRegexp) + if err != nil { + errorCh <- err + return + } + } + + logger.Info("listening to file notifications...") + for { + select { + case event, ok := <-watcher.Events: + if !ok { + logger.Info("fs watcher has stopped") + // watcher stopped watching file events + errorCh <- errors.New("fs watcher stopped") + return + } + // fwc.Path == event.Name is required because we don't want to send event when .swp files are created + matched := false + relPath := strings.TrimPrefix(event.Name, fileEventSource.WatchPathConfig.Directory) + if fileEventSource.WatchPathConfig.Path != "" && fileEventSource.WatchPathConfig.Path == relPath { + matched = true + } else if pathRegexp != nil && pathRegexp.MatchString(relPath) { + matched = true + } + if matched && fileEventSource.EventType == event.Op.String() { + logger.WithFields( + map[string]interface{}{ + "event-type": event.Op.String(), + "descriptor-name": event.Name, + }, + ).Infoln("file event") + + // Assume fsnotify event has the same Op spec of our file event + fileEvent := fsevent.Event{Name: event.Name, Op: fsevent.NewOp(event.Op.String())} + payload, err := json.Marshal(fileEvent) + if err != nil { + errorCh <- err + return + } + logger.WithFields( + map[string]interface{}{ + "event-type": event.Op.String(), + "descriptor-name": event.Name, + }, + ).Infoln("dispatching file event on data channel...") + dataCh <- payload + } + case err := <-watcher.Errors: + errorCh <- err + return + case <-doneCh: + return + } + } +} diff --git a/gateways/server/file/validate.go b/gateways/server/file/validate.go new file mode 100644 index 0000000000..408ed85da2 --- /dev/null +++ b/gateways/server/file/validate.go @@ -0,0 +1,68 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package file + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates file event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.FileEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.FileEvent)), + }, nil + } + + var fileEventSource *v1alpha1.FileEventSource + if err := yaml.Unmarshal(eventSource.Value, &fileEventSource); err != nil { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(fileEventSource); err != nil { + return &gateways.ValidEventSource{ + Reason: err.Error(), + IsValid: false, + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(fileEventSource *v1alpha1.FileEventSource) error { + if fileEventSource == nil { + return common.ErrNilEventSource + } + if fileEventSource.EventType == "" { + return fmt.Errorf("type must be specified") + } + err := fileEventSource.WatchPathConfig.Validate() + return err +} diff --git a/gateways/server/file/validate_test.go b/gateways/server/file/validate_test.go new file mode 100644 index 0000000000..26725bae21 --- /dev/null +++ b/gateways/server/file/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package file + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestEventSourceListener_ValidateEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "file", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("file"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "file.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.File { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "file", + Value: content, + Type: "file", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/gcp-pubsub/Dockerfile b/gateways/server/gcp-pubsub/Dockerfile similarity index 100% rename from gateways/community/gcp-pubsub/Dockerfile rename to gateways/server/gcp-pubsub/Dockerfile diff --git a/gateways/core/calendar/cmd/main.go b/gateways/server/gcp-pubsub/cmd/main.go similarity index 76% rename from gateways/core/calendar/cmd/main.go rename to gateways/server/gcp-pubsub/cmd/main.go index e8c21b3c1e..342d163091 100644 --- a/gateways/core/calendar/cmd/main.go +++ b/gateways/server/gcp-pubsub/cmd/main.go @@ -18,12 +18,12 @@ package main import ( "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/calendar" + "github.com/argoproj/argo-events/gateways/server" + pubsub "github.com/argoproj/argo-events/gateways/server/gcp-pubsub" ) func main() { - gateways.StartGateway(&calendar.CalendarEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&pubsub.EventListener{ + Logger: common.NewArgoEventsLogger(), }) } diff --git a/gateways/server/gcp-pubsub/start.go b/gateways/server/gcp-pubsub/start.go new file mode 100644 index 0000000000..471dd32575 --- /dev/null +++ b/gateways/server/gcp-pubsub/start.go @@ -0,0 +1,156 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pubsub + +import ( + "context" + "fmt" + + "cloud.google.com/go/pubsub" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/sirupsen/logrus" + "google.golang.org/api/option" +) + +// EventListener implements Eventing for gcp pub-sub event source +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger +} + +// StartEventSource starts processing the GCP PubSub event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + ctx := eventStream.Context() + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(ctx, eventSource, dataCh, errorCh, doneCh) + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listens to GCP PubSub events +func (listener *EventListener) listenEvents(ctx context.Context, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + // In order to listen events from GCP PubSub, + // 1. Parse the event source that contains configuration to connect to GCP PubSub + // 2. Create a new PubSub client + // 3. Create the topic if one doesn't exist already + // 4. Create a subscription if one doesn't exist already. + // 5. Start listening to messages on the queue + // 6. Once the event source is stopped perform cleaning up - 1. Delete the subscription if configured so 2. Close the PubSub client + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing PubSub event source...") + var pubsubEventSource *v1alpha1.PubSubEventSource + if err := yaml.Unmarshal(eventSource.Value, &pubsubEventSource); err != nil { + errorCh <- err + return + } + + logger = logger.WithField("topic", pubsubEventSource.Topic) + + logger.Infoln("setting up a client to connect to PubSub...") + + // Create a new topic with the given name if none exists + client, err := pubsub.NewClient(ctx, pubsubEventSource.ProjectID, option.WithCredentialsFile(pubsubEventSource.CredentialsFile)) + if err != nil { + errorCh <- err + return + } + + // use same client for topic and subscription by default + topicClient := client + if pubsubEventSource.TopicProjectID != "" && pubsubEventSource.TopicProjectID != pubsubEventSource.ProjectID { + topicClient, err = pubsub.NewClient(ctx, pubsubEventSource.TopicProjectID, option.WithCredentialsFile(pubsubEventSource.CredentialsFile)) + if err != nil { + errorCh <- err + return + } + } + + logger.Infoln("getting topic information from PubSub...") + topic := topicClient.Topic(pubsubEventSource.Topic) + exists, err := topic.Exists(ctx) + if err != nil { + errorCh <- err + return + } + if !exists { + logger.Infoln("topic doesn't exist, creating the GCP PubSub topic...") + if _, err := topicClient.CreateTopic(ctx, pubsubEventSource.Topic); err != nil { + errorCh <- err + return + } + } + + subscriptionName := fmt.Sprintf("%s-%s", eventSource.Name, eventSource.Id) + + logger = logger.WithField("subscription", subscriptionName) + + logger.Infoln("subscribing to PubSub topic...") + subscription := client.Subscription(subscriptionName) + exists, err = subscription.Exists(ctx) + + if err != nil { + errorCh <- err + return + } + if exists { + logger.Warnln("using an existing subscription...") + } else { + logger.Infoln("creating a new subscription...") + if _, err := client.CreateSubscription(ctx, subscriptionName, pubsub.SubscriptionConfig{Topic: topic}); err != nil { + errorCh <- err + return + } + } + + logger.Infoln("listening for messages from PubSub...") + err = subscription.Receive(ctx, func(msgCtx context.Context, m *pubsub.Message) { + logger.Info("received GCP PubSub Message from topic") + dataCh <- m.Data + m.Ack() + }) + if err != nil { + errorCh <- err + return + } + + <-doneCh + + if pubsubEventSource.DeleteSubscriptionOnFinish { + logger.Info("deleting PubSub subscription...") + if err = subscription.Delete(context.Background()); err != nil { + logger.WithError(err).Errorln("failed to delete the PubSub subscription") + } + } + + logger.Info("closing PubSub client...") + if err = client.Close(); err != nil { + logger.WithError(err).Errorln("failed to close the PubSub client") + } +} diff --git a/gateways/server/gcp-pubsub/validate.go b/gateways/server/gcp-pubsub/validate.go new file mode 100644 index 0000000000..3bf4c758b6 --- /dev/null +++ b/gateways/server/gcp-pubsub/validate.go @@ -0,0 +1,76 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pubsub + +import ( + "context" + "fmt" + "os" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates gateway event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.PubSubEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.PubSubEvent)), + }, nil + } + + var pubsubEventSource *v1alpha1.PubSubEventSource + if err := yaml.Unmarshal(eventSource.Value, &pubsubEventSource); err != nil { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(pubsubEventSource); err != nil { + return &gateways.ValidEventSource{ + Reason: err.Error(), + IsValid: false, + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.PubSubEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.ProjectID == "" { + return fmt.Errorf("must specify projectId") + } + if eventSource.Topic == "" { + return fmt.Errorf("must specify topic") + } + if eventSource.CredentialsFile != "" { + if _, err := os.Stat(eventSource.CredentialsFile); err != nil { + return err + } + } + return nil +} diff --git a/gateways/server/gcp-pubsub/validate_test.go b/gateways/server/gcp-pubsub/validate_test.go new file mode 100644 index 0000000000..5cf2284e00 --- /dev/null +++ b/gateways/server/gcp-pubsub/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pubsub + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestEventListener_ValidateEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "pubsub", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("pubsub"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "gcp-pubsub.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.PubSub { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "pubsub", + Value: content, + Type: "pubsub", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/github/Dockerfile b/gateways/server/github/Dockerfile similarity index 100% rename from gateways/community/github/Dockerfile rename to gateways/server/github/Dockerfile diff --git a/gateways/community/github/cmd/main.go b/gateways/server/github/cmd/main.go similarity index 76% rename from gateways/community/github/cmd/main.go rename to gateways/server/github/cmd/main.go index 5488e25db8..50a39718a9 100644 --- a/gateways/community/github/cmd/main.go +++ b/gateways/server/github/cmd/main.go @@ -17,11 +17,12 @@ limitations under the License. package main import ( + "os" + "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/github" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/github" "k8s.io/client-go/kubernetes" - "os" ) func main() { @@ -31,13 +32,13 @@ func main() { panic(err) } clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) + namespace, ok := os.LookupEnv(common.EnvVarNamespace) if !ok { panic("namespace is not provided") } - gateways.StartGateway(&github.GithubEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&github.EventListener{ + Logger: common.NewArgoEventsLogger(), Namespace: namespace, - Clientset: clientset, + K8sClient: clientset, }) } diff --git a/gateways/community/github/hook_util.go b/gateways/server/github/hook_util.go similarity index 100% rename from gateways/community/github/hook_util.go rename to gateways/server/github/hook_util.go diff --git a/gateways/community/github/hook_util_test.go b/gateways/server/github/hook_util_test.go similarity index 100% rename from gateways/community/github/hook_util_test.go rename to gateways/server/github/hook_util_test.go index 595e88ad85..e474cc3377 100644 --- a/gateways/community/github/hook_util_test.go +++ b/gateways/server/github/hook_util_test.go @@ -1,9 +1,9 @@ package github import ( - "testing" - "github.com/stretchr/testify/assert" gh "github.com/google/go-github/github" + "github.com/stretchr/testify/assert" + "testing" ) func TestSliceEqual(t *testing.T) { diff --git a/gateways/server/github/start.go b/gateways/server/github/start.go new file mode 100644 index 0000000000..6bc3ca48c7 --- /dev/null +++ b/gateways/server/github/start.go @@ -0,0 +1,299 @@ +/* +Copyright 2018 KompiTech GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "context" + "encoding/json" + "github.com/argoproj/argo-events/gateways/server" + "net/http" + "net/url" + "time" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/argoproj/argo-events/store" + "github.com/ghodss/yaml" + gh "github.com/google/go-github/github" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" +) + +// GitHub headers +const ( + githubEventHeader = "X-GitHub-Event" + githubDeliveryHeader = "X-GitHub-Delivery" +) + +// controller controls the webhook operations +var ( + controller = webhook.NewController() +) + +// set up the activation and inactivation channels to control the state of routes. +func init() { + go webhook.ProcessRouteStatus(controller) +} + +// getCredentials for retrieves credentials for GitHub connection +func (router *Router) getCredentials(keySelector *corev1.SecretKeySelector, namespace string) (*cred, error) { + token, err := store.GetSecrets(router.k8sClient, namespace, keySelector.Name, keySelector.Key) + if err != nil { + return nil, err + } + return &cred{ + secret: token, + }, nil +} + +// Implement Router +// 1. GetRoute +// 2. HandleRoute +// 3. PostActivate +// 4. PostDeactivate + +// GetRoute returns the route +func (router *Router) GetRoute() *webhook.Route { + return router.route +} + +// HandleRoute handles incoming requests on the route +func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { + route := router.route + + logger := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelPort: route.Context.Port, + }) + + logger.Info("received a request, processing it...") + + if !route.Active { + logger.Info("endpoint is not active, won't process the request") + common.SendErrorResponse(writer, "endpoint is inactive") + return + } + + hook := router.hook + secret := "" + if s, ok := hook.Config["secret"]; ok { + secret = s.(string) + } + + body, err := parseValidateRequest(request, []byte(secret)) + if err != nil { + logger.WithError(err).Error("request is not valid event notification, discarding it") + common.SendErrorResponse(writer, err.Error()) + return + } + + logger.Infoln("dispatching event on route's data channel") + route.DataCh <- body + logger.Info("request successfully processed") + + common.SendSuccessResponse(writer, "success") +} + +// PostActivate performs operations once the route is activated and ready to consume requests +func (router *Router) PostActivate() error { + // In order to successfully setup a GitHub hook for the given repository, + // 1. Get the API Token and Webhook secret from K8s secrets + // 2. Configure the hook with url, content type, ssl etc. + // 3. Set up a GitHub client + // 4. Set the base and upload url for the client + // 5. Create the hook if one doesn't exist already. If exists already, then use that one. + + route := router.route + githubEventSource := router.githubEventSource + + logger := route.Logger.WithFields(map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + "repository": githubEventSource.Repository, + }) + + logger.Infoln("retrieving api token credentials...") + apiTokenCreds, err := router.getCredentials(githubEventSource.APIToken, githubEventSource.Namespace) + if err != nil { + return errors.Errorf("failed to retrieve api token credentials. err: %+v", err) + } + + logger.Infoln("setting up auth with api token...") + PATTransport := TokenAuthTransport{ + Token: apiTokenCreds.secret, + } + + logger.Infoln("configuring GitHub hook...") + formattedUrl := common.FormattedURL(githubEventSource.Webhook.URL, githubEventSource.Webhook.Endpoint) + hookConfig := map[string]interface{}{ + "url": &formattedUrl, + } + + if githubEventSource.ContentType != "" { + hookConfig["content_type"] = githubEventSource.ContentType + } + + if githubEventSource.Insecure { + hookConfig["insecure_ssl"] = "1" + } else { + hookConfig["insecure_ssl"] = "0" + } + + logger.Infoln("retrieving webhook secret credentials...") + if githubEventSource.WebhookSecret != nil { + webhookSecretCreds, err := router.getCredentials(githubEventSource.WebhookSecret, githubEventSource.Namespace) + if err != nil { + return errors.Errorf("failed to retrieve webhook secret. err: %+v", err) + } + hookConfig["secret"] = webhookSecretCreds.secret + } + + router.hook = &gh.Hook{ + Events: githubEventSource.Events, + Active: gh.Bool(githubEventSource.Active), + Config: hookConfig, + } + + logger.Infoln("setting up client for GitHub...") + router.githubClient = gh.NewClient(PATTransport.Client()) + + logger.Infoln("setting up base url for GitHub client...") + if githubEventSource.GithubBaseURL != "" { + baseURL, err := url.Parse(githubEventSource.GithubBaseURL) + if err != nil { + return errors.Errorf("failed to parse github base url. err: %s", err) + } + router.githubClient.BaseURL = baseURL + } + + logger.Infoln("setting up the upload url for GitHub client...") + if githubEventSource.GithubUploadURL != "" { + uploadURL, err := url.Parse(githubEventSource.GithubUploadURL) + if err != nil { + return errors.Errorf("failed to parse github upload url. err: %s", err) + } + router.githubClient.UploadURL = uploadURL + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + logger.Infoln("creating a GitHub hook for the repository...") + hook, _, err := router.githubClient.Repositories.CreateHook(ctx, githubEventSource.Owner, githubEventSource.Repository, router.hook) + if err != nil { + // Continue if error is because hook already exists + er, ok := err.(*gh.ErrorResponse) + if !ok || er.Response.StatusCode != http.StatusUnprocessableEntity { + return errors.Errorf("failed to create webhook. err: %+v", err) + } + } + + // if hook alreay exists then CreateHook returns hook value as nil + if hook == nil { + logger.Infoln("GitHub hook for the repository already exists, trying to use the existing hook...") + ctx, cancel = context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + hooks, _, err := router.githubClient.Repositories.ListHooks(ctx, githubEventSource.Owner, githubEventSource.Repository, nil) + if err != nil { + return errors.Errorf("failed to list existing webhooks. err: %+v", err) + } + + hook = getHook(hooks, formattedUrl, githubEventSource.Events) + if hook == nil { + return errors.New("failed to find existing webhook") + } + } + + if githubEventSource.WebhookSecret != nil { + // As secret in hook config is masked with asterisk (*), replace it with unmasked secret. + hook.Config["secret"] = hookConfig["secret"] + } + + router.hook = hook + logger.Infoln("GitHub hook has been successfully set for the repository") + + return nil +} + +// PostInactivate performs operations after the route is inactivated +func (router *Router) PostInactivate() error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + githubEventSource := router.githubEventSource + + if githubEventSource.DeleteHookOnFinish { + logger := router.route.Logger.WithFields(map[string]interface{}{ + common.LabelEventSource: router.route.EventSource.Name, + "repository": githubEventSource.Repository, + "hook-id": *router.hook.ID, + }) + + logger.Infoln("deleting GitHub hook...") + if _, err := router.githubClient.Repositories.DeleteHook(ctx, githubEventSource.Owner, githubEventSource.Repository, *router.hook.ID); err != nil { + return errors.Errorf("failed to delete hook. err: %+v", err) + } + logger.Infoln("GitHub hook deleted") + } + + return nil +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + var githubEventSource *v1alpha1.GithubEventSource + if err := yaml.Unmarshal(eventSource.Value, &githubEventSource); err != nil { + listener.Logger.WithError(err).WithField(common.LabelEventSource, eventSource.Name).Infoln("failed to parse the event source") + return err + } + + route := webhook.NewRoute(githubEventSource.Webhook, listener.Logger, eventSource) + + return webhook.ManageRoute(&Router{ + route: route, + k8sClient: listener.K8sClient, + githubEventSource: githubEventSource, + }, controller, eventStream) +} + +// parseValidateRequest parses a http request and checks if it is valid GitHub notification +func parseValidateRequest(r *http.Request, secret []byte) ([]byte, error) { + body, err := gh.ValidatePayload(r, secret) + if err != nil { + return nil, err + } + + payload := make(map[string]interface{}) + if err := json.Unmarshal(body, &payload); err != nil { + return nil, err + } + for _, h := range []string{ + githubEventHeader, + githubDeliveryHeader, + } { + payload[h] = r.Header.Get(h) + } + return json.Marshal(payload) +} diff --git a/gateways/community/github/start_test.go b/gateways/server/github/start_test.go similarity index 60% rename from gateways/community/github/start_test.go rename to gateways/server/github/start_test.go index 9206c8d5bf..f8f06577c3 100644 --- a/gateways/community/github/start_test.go +++ b/gateways/server/github/start_test.go @@ -19,11 +19,12 @@ package github import ( "bytes" "encoding/json" + "github.com/argoproj/argo-events/gateways/server/common/webhook" "io/ioutil" "net/http" "testing" - gwcommon "github.com/argoproj/argo-events/gateways/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/ghodss/yaml" "github.com/google/go-github/github" "github.com/smartystreets/goconvey/convey" @@ -33,24 +34,24 @@ import ( ) var ( - rc = &RouteConfig{ - route: gwcommon.GetFakeRoute(), - clientset: fake.NewSimpleClientset(), - namespace: "fake", + router = &Router{ + route: webhook.GetFakeRoute(), + k8sClient: fake.NewSimpleClientset(), + githubEventSource: &v1alpha1.GithubEventSource{ + Namespace: "fake", + }, } - secretName = "githab-access" + secretName = "github-access" accessKey = "YWNjZXNz" LabelAccessKey = "accesskey" ) func TestGetCredentials(t *testing.T) { convey.Convey("Given a kubernetes secret, get credentials", t, func() { - - secret, err := rc.clientset.CoreV1().Secrets(rc.namespace).Create(&corev1.Secret{ + secret, err := router.k8sClient.CoreV1().Secrets(router.githubEventSource.Namespace).Create(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: rc.namespace, + Name: secretName, }, Data: map[string][]byte{ LabelAccessKey: []byte(accessKey), @@ -59,9 +60,26 @@ func TestGetCredentials(t *testing.T) { convey.So(err, convey.ShouldBeNil) convey.So(secret, convey.ShouldNotBeNil) - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - creds, err := rc.getCredentials(ps.(*githubEventSource).APIToken) + githubEventSource := &v1alpha1.GithubEventSource{ + Webhook: &webhook.Context{ + Endpoint: "/push", + URL: "http://webhook-gateway-svc", + Port: "12000", + }, + Owner: "fake", + Repository: "fake", + Events: []string{ + "PushEvent", + }, + APIToken: &corev1.SecretKeySelector{ + Key: LabelAccessKey, + LocalObjectReference: corev1.LocalObjectReference{ + Name: "github-access", + }, + }, + } + + creds, err := router.getCredentials(githubEventSource.APIToken, githubEventSource.Namespace) convey.So(err, convey.ShouldBeNil) convey.So(creds, convey.ShouldNotBeNil) convey.So(creds.secret, convey.ShouldEqual, "YWNjZXNz") @@ -70,35 +88,51 @@ func TestGetCredentials(t *testing.T) { func TestRouteActiveHandler(t *testing.T) { convey.Convey("Given a route configuration", t, func() { - r := rc.route - helper.ActiveEndpoints[r.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } + route := router.route + route.DataCh = make(chan []byte) convey.Convey("Inactive route should return error", func() { - writer := &gwcommon.FakeHttpWriter{} - ps, err := parseEventSource(es) + writer := &webhook.FakeHttpWriter{} + githubEventSource := &v1alpha1.GithubEventSource{ + Webhook: &webhook.Context{ + Endpoint: "/push", + URL: "http://webhook-gateway-svc", + Port: "12000", + }, + Owner: "fake", + Repository: "fake", + Events: []string{ + "PushEvent", + }, + APIToken: &corev1.SecretKeySelector{ + Key: "accessKey", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "github-access", + }, + }, + } + + body, err := yaml.Marshal(githubEventSource) convey.So(err, convey.ShouldBeNil) - pbytes, err := yaml.Marshal(ps.(*githubEventSource)) - convey.So(err, convey.ShouldBeNil) - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(pbytes)), + + router.HandleRoute(writer, &http.Request{ + Body: ioutil.NopCloser(bytes.NewReader(body)), }) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) convey.Convey("Active route should return success", func() { - helper.ActiveEndpoints[r.Webhook.Endpoint].Active = true - rc.hook = &github.Hook{ + route.Active = true + router.hook = &github.Hook{ Config: make(map[string]interface{}), } - rc.RouteHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(pbytes)), + router.HandleRoute(writer, &http.Request{ + Body: ioutil.NopCloser(bytes.NewReader(body)), }) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) - rc.ges = ps.(*githubEventSource) - err = rc.PostStart() + router.githubEventSource = githubEventSource + err = router.PostActivate() convey.So(err, convey.ShouldNotBeNil) }) }) diff --git a/gateways/community/github/tokenauth.go b/gateways/server/github/tokenauth.go similarity index 100% rename from gateways/community/github/tokenauth.go rename to gateways/server/github/tokenauth.go diff --git a/gateways/server/github/types.go b/gateways/server/github/types.go new file mode 100644 index 0000000000..8f593090a8 --- /dev/null +++ b/gateways/server/github/types.go @@ -0,0 +1,54 @@ +/* +Copyright 2018 KompiTech GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/google/go-github/github" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" +) + +// EventListener implements Eventing for GitHub event source +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger + // K8sClient is the Kubernetes client + K8sClient kubernetes.Interface + // Namespace where gateway is deployed + Namespace string +} + +// Router contains information about the route +type Router struct { + // route contains configuration for an API endpoint + route *webhook.Route + // githubEventSource is the event source that holds information to consume events from GitHub + githubEventSource *v1alpha1.GithubEventSource + // githubClient is the client to connect to GitHub + githubClient *github.Client + // hook represents a GitHub (web and service) hook for a repository. + hook *github.Hook + // K8sClient is the Kubernetes client + k8sClient kubernetes.Interface +} + +// cred stores the api access token or webhook secret +type cred struct { + secret string +} diff --git a/gateways/server/github/validate.go b/gateways/server/github/validate.go new file mode 100644 index 0000000000..1cf261bca4 --- /dev/null +++ b/gateways/server/github/validate.go @@ -0,0 +1,79 @@ +/* +Copyright 2018 KompiTech GmbH +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates a github event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.GitHubEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.GitHubEvent)), + }, nil + } + + var githubEventSource *v1alpha1.GithubEventSource + if err := yaml.Unmarshal(eventSource.Value, &githubEventSource); err != nil { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, err + } + + if err := validate(githubEventSource); err != nil { + return &gateways.ValidEventSource{ + Reason: err.Error(), + IsValid: false, + }, err + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(githubEventSource *v1alpha1.GithubEventSource) error { + if githubEventSource == nil { + return common.ErrNilEventSource + } + if githubEventSource.Repository == "" { + return fmt.Errorf("repository cannot be empty") + } + if githubEventSource.Owner == "" { + return fmt.Errorf("owner cannot be empty") + } + if githubEventSource.APIToken == nil { + return fmt.Errorf("api token can't be empty") + } + if githubEventSource.Events == nil || len(githubEventSource.Events) < 1 { + return fmt.Errorf("events must be defined") + } + if githubEventSource.ContentType != "" { + if !(githubEventSource.ContentType == "json" || githubEventSource.ContentType == "form") { + return fmt.Errorf("content type must be \"json\" or \"form\"") + } + } + return webhook.ValidateWebhookContext(githubEventSource.Webhook) +} diff --git a/gateways/server/github/validate_test.go b/gateways/server/github/validate_test.go new file mode 100644 index 0000000000..be76f0be8e --- /dev/null +++ b/gateways/server/github/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateGithubEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "github", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("github"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "github.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Github { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "github", + Value: content, + Type: "github", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/gitlab/Dockerfile b/gateways/server/gitlab/Dockerfile similarity index 100% rename from gateways/community/gitlab/Dockerfile rename to gateways/server/gitlab/Dockerfile diff --git a/gateways/community/aws-sqs/cmd/main.go b/gateways/server/gitlab/cmd/main.go similarity index 70% rename from gateways/community/aws-sqs/cmd/main.go rename to gateways/server/gitlab/cmd/main.go index 5fbe7e123e..44ff8a90de 100644 --- a/gateways/community/aws-sqs/cmd/main.go +++ b/gateways/server/gitlab/cmd/main.go @@ -20,8 +20,8 @@ import ( "os" "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/aws-sqs" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/gitlab" "k8s.io/client-go/kubernetes" ) @@ -32,13 +32,8 @@ func main() { panic(err) } clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) - if !ok { - panic("namespace is not provided") - } - gateways.StartGateway(&aws_sqs.SQSEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - Clientset: clientset, - Namespace: namespace, + server.StartGateway(&gitlab.EventListener{ + Logger: common.NewArgoEventsLogger(), + K8sClient: clientset, }) } diff --git a/gateways/server/gitlab/start.go b/gateways/server/gitlab/start.go new file mode 100644 index 0000000000..2251d0c9a6 --- /dev/null +++ b/gateways/server/gitlab/start.go @@ -0,0 +1,202 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitlab + +import ( + "io/ioutil" + "net/http" + "reflect" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/argoproj/argo-events/store" + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "github.com/xanzy/go-gitlab" + corev1 "k8s.io/api/core/v1" +) + +// controller controls the webhook operations +var ( + controller = webhook.NewController() +) + +// set up the activation and inactivation channels to control the state of routes. +func init() { + go webhook.ProcessRouteStatus(controller) +} + +// getCredentials retrieves credentials to connect to GitLab +func (router *Router) getCredentials(keySelector *corev1.SecretKeySelector, namespace string) (*cred, error) { + token, err := store.GetSecrets(router.k8sClient, namespace, keySelector.Name, keySelector.Key) + if err != nil { + return nil, err + } + return &cred{ + token: token, + }, nil +} + +// Implement Router +// 1. GetRoute +// 2. HandleRoute +// 3. PostActivate +// 4. PostDeactivate + +// GetRoute returns the route +func (router *Router) GetRoute() *webhook.Route { + return router.route +} + +// HandleRoute handles incoming requests on the route +func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { + route := router.route + + logger := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelPort: route.Context.Port, + }) + + logger.Info("received a request, processing it...") + + if route.Active { + logger.Info("endpoint is not active, won't process the request") + common.SendErrorResponse(writer, "inactive endpoint") + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + logger.WithError(err).Error("failed to parse request body") + common.SendErrorResponse(writer, err.Error()) + return + } + + logger.Infoln("dispatching event on route's data channel") + route.DataCh <- body + + logger.Info("request successfully processed") + common.SendSuccessResponse(writer, "success") +} + +// PostActivate performs operations once the route is activated and ready to consume requests +func (router *Router) PostActivate() error { + route := router.GetRoute() + gitlabEventSource := router.gitlabEventSource + + // In order to set up a hook for the GitLab project, + // 1. Get the API access token for client + // 2. Set up GitLab client + // 3. Configure Hook with given event type + // 4. Create project hook + + logger := route.Logger.WithFields(map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + "event-type": gitlabEventSource.Event, + "project-id": gitlabEventSource.ProjectId, + }) + + logger.Infoln("retrieving the access token credentials...") + c, err := router.getCredentials(gitlabEventSource.AccessToken, gitlabEventSource.Namespace) + if err != nil { + return errors.Errorf("failed to get gitlab credentials. err: %+v", err) + } + + logger.Infoln("setting up the client to connect to GitLab...") + router.gitlabClient = gitlab.NewClient(nil, c.token) + if err = router.gitlabClient.SetBaseURL(gitlabEventSource.GitlabBaseURL); err != nil { + return errors.Errorf("failed to set gitlab base url, err: %+v", err) + } + + formattedUrl := common.FormattedURL(gitlabEventSource.Webhook.URL, gitlabEventSource.Webhook.Endpoint) + + opt := &gitlab.AddProjectHookOptions{ + URL: &formattedUrl, + Token: &c.token, + EnableSSLVerification: &router.gitlabEventSource.EnableSSLVerification, + } + + logger.Infoln("configuring the type of the GitLab event the hook must register against...") + elem := reflect.ValueOf(opt).Elem().FieldByName(string(router.gitlabEventSource.Event)) + if ok := elem.IsValid(); !ok { + return errors.Errorf("unknown event %s", router.gitlabEventSource.Event) + } + + iev := reflect.New(elem.Type().Elem()) + reflect.Indirect(iev).SetBool(true) + elem.Set(iev) + + logger.Infoln("creating project hook...") + hook, _, err := router.gitlabClient.Projects.AddProjectHook(router.gitlabEventSource.ProjectId, opt) + if err != nil { + return errors.Errorf("failed to add project hook. err: %+v", err) + } + + router.hook = hook + logger.WithField("hook-id", hook.ID).Info("hook created for the project") + return nil +} + +// PostInactivate performs operations after the route is inactivated +func (router *Router) PostInactivate() error { + gitlabEventSource := router.gitlabEventSource + route := router.route + + if gitlabEventSource.DeleteHookOnFinish { + logger := route.Logger.WithFields(map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + "project-id": gitlabEventSource.ProjectId, + "hook-id": router.hook.ID, + }) + + logger.Infoln("deleting project hook...") + if _, err := router.gitlabClient.Projects.DeleteProjectHook(router.gitlabEventSource.ProjectId, router.hook.ID); err != nil { + return errors.Errorf("failed to delete hook. err: %+v", err) + } + + logger.Infoln("gitlab hook deleted") + } + return nil +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Info("started processing the event source...") + + var gitlabEventSource *v1alpha1.GitlabEventSource + if err := yaml.Unmarshal(eventSource.Value, &gitlabEventSource); err != nil { + logger.WithError(err).Error("failed to parse the event source") + return err + } + + route := webhook.NewRoute(gitlabEventSource.Webhook, listener.Logger, eventSource) + + return webhook.ManageRoute(&Router{ + route: route, + k8sClient: listener.K8sClient, + gitlabEventSource: gitlabEventSource, + }, controller, eventStream) +} diff --git a/gateways/server/gitlab/types.go b/gateways/server/gitlab/types.go new file mode 100644 index 0000000000..392be63e1d --- /dev/null +++ b/gateways/server/gitlab/types.go @@ -0,0 +1,54 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitlab + +import ( + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/sirupsen/logrus" + "github.com/xanzy/go-gitlab" + "k8s.io/client-go/kubernetes" +) + +// EventListener implements ConfigExecutor +type EventListener struct { + Logger *logrus.Logger + // K8sClient is kubernetes client + K8sClient kubernetes.Interface +} + +// Router contains the configuration information for a route +type Router struct { + // route contains information about a API endpoint + route *webhook.Route + // K8sClient is the Kubernetes client + k8sClient kubernetes.Interface + // gitlabClient is the client to connect to GitLab + gitlabClient *gitlab.Client + // hook is gitlab project hook + // GitLab API docs: + // https://docs.gitlab.com/ce/api/projects.html#list-project-hooks + hook *gitlab.ProjectHook + // gitlabEventSource is the event source that contains configuration necessary to consume events from GitLab + gitlabEventSource *v1alpha1.GitlabEventSource +} + +// cred stores the api access token +type cred struct { + // token is gitlab api access token + token string +} diff --git a/gateways/server/gitlab/validate.go b/gateways/server/gitlab/validate.go new file mode 100644 index 0000000000..54f5b11467 --- /dev/null +++ b/gateways/server/gitlab/validate.go @@ -0,0 +1,76 @@ +/* +Copyright 2018 BlackRock, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitlab + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates gitlab event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.GitLabEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.GitLabEvent)), + }, nil + } + + var gitlabEventSource *v1alpha1.GitlabEventSource + if err := yaml.Unmarshal(eventSource.Value, &gitlabEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(gitlabEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate gitlab event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.GitlabEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.ProjectId == "" { + return fmt.Errorf("project id can't be empty") + } + if eventSource.Event == "" { + return fmt.Errorf("event type can't be empty") + } + if eventSource.GitlabBaseURL == "" { + return fmt.Errorf("gitlab base url can't be empty") + } + if eventSource.AccessToken == nil { + return fmt.Errorf("access token can't be nil") + } + return webhook.ValidateWebhookContext(eventSource.Webhook) +} diff --git a/gateways/server/gitlab/validate_test.go b/gateways/server/gitlab/validate_test.go new file mode 100644 index 0000000000..cfa36491cc --- /dev/null +++ b/gateways/server/gitlab/validate_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitlab + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +func TestValidateGitlabEventSource(t *testing.T) { + listener := &EventListener{ + Logger: common.NewArgoEventsLogger(), + } + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "gitlab", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("gitlab"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "gitlab.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Gitlab { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "gitlab", + Value: content, + Type: "gitlab", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/hdfs/Dockerfile b/gateways/server/hdfs/Dockerfile similarity index 100% rename from gateways/community/hdfs/Dockerfile rename to gateways/server/hdfs/Dockerfile diff --git a/gateways/community/hdfs/client.go b/gateways/server/hdfs/client.go similarity index 79% rename from gateways/community/hdfs/client.go rename to gateways/server/hdfs/client.go index a8a5724de3..a7c6540357 100644 --- a/gateways/community/hdfs/client.go +++ b/gateways/server/hdfs/client.go @@ -3,6 +3,7 @@ package hdfs import ( "fmt" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/colinmarc/hdfs" krb "gopkg.in/jcmturner/gokrb5.v5/client" "gopkg.in/jcmturner/gokrb5.v5/config" @@ -67,19 +68,19 @@ func getSecretKey(clientset kubernetes.Interface, namespace string, selector *co } // createHDFSConfig constructs HDFSConfig -func createHDFSConfig(clientset kubernetes.Interface, namespace string, config *GatewayClientConfig) (*HDFSConfig, error) { +func createHDFSConfig(clientset kubernetes.Interface, namespace string, hdfsEventSource *v1alpha1.HDFSEventSource) (*HDFSConfig, error) { var krbConfig string var krbOptions *KrbOptions var err error - if config.KrbConfigConfigMap != nil && config.KrbConfigConfigMap.Name != "" { - krbConfig, err = getConfigMapKey(clientset, namespace, config.KrbConfigConfigMap) + if hdfsEventSource.KrbConfigConfigMap != nil && hdfsEventSource.KrbConfigConfigMap.Name != "" { + krbConfig, err = getConfigMapKey(clientset, namespace, hdfsEventSource.KrbConfigConfigMap) if err != nil { return nil, err } } - if config.KrbCCacheSecret != nil && config.KrbCCacheSecret.Name != "" { - bytes, err := getSecretKey(clientset, namespace, config.KrbCCacheSecret) + if hdfsEventSource.KrbCCacheSecret != nil && hdfsEventSource.KrbCCacheSecret.Name != "" { + bytes, err := getSecretKey(clientset, namespace, hdfsEventSource.KrbCCacheSecret) if err != nil { return nil, err } @@ -92,11 +93,11 @@ func createHDFSConfig(clientset kubernetes.Interface, namespace string, config * CCache: ccache, }, Config: krbConfig, - ServicePrincipalName: config.KrbServicePrincipalName, + ServicePrincipalName: hdfsEventSource.KrbServicePrincipalName, } } - if config.KrbKeytabSecret != nil && config.KrbKeytabSecret.Name != "" { - bytes, err := getSecretKey(clientset, namespace, config.KrbKeytabSecret) + if hdfsEventSource.KrbKeytabSecret != nil && hdfsEventSource.KrbKeytabSecret.Name != "" { + bytes, err := getSecretKey(clientset, namespace, hdfsEventSource.KrbKeytabSecret) if err != nil { return nil, err } @@ -107,17 +108,17 @@ func createHDFSConfig(clientset kubernetes.Interface, namespace string, config * krbOptions = &KrbOptions{ KeytabOptions: &KeytabOptions{ Keytab: ktb, - Username: config.KrbUsername, - Realm: config.KrbRealm, + Username: hdfsEventSource.KrbUsername, + Realm: hdfsEventSource.KrbRealm, }, Config: krbConfig, - ServicePrincipalName: config.KrbServicePrincipalName, + ServicePrincipalName: hdfsEventSource.KrbServicePrincipalName, } } hdfsConfig := HDFSConfig{ - Addresses: config.Addresses, - HDFSUser: config.HDFSUser, + Addresses: hdfsEventSource.Addresses, + HDFSUser: hdfsEventSource.HDFSUser, KrbOptions: krbOptions, } return &hdfsConfig, nil diff --git a/gateways/server/hdfs/cmd/main.go b/gateways/server/hdfs/cmd/main.go new file mode 100644 index 0000000000..c789ec5d68 --- /dev/null +++ b/gateways/server/hdfs/cmd/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/hdfs" + "k8s.io/client-go/kubernetes" +) + +func main() { + kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig) + restConfig, err := common.GetClientConfig(kubeConfig) + if err != nil { + panic(err) + } + clientset := kubernetes.NewForConfigOrDie(restConfig) + server.StartGateway(&hdfs.EventListener{ + Logger: common.NewArgoEventsLogger(), + K8sClient: clientset, + }) +} diff --git a/gateways/server/hdfs/start.go b/gateways/server/hdfs/start.go new file mode 100644 index 0000000000..a7f4fe9996 --- /dev/null +++ b/gateways/server/hdfs/start.go @@ -0,0 +1,182 @@ +package hdfs + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/common/fsevent" + "github.com/argoproj/argo-events/gateways/server/common/naivewatcher" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/colinmarc/hdfs" + "github.com/ghodss/yaml" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" +) + +// EventListener implements Eventing for HDFS events +type EventListener struct { + // Logger logs stuff + Logger *logrus.Logger + // k8sClient is kubernetes client + K8sClient kubernetes.Interface +} + +// WatchableHDFS wraps hdfs.Client for naivewatcher +type WatchableHDFS struct { + hdfscli *hdfs.Client +} + +// Walk walks a directory +func (w *WatchableHDFS) Walk(root string, walkFn filepath.WalkFunc) error { + return w.hdfscli.Walk(root, walkFn) +} + +// GetFileID returns the file ID +func (w *WatchableHDFS) GetFileID(fi os.FileInfo) interface{} { + return fi.Name() + // FIXME: Use HDFS File ID once it's exposed + // https://github.com/colinmarc/hdfs/pull/171 + // return fi.Sys().(*hadoop_hdfs.HdfsFileStatusProto).GetFileID() +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Info("start processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listens to HDFS events +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing the event source...") + + var hdfsEventSource *v1alpha1.HDFSEventSource + if err := yaml.Unmarshal(eventSource.Value, &hdfsEventSource); err != nil { + errorCh <- err + return + } + + logger.Infoln("setting up HDFS configuration...") + hdfsConfig, err := createHDFSConfig(listener.K8sClient, hdfsEventSource.Namespace, hdfsEventSource) + if err != nil { + errorCh <- err + return + } + + logger.Infoln("setting up HDFS client...") + hdfscli, err := createHDFSClient(hdfsConfig.Addresses, hdfsConfig.HDFSUser, hdfsConfig.KrbOptions) + if err != nil { + errorCh <- err + return + } + defer hdfscli.Close() + + logger.Infoln("setting up a new watcher...") + watcher, err := naivewatcher.NewWatcher(&WatchableHDFS{hdfscli: hdfscli}) + if err != nil { + errorCh <- err + return + } + defer watcher.Close() + + intervalDuration := 1 * time.Minute + if hdfsEventSource.CheckInterval != "" { + d, err := time.ParseDuration(hdfsEventSource.CheckInterval) + if err != nil { + errorCh <- err + return + } + intervalDuration = d + } + + logger.Infoln("started HDFS watcher") + err = watcher.Start(intervalDuration) + if err != nil { + errorCh <- err + return + } + + // directory to watch must be available in HDFS. You can't watch a directory that is not present. + logger.Infoln("adding configured directory to watcher...") + err = watcher.Add(hdfsEventSource.Directory) + if err != nil { + errorCh <- err + return + } + + op := fsevent.NewOp(hdfsEventSource.Type) + var pathRegexp *regexp.Regexp + if hdfsEventSource.PathRegexp != "" { + pathRegexp, err = regexp.Compile(hdfsEventSource.PathRegexp) + if err != nil { + errorCh <- err + return + } + } + + logger.Infoln("listening to HDFS notifications...") + for { + select { + case event, ok := <-watcher.Events: + if !ok { + logger.Info("HDFS watcher has stopped") + // watcher stopped watching file events + errorCh <- fmt.Errorf("HDFS watcher stopped") + return + } + matched := false + relPath := strings.TrimPrefix(event.Name, hdfsEventSource.Directory) + + if hdfsEventSource.Path != "" && hdfsEventSource.Path == relPath { + matched = true + } else if pathRegexp != nil && pathRegexp.MatchString(relPath) { + matched = true + } + + if matched && (op&event.Op != 0) { + logger := logger.WithFields( + map[string]interface{}{ + "event-type": event.Op.String(), + "descriptor-name": event.Name, + }, + ) + logger.Infoln("received an event") + + logger.Infoln("parsing the event...") + payload, err := json.Marshal(event) + if err != nil { + errorCh <- err + return + } + + logger.Infoln("dispatching event on data channel") + dataCh <- payload + } + case err := <-watcher.Errors: + errorCh <- err + return + case <-doneCh: + return + } + } +} diff --git a/gateways/server/hdfs/validate.go b/gateways/server/hdfs/validate.go new file mode 100644 index 0000000000..4c546fd13e --- /dev/null +++ b/gateways/server/hdfs/validate.go @@ -0,0 +1,101 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hdfs + +import ( + "context" + "errors" + "time" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/fsevent" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates hdfs event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.HDFSEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.HDFSEvent)), + }, nil + } + + var hdfsEventSource *v1alpha1.HDFSEventSource + if err := yaml.Unmarshal(eventSource.Value, &hdfsEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(hdfsEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate HDFS event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.HDFSEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.Type == "" { + return errors.New("type is required") + } + op := fsevent.NewOp(eventSource.Type) + if op == 0 { + return errors.New("type is invalid") + } + if eventSource.CheckInterval != "" { + _, err := time.ParseDuration(eventSource.CheckInterval) + if err != nil { + return errors.New("failed to parse interval") + } + } + err := eventSource.WatchPathConfig.Validate() + if err != nil { + return err + } + if len(eventSource.Addresses) == 0 { + return errors.New("addresses is required") + } + + hasKrbCCache := eventSource.KrbCCacheSecret != nil + hasKrbKeytab := eventSource.KrbKeytabSecret != nil + + if eventSource.HDFSUser == "" && !hasKrbCCache && !hasKrbKeytab { + return errors.New("either hdfsUser, krbCCacheSecret or krbKeytabSecret is required") + } + if hasKrbKeytab && (eventSource.KrbServicePrincipalName == "" || eventSource.KrbConfigConfigMap == nil || eventSource.KrbUsername == "" || eventSource.KrbRealm == "") { + return errors.New("krbServicePrincipalName, krbConfigConfigMap, krbUsername and krbRealm are required with krbKeytabSecret") + } + if hasKrbCCache && (eventSource.KrbServicePrincipalName == "" || eventSource.KrbConfigConfigMap == nil) { + return errors.New("krbServicePrincipalName and krbConfigConfigMap are required with krbCCacheSecret") + } + return err +} diff --git a/gateways/server/hdfs/validate_test.go b/gateways/server/hdfs/validate_test.go new file mode 100644 index 0000000000..172c2614be --- /dev/null +++ b/gateways/server/hdfs/validate_test.go @@ -0,0 +1,50 @@ +package hdfs + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateEventSource(t *testing.T) { + listener := &EventListener{ + Logger: common.NewArgoEventsLogger(), + } + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "hdfs", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("hdfs"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "hdfs.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.HDFS { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "hdfs", + Value: content, + Type: "hdfs", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/core/stream/kafka/Dockerfile b/gateways/server/kafka/Dockerfile similarity index 100% rename from gateways/core/stream/kafka/Dockerfile rename to gateways/server/kafka/Dockerfile diff --git a/gateways/core/stream/amqp/cmd/main.go b/gateways/server/kafka/cmd/main.go similarity index 76% rename from gateways/core/stream/amqp/cmd/main.go rename to gateways/server/kafka/cmd/main.go index 6018d125c0..c145d9941a 100644 --- a/gateways/core/stream/amqp/cmd/main.go +++ b/gateways/server/kafka/cmd/main.go @@ -18,12 +18,12 @@ package main import ( "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/stream/amqp" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/kafka" ) func main() { - gateways.StartGateway(&amqp.AMQPEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&kafka.EventListener{ + Logger: common.NewArgoEventsLogger(), }) } diff --git a/gateways/server/kafka/start.go b/gateways/server/kafka/start.go new file mode 100644 index 0000000000..b117f9cfc5 --- /dev/null +++ b/gateways/server/kafka/start.go @@ -0,0 +1,143 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kafka + +import ( + "strconv" + + "github.com/Shopify/sarama" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" +) + +// EventListener implements Eventing kafka event source +type EventListener struct { + // Logger logs stuff + Logger *logrus.Logger +} + +func verifyPartitionAvailable(part int32, partitions []int32) bool { + for _, p := range partitions { + if part == p { + return true + } + } + return false +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing the event source...") + var kafkaEventSource *v1alpha1.KafkaEventSource + if err := yaml.Unmarshal(eventSource.Value, kafkaEventSource); err != nil { + errorCh <- err + return + } + + var consumer sarama.Consumer + + logger.Infoln("connecting to Kafka cluster...") + if err := server.Connect(&wait.Backoff{ + Steps: kafkaEventSource.ConnectionBackoff.Steps, + Jitter: kafkaEventSource.ConnectionBackoff.Jitter, + Duration: kafkaEventSource.ConnectionBackoff.Duration, + Factor: kafkaEventSource.ConnectionBackoff.Factor, + }, func() error { + var err error + consumer, err = sarama.NewConsumer([]string{kafkaEventSource.URL}, nil) + if err != nil { + return err + } + return nil + }); err != nil { + logger.WithError(err).WithField(common.LabelURL, kafkaEventSource.URL).Error("failed to connect") + errorCh <- err + return + } + + logger = logger.WithField("partition-id", kafkaEventSource.Partition) + + logger.Infoln("parsing the partition value...") + pInt, err := strconv.ParseInt(kafkaEventSource.Partition, 10, 32) + if err != nil { + errorCh <- err + return + } + partition := int32(pInt) + + logger.Infoln("getting available partitions...") + availablePartitions, err := consumer.Partitions(kafkaEventSource.Topic) + if err != nil { + errorCh <- err + return + } + + logger.Infoln("verifying the partition exists within available partitions...") + if ok := verifyPartitionAvailable(partition, availablePartitions); !ok { + errorCh <- errors.Errorf("partition %d is not available", partition) + return + } + + logger.Infoln("getting partition consumer...") + partitionConsumer, err := consumer.ConsumePartition(kafkaEventSource.Topic, partition, sarama.OffsetNewest) + if err != nil { + errorCh <- err + return + } + + logger.Info("listening to messages on the partition...") + for { + select { + case msg := <-partitionConsumer.Messages(): + logger.Infoln("dispatching event on the data channel...") + dataCh <- msg.Value + + case err := <-partitionConsumer.Errors(): + errorCh <- err + return + + case <-doneCh: + err = partitionConsumer.Close() + if err != nil { + logger.WithError(err).Error("failed to close consumer") + } + return + } + } +} diff --git a/gateways/server/kafka/validate.go b/gateways/server/kafka/validate.go new file mode 100644 index 0000000000..1b1cd9b481 --- /dev/null +++ b/gateways/server/kafka/validate.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kafka + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates the gateway event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.KafkaEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.KafkaEvent)), + }, nil + } + + var kafkaGridEventSource *v1alpha1.KafkaEventSource + if err := yaml.Unmarshal(eventSource.Value, &kafkaGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(kafkaGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate kafka event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.KafkaEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.URL == "" { + return fmt.Errorf("url must be specified") + } + if eventSource.Topic == "" { + return fmt.Errorf("topic must be specified") + } + if eventSource.Partition == "" { + return fmt.Errorf("partition must be specified") + } + return nil +} diff --git a/gateways/server/kafka/validate_test.go b/gateways/server/kafka/validate_test.go new file mode 100644 index 0000000000..ecc555ca6b --- /dev/null +++ b/gateways/server/kafka/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kafka + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateKafkaEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "kafka", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("kafka"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "kafka.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Kafka { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "kafka", + Value: content, + Type: "kafka", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/server/minio/Dockerfile b/gateways/server/minio/Dockerfile new file mode 100644 index 0000000000..893b248671 --- /dev/null +++ b/gateways/server/minio/Dockerfile @@ -0,0 +1,3 @@ +FROM centos:7 +COPY dist/minio-gateway /bin/ +ENTRYPOINT [ "/bin/minio-gateway" ] \ No newline at end of file diff --git a/gateways/community/slack/cmd/main.go b/gateways/server/minio/cmd/main.go similarity index 74% rename from gateways/community/slack/cmd/main.go rename to gateways/server/minio/cmd/main.go index 57c07e1961..2036149a18 100644 --- a/gateways/community/slack/cmd/main.go +++ b/gateways/server/minio/cmd/main.go @@ -20,8 +20,8 @@ import ( "os" "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/slack" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/minio" "k8s.io/client-go/kubernetes" ) @@ -32,13 +32,15 @@ func main() { panic(err) } clientset := kubernetes.NewForConfigOrDie(restConfig) - namespace, ok := os.LookupEnv(common.EnvVarGatewayNamespace) + + namespace, ok := os.LookupEnv(common.EnvVarNamespace) if !ok { panic("namespace is not provided") } - gateways.StartGateway(&slack.SlackEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - Clientset: clientset, - Namespace: namespace, + + server.StartGateway(&minio.EventListener{ + common.NewArgoEventsLogger(), + clientset, + namespace, }) } diff --git a/gateways/server/minio/start.go b/gateways/server/minio/start.go new file mode 100644 index 0000000000..8a19617bca --- /dev/null +++ b/gateways/server/minio/start.go @@ -0,0 +1,108 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package minio + +import ( + "encoding/json" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/store" + "github.com/ghodss/yaml" + "github.com/minio/minio-go" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" +) + +// MinioEventSourceListener implements Eventing for minio event sources +type EventListener struct { + // Logger + Logger *logrus.Logger + // K8sClient is kubernetes client + K8sClient kubernetes.Interface + // Namespace where gateway is deployed + Namespace string +} + +// StartEventSource activates an event source and streams back events +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("activating the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listens to minio bucket notifications +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + Logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + Logger.Infoln("parsing minio event source...") + + var minioEventSource *apicommon.S3Artifact + err := yaml.Unmarshal(eventSource.Value, &minioEventSource) + if err != nil { + errorCh <- err + return + } + + Logger.Info("started processing the event source...") + + Logger.Info("retrieving access and secret key...") + accessKey, err := store.GetSecrets(listener.K8sClient, listener.Namespace, minioEventSource.AccessKey.Name, minioEventSource.AccessKey.Key) + if err != nil { + errorCh <- err + return + } + secretKey, err := store.GetSecrets(listener.K8sClient, listener.Namespace, minioEventSource.SecretKey.Name, minioEventSource.SecretKey.Key) + if err != nil { + errorCh <- err + return + } + + Logger.Infoln("setting up a minio client...") + minioClient, err := minio.New(minioEventSource.Endpoint, accessKey, secretKey, !minioEventSource.Insecure) + if err != nil { + errorCh <- err + return + } + + Logger.Info("started listening to bucket notifications...") + for notification := range minioClient.ListenBucketNotification(minioEventSource.Bucket.Name, minioEventSource.Filter.Prefix, minioEventSource.Filter.Suffix, minioEventSource.Events, doneCh) { + if notification.Err != nil { + errorCh <- notification.Err + return + } + + Logger.Infoln("parsing notification from minio...") + payload, err := json.Marshal(notification.Records[0]) + if err != nil { + errorCh <- err + return + } + + Logger.Infoln("dispatching notification on data channel...") + dataCh <- payload + } +} diff --git a/gateways/core/artifact/start_test.go b/gateways/server/minio/start_test.go similarity index 59% rename from gateways/core/artifact/start_test.go rename to gateways/server/minio/start_test.go index 053449cc12..64474022e6 100644 --- a/gateways/core/artifact/start_test.go +++ b/gateways/server/minio/start_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package artifact +package minio import ( "testing" @@ -22,6 +22,7 @@ import ( "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/gateways" apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/ghodss/yaml" "github.com/smartystreets/goconvey/convey" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,15 +31,15 @@ import ( func TestListeEvents(t *testing.T) { convey.Convey("Given an event source, listen to events", t, func() { - ese := &S3EventSourceExecutor{ - Clientset: fake.NewSimpleClientset(), - Log: common.NewArgoEventsLogger(), + listener := &EventListener{ + K8sClient: fake.NewSimpleClientset(), + Logger: common.NewArgoEventsLogger(), Namespace: "fake", } - secret, err := ese.Clientset.CoreV1().Secrets(ese.Namespace).Create(&corev1.Secret{ + secret, err := listener.K8sClient.CoreV1().Secrets(listener.Namespace).Create(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "artifacts-minio", - Namespace: ese.Namespace, + Namespace: listener.Namespace, }, Data: map[string][]byte{ "accesskey": []byte("access"), @@ -58,12 +59,42 @@ func TestListeEvents(t *testing.T) { errCh2 <- err }() - ps, err := parseEventSource(es) + minioEventSource := &apicommon.S3Artifact{ + Bucket: &apicommon.S3Bucket{ + Name: "input", + }, + Endpoint: "minio-service.argo-events:9000", + Events: []string{ + "s3:ObjectCreated:Put", + }, + Filter: &apicommon.S3Filter{ + Prefix: "", + Suffix: "", + }, + Insecure: true, + AccessKey: &corev1.SecretKeySelector{ + Key: "accesskey", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "artifacts-minio", + }, + }, + SecretKey: &corev1.SecretKeySelector{ + Key: "secretkey", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "artifacts-minio", + }, + }, + } + convey.So(err, convey.ShouldBeNil) - ese.listenEvents(ps.(*apicommon.S3Artifact), &gateways.EventSource{ - Id: "1234", - Data: es, - Name: "fake", + + body, err := yaml.Marshal(minioEventSource) + convey.So(err, convey.ShouldBeNil) + + listener.listenEvents(&gateways.EventSource{ + Id: "1234", + Value: body, + Name: "fake", }, dataCh, errorCh, doneCh) err = <-errCh2 diff --git a/gateways/server/minio/validate.go b/gateways/server/minio/validate.go new file mode 100644 index 0000000000..377af789a9 --- /dev/null +++ b/gateways/server/minio/validate.go @@ -0,0 +1,84 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package minio + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/ghodss/yaml" + "github.com/minio/minio-go" +) + +// ValidateEventSource validates the minio event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.MinioEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.MinioEvent)), + }, nil + } + + var minioEventSource *apicommon.S3Artifact + err := yaml.Unmarshal(eventSource.Value, &minioEventSource) + if err != nil { + listener.Logger.WithError(err).Error("failed to parse the minio event source") + return &gateways.ValidEventSource{ + Reason: err.Error(), + IsValid: false, + }, nil + } + + if err := validate(minioEventSource); err != nil { + return &gateways.ValidEventSource{ + Reason: err.Error(), + IsValid: false, + }, nil + } + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *apicommon.S3Artifact) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.AccessKey == nil { + return fmt.Errorf("access key can't be empty") + } + if eventSource.SecretKey == nil { + return fmt.Errorf("secret key can't be empty") + } + if eventSource.Endpoint == "" { + return fmt.Errorf("endpoint url can't be empty") + } + if eventSource.Bucket != nil && eventSource.Bucket.Name == "" { + return fmt.Errorf("bucket name can't be empty") + } + if eventSource.Events != nil { + for _, event := range eventSource.Events { + if minio.NotificationEventType(event) == "" { + return fmt.Errorf("unknown event %s", event) + } + } + } + return nil +} diff --git a/gateways/server/minio/validate_test.go b/gateways/server/minio/validate_test.go new file mode 100644 index 0000000000..1ab9f89d47 --- /dev/null +++ b/gateways/server/minio/validate_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package minio + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateS3EventSource(t *testing.T) { + listener := &EventListener{ + Logger: common.NewArgoEventsLogger(), + } + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "minio", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("minio"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "minio.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Minio { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "minio", + Value: content, + Type: "minio", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/core/stream/mqtt/Dockerfile b/gateways/server/mqtt/Dockerfile similarity index 100% rename from gateways/core/stream/mqtt/Dockerfile rename to gateways/server/mqtt/Dockerfile diff --git a/gateways/core/stream/mqtt/cmd/main.go b/gateways/server/mqtt/cmd/main.go similarity index 76% rename from gateways/core/stream/mqtt/cmd/main.go rename to gateways/server/mqtt/cmd/main.go index 14a4c1dc3c..c383529f16 100644 --- a/gateways/core/stream/mqtt/cmd/main.go +++ b/gateways/server/mqtt/cmd/main.go @@ -18,12 +18,12 @@ package main import ( "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/stream/mqtt" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/mqtt" ) func main() { - gateways.StartGateway(&mqtt.MqttEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&mqtt.EventListener{ + Logger: common.NewArgoEventsLogger(), }) } diff --git a/gateways/server/mqtt/start.go b/gateways/server/mqtt/start.go new file mode 100644 index 0000000000..af429fa2b4 --- /dev/null +++ b/gateways/server/mqtt/start.go @@ -0,0 +1,110 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mqtt + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + mqttlib "github.com/eclipse/paho.mqtt.golang" + "github.com/ghodss/yaml" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" +) + +// EventListener implements Eventing for mqtt event source +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listens to events from a mqtt broker +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing the event source...") + var mqttEventSource *v1alpha1.MQTTEventSource + if err := yaml.Unmarshal(eventSource.Value, &mqttEventSource); err != nil { + errorCh <- err + return + } + + logger = logger.WithFields( + map[string]interface{}{ + common.LabelURL: mqttEventSource.URL, + common.LabelClientID: mqttEventSource.ClientId, + }, + ) + + logger.Infoln("setting up the message handler...") + handler := func(c mqttlib.Client, msg mqttlib.Message) { + logger.Infoln("dispatching event on data channel...") + dataCh <- msg.Payload() + } + + logger.Infoln("setting up the mqtt broker client...") + opts := mqttlib.NewClientOptions().AddBroker(mqttEventSource.URL).SetClientID(mqttEventSource.ClientId) + + var client mqttlib.Client + + logger.Infoln("connecting to mqtt broker...") + if err := server.Connect(&wait.Backoff{ + Factor: mqttEventSource.ConnectionBackoff.Factor, + Duration: mqttEventSource.ConnectionBackoff.Duration, + Jitter: mqttEventSource.ConnectionBackoff.Jitter, + Steps: mqttEventSource.ConnectionBackoff.Steps, + }, func() error { + client = mqttlib.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + return token.Error() + } + return nil + }); err != nil { + logger.Info("failed to connect") + errorCh <- err + return + } + + logger.Info("subscribing to the topic...") + if token := client.Subscribe(mqttEventSource.Topic, 0, handler); token.Wait() && token.Error() != nil { + logger.WithError(token.Error()).Error("failed to subscribe") + errorCh <- token.Error() + return + } + + <-doneCh + token := client.Unsubscribe(mqttEventSource.Topic) + if token.Error() != nil { + logger.WithError(token.Error()).Error("failed to unsubscribe client") + } +} diff --git a/gateways/server/mqtt/validate.go b/gateways/server/mqtt/validate.go new file mode 100644 index 0000000000..d1b0eb5c78 --- /dev/null +++ b/gateways/server/mqtt/validate.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mqtt + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates mqtt event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.MQTTEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.MQTTEvent)), + }, nil + } + + var mqttGridEventSource *v1alpha1.MQTTEventSource + if err := yaml.Unmarshal(eventSource.Value, &mqttGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(mqttGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate mqtt event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.MQTTEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.URL == "" { + return fmt.Errorf("url must be specified") + } + if eventSource.Topic == "" { + return fmt.Errorf("topic must be specified") + } + if eventSource.ClientId == "" { + return fmt.Errorf("client id must be specified") + } + return nil +} diff --git a/gateways/server/mqtt/validate_test.go b/gateways/server/mqtt/validate_test.go new file mode 100644 index 0000000000..47d96bad0d --- /dev/null +++ b/gateways/server/mqtt/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mqtt + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +func TestValidateMqttEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "mqtt", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("mqtt"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "mqtt.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.MQTT { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "mqtt", + Value: content, + Type: "mqtt", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/core/stream/nats/Dockerfile b/gateways/server/nats/Dockerfile similarity index 100% rename from gateways/core/stream/nats/Dockerfile rename to gateways/server/nats/Dockerfile diff --git a/gateways/server/nats/cmd/main.go b/gateways/server/nats/cmd/main.go new file mode 100644 index 0000000000..75f311a647 --- /dev/null +++ b/gateways/server/nats/cmd/main.go @@ -0,0 +1,29 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/nats" +) + +func main() { + server.StartGateway(&nats.EventListener{ + Logger: common.NewArgoEventsLogger(), + }) +} diff --git a/gateways/server/nats/start.go b/gateways/server/nats/start.go new file mode 100644 index 0000000000..6d00211fc1 --- /dev/null +++ b/gateways/server/nats/start.go @@ -0,0 +1,109 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nats + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + natslib "github.com/nats-io/go-nats" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" +) + +// EventListener implements Eventing for nats event source +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("started processing the event source...") + + dataCh := make(chan []byte) + errorCh := make(chan error) + doneCh := make(chan struct{}, 1) + + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) + + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) +} + +// listenEvents listens events from nats cluster +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("parsing the event source...") + var natsEventSource *v1alpha1.NATSEventsSource + if err := yaml.Unmarshal(eventSource.Value, &natsEventSource); err != nil { + errorCh <- err + return + } + + logger = logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: eventSource.Name, + common.LabelURL: natsEventSource.URL, + "subject": natsEventSource.Subject, + }, + ) + + var conn *natslib.Conn + + logger.Infoln("connecting to nats cluster...") + if err := server.Connect(&wait.Backoff{ + Steps: natsEventSource.ConnectionBackoff.Steps, + Jitter: natsEventSource.ConnectionBackoff.Jitter, + Duration: natsEventSource.ConnectionBackoff.Duration, + Factor: natsEventSource.ConnectionBackoff.Factor, + }, func() error { + var err error + if conn, err = natslib.Connect(natsEventSource.URL); err != nil { + return err + } + return nil + }); err != nil { + logger.WithError(err).Error("failed to connect to nats cluster") + errorCh <- err + return + } + + logger.Info("subscribing to messages on the queue...") + _, err := conn.Subscribe(natsEventSource.Subject, func(msg *natslib.Msg) { + logger.Infoln("dispatching event on data channel...") + dataCh <- msg.Data + }) + + if err != nil { + logger.WithError(err).Error("failed to subscribe") + errorCh <- err + return + } + + conn.Flush() + if err := conn.LastError(); err != nil { + errorCh <- err + return + } + + <-doneCh +} diff --git a/gateways/server/nats/validate.go b/gateways/server/nats/validate.go new file mode 100644 index 0000000000..2e11bcd677 --- /dev/null +++ b/gateways/server/nats/validate.go @@ -0,0 +1,72 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nats + +import ( + "context" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/pkg/errors" +) + +// ValidateEventSource validates nats event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.NATSEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.NATSEvent)), + }, nil + } + + var natsGridEventSource *v1alpha1.NATSEventsSource + if err := yaml.Unmarshal(eventSource.Value, &natsGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(natsGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate nats event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.NATSEventsSource) error { + if eventSource == nil { + return errors.New("configuration must be non empty") + } + if eventSource.URL == "" { + return errors.New("url must be specified") + } + if eventSource.Subject == "" { + return errors.New("subject must be specified") + } + return nil +} diff --git a/gateways/server/nats/validate_test.go b/gateways/server/nats/validate_test.go new file mode 100644 index 0000000000..105588d0f8 --- /dev/null +++ b/gateways/server/nats/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nats + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidateNatsEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "nats", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("nats"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "nats.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.NATS { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "nats", + Value: content, + Type: "nats", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/core/resource/Dockerfile b/gateways/server/resource/Dockerfile similarity index 100% rename from gateways/core/resource/Dockerfile rename to gateways/server/resource/Dockerfile diff --git a/gateways/core/resource/cmd/main.go b/gateways/server/resource/cmd/main.go similarity index 79% rename from gateways/core/resource/cmd/main.go rename to gateways/server/resource/cmd/main.go index 8ca6c1d882..242e3b876e 100644 --- a/gateways/core/resource/cmd/main.go +++ b/gateways/server/resource/cmd/main.go @@ -20,8 +20,8 @@ import ( "os" "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/resource" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/resource" ) func main() { @@ -30,8 +30,8 @@ func main() { if err != nil { panic(err) } - gateways.StartGateway(&resource.ResourceEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&resource.EventListener{ + Logger: common.NewArgoEventsLogger(), K8RestConfig: rest, }) } diff --git a/gateways/core/resource/start.go b/gateways/server/resource/start.go similarity index 62% rename from gateways/core/resource/start.go rename to gateways/server/resource/start.go index 333d4b7f6f..b5e1de18fc 100644 --- a/gateways/core/resource/start.go +++ b/gateways/server/resource/start.go @@ -23,7 +23,11 @@ import ( "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" "github.com/pkg/errors" + "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" @@ -32,53 +36,73 @@ import ( "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" ) -// StartEventSource starts an event source -func (executor *ResourceEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - log := executor.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("operating on event source") +// InformerEvent holds event generated from resource state change +type InformerEvent struct { + Obj interface{} + OldObj interface{} + Type v1alpha1.ResourceEventType +} - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") - return err - } +// EventListener implements Eventing +type EventListener struct { + // Logger to log stuff + Logger *logrus.Logger + // K8RestConfig is kubernetes cluster config + K8RestConfig *rest.Config +} + +// StartEventSource starts an event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + listener.Logger.WithField(common.LabelEventSource, eventSource.Name).Infoln("activating the event source...") dataCh := make(chan []byte) errorCh := make(chan error) doneCh := make(chan struct{}, 1) - go executor.listenEvents(config.(*resource), eventSource, dataCh, errorCh, doneCh) + go listener.listenEvents(eventSource, dataCh, errorCh, doneCh) - return gateways.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, executor.Log) + return server.HandleEventsFromEventSource(eventSource.Name, eventStream, dataCh, errorCh, doneCh, listener.Logger) } // listenEvents watches resource updates and consume those events -func (executor *ResourceEventSourceExecutor) listenEvents(resourceCfg *resource, eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { - defer gateways.Recover(eventSource.Name) +func (listener *EventListener) listenEvents(eventSource *gateways.EventSource, dataCh chan []byte, errorCh chan error, doneCh chan struct{}) { + defer server.Recover(eventSource.Name) + + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + logger.Infoln("started processing the event source...") - executor.Log.WithField(common.LabelEventSource, eventSource.Name).Info("started listening resource notifications") + logger.Infoln("parsing resource event source...") + var resourceEventSource *v1alpha1.ResourceEventSource + if err := yaml.Unmarshal(eventSource.Value, &resourceEventSource); err != nil { + errorCh <- err + return + } - client, err := dynamic.NewForConfig(executor.K8RestConfig) + logger.Infoln("setting up a K8s client") + client, err := dynamic.NewForConfig(listener.K8RestConfig) if err != nil { errorCh <- err return } gvr := schema.GroupVersionResource{ - Group: resourceCfg.Group, - Version: resourceCfg.Version, - Resource: resourceCfg.Resource, + Group: resourceEventSource.Group, + Version: resourceEventSource.Version, + Resource: resourceEventSource.Resource, } client.Resource(gvr) options := &metav1.ListOptions{} - if resourceCfg.Filter != nil && resourceCfg.Filter.Labels != nil { - sel, err := LabelSelector(resourceCfg.Filter.Labels) + logger.Infoln("configuring label selectors if filters are selected...") + if resourceEventSource.Filter != nil && resourceEventSource.Filter.Labels != nil { + sel, err := LabelSelector(resourceEventSource.Filter.Labels) if err != nil { errorCh <- err return @@ -86,8 +110,8 @@ func (executor *ResourceEventSourceExecutor) listenEvents(resourceCfg *resource, options.LabelSelector = sel.String() } - if resourceCfg.Filter != nil && resourceCfg.Filter.Fields != nil { - sel, err := LabelSelector(resourceCfg.Filter.Fields) + if resourceEventSource.Filter != nil && resourceEventSource.Filter.Fields != nil { + sel, err := LabelSelector(resourceEventSource.Filter.Fields) if err != nil { errorCh <- err return @@ -99,13 +123,15 @@ func (executor *ResourceEventSourceExecutor) listenEvents(resourceCfg *resource, op = options } - factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(client, 0, resourceCfg.Namespace, tweakListOptions) + logger.Infoln("setting up informer factory...") + factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(client, 0, resourceEventSource.Namespace, tweakListOptions) informer := factory.ForResource(gvr) informerEventCh := make(chan *InformerEvent) go func() { + logger.Infoln("listening to resource events...") for { select { case event, ok := <-informerEventCh: @@ -114,11 +140,11 @@ func (executor *ResourceEventSourceExecutor) listenEvents(resourceCfg *resource, } eventBody, err := json.Marshal(event) if err != nil { - executor.Log.WithField(common.LabelEventSource, eventSource.Name).WithError(err).Errorln("failed to parse event from resource informer") + logger.WithError(err).Errorln("failed to parse event from resource informer") continue } - if err := passFilters(event.Obj.(*unstructured.Unstructured), resourceCfg.Filter); err != nil { - executor.Log.WithField(common.LabelEventSource, eventSource.Name).WithError(err).Warnln("failed to apply the filter") + if err := passFilters(event.Obj.(*unstructured.Unstructured), resourceEventSource.Filter); err != nil { + logger.WithError(err).Warnln("failed to apply the filter") continue } dataCh <- eventBody @@ -132,27 +158,27 @@ func (executor *ResourceEventSourceExecutor) listenEvents(resourceCfg *resource, AddFunc: func(obj interface{}) { informerEventCh <- &InformerEvent{ Obj: obj, - Type: ADD, + Type: v1alpha1.ADD, } }, UpdateFunc: func(oldObj, newObj interface{}) { informerEventCh <- &InformerEvent{ Obj: newObj, OldObj: oldObj, - Type: UPDATE, + Type: v1alpha1.UPDATE, } }, DeleteFunc: func(obj interface{}) { informerEventCh <- &InformerEvent{ Obj: obj, - Type: DELETE, + Type: v1alpha1.DELETE, } }, }, ) sharedInformer.Run(doneCh) - executor.Log.WithField(common.LabelEventSource, eventSource.Name).Infoln("resource informer is stopped") + logger.Infoln("resource informer is stopped") close(informerEventCh) close(doneCh) } @@ -193,7 +219,7 @@ func FieldSelector(fieldSelectors map[string]string) (fields.Selector, error) { } // helper method to check if the object passed the user defined filters -func passFilters(obj *unstructured.Unstructured, filter *ResourceFilter) error { +func passFilters(obj *unstructured.Unstructured, filter *v1alpha1.ResourceFilter) error { // no filters are applied. if filter == nil { return nil diff --git a/gateways/core/resource/start_test.go b/gateways/server/resource/start_test.go similarity index 72% rename from gateways/core/resource/start_test.go rename to gateways/server/resource/start_test.go index f0c06d0b9a..0ece326906 100644 --- a/gateways/core/resource/start_test.go +++ b/gateways/server/resource/start_test.go @@ -17,19 +17,33 @@ limitations under the License. package resource import ( + "testing" + + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/mitchellh/mapstructure" "github.com/smartystreets/goconvey/convey" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/kubernetes/fake" - "testing" ) func TestFilter(t *testing.T) { convey.Convey("Given a resource object, apply filter on it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) + resourceEventSource := &v1alpha1.ResourceEventSource{ + Namespace: "fake", + GroupVersionResource: metav1.GroupVersionResource{ + Group: "", + Resource: "pods", + Version: "v1", + }, + Filter: &v1alpha1.ResourceFilter{ + Labels: map[string]string{ + "workflows.argoproj.io/phase": "Succeeded", + "name": "my-workflow", + }, + }, + } pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "fake", @@ -40,7 +54,7 @@ func TestFilter(t *testing.T) { }, }, } - pod, err = fake.NewSimpleClientset().CoreV1().Pods("fake").Create(pod) + pod, err := fake.NewSimpleClientset().CoreV1().Pods("fake").Create(pod) convey.So(err, convey.ShouldBeNil) outmap := make(map[string]interface{}) @@ -49,7 +63,7 @@ func TestFilter(t *testing.T) { err = passFilters(&unstructured.Unstructured{ Object: outmap, - }, ps.(*resource).Filter) + }, resourceEventSource.Filter) convey.So(err, convey.ShouldBeNil) }) } diff --git a/gateways/server/resource/validate.go b/gateways/server/resource/validate.go new file mode 100644 index 0000000000..727dec6652 --- /dev/null +++ b/gateways/server/resource/validate.go @@ -0,0 +1,72 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates a resource event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.ResourceEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.ResourceEvent)), + }, nil + } + + var resourceEventSource *v1alpha1.ResourceEventSource + if err := yaml.Unmarshal(eventSource.Value, &resourceEventSource); err != nil { + listener.Logger.WithError(err).Errorln("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, err + } + + if err := validate(resourceEventSource); err != nil { + listener.Logger.WithError(err).Errorln("failed to validate the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, err + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.ResourceEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.Version == "" { + return fmt.Errorf("version must be specified") + } + if eventSource.Resource == "" { + return fmt.Errorf("resource must be specified") + } + return nil +} diff --git a/gateways/server/resource/validate_test.go b/gateways/server/resource/validate_test.go new file mode 100644 index 0000000000..ea6e87cddb --- /dev/null +++ b/gateways/server/resource/validate_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestEventListener_ValidateEventSource(t *testing.T) { + listener := &EventListener{ + Logger: common.NewArgoEventsLogger(), + } + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "resource", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("resource"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "resource.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Resource { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "resource", + Value: content, + Type: "resource", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/gateway.go b/gateways/server/server.go similarity index 80% rename from gateways/gateway.go rename to gateways/server/server.go index e29c6d9b3e..dc7bc407d2 100644 --- a/gateways/gateway.go +++ b/gateways/server/server.go @@ -14,20 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package server import ( "fmt" + "net" + "os" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" "github.com/sirupsen/logrus" "google.golang.org/grpc" - "net" - "os" - "runtime/debug" ) // StartGateway start a gateway -func StartGateway(es EventingServer) { +func StartGateway(es gateways.EventingServer) { port, ok := os.LookupEnv(common.EnvVarGatewayServerPort) if !ok { panic(fmt.Errorf("port is not provided")) @@ -37,7 +38,7 @@ func StartGateway(es EventingServer) { panic(err) } srv := grpc.NewServer() - RegisterEventingServer(srv, es) + gateways.RegisterEventingServer(srv, es) fmt.Println("starting gateway server") @@ -50,17 +51,16 @@ func StartGateway(es EventingServer) { func Recover(eventSource string) { if r := recover(); r != nil { fmt.Printf("recovered event source %s from error. recover: %v", eventSource, r) - debug.PrintStack() } } // HandleEventsFromEventSource handles events from the event source. -func HandleEventsFromEventSource(name string, eventStream Eventing_StartEventSourceServer, dataCh chan []byte, errorCh chan error, doneCh chan struct{}, log *logrus.Logger) error { +func HandleEventsFromEventSource(name string, eventStream gateways.Eventing_StartEventSourceServer, dataCh chan []byte, errorCh chan error, doneCh chan struct{}, log *logrus.Logger) error { for { select { case data := <-dataCh: log.WithField(common.LabelEventSource, name).Info("new event received, dispatching to gateway client") - err := eventStream.Send(&Event{ + err := eventStream.Send(&gateways.Event{ Name: name, Payload: data, }) @@ -69,7 +69,7 @@ func HandleEventsFromEventSource(name string, eventStream Eventing_StartEventSou } case err := <-errorCh: - log.WithField(common.LabelEventSource, name).WithError(err).Error("error occurred while getting event from event source") + log.WithField(common.LabelEventSource, name).WithError(err).Error("error occurred processing the event source") return err case <-eventStream.Context().Done(): diff --git a/gateways/gateway_test.go b/gateways/server/server_test.go similarity index 93% rename from gateways/gateway_test.go rename to gateways/server/server_test.go index f27e59669f..508ea74e86 100644 --- a/gateways/gateway_test.go +++ b/gateways/server/server_test.go @@ -14,24 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package server import ( "context" "fmt" + "testing" + "time" + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" "github.com/smartystreets/goconvey/convey" "google.golang.org/grpc/metadata" - "testing" - "time" ) type FakeGRPCStream struct { - SentData *Event + SentData *gateways.Event Ctx context.Context } -func (f *FakeGRPCStream) Send(event *Event) error { +func (f *FakeGRPCStream) Send(event *gateways.Event) error { f.SentData = event return nil } diff --git a/gateways/community/slack/Dockerfile b/gateways/server/slack/Dockerfile similarity index 100% rename from gateways/community/slack/Dockerfile rename to gateways/server/slack/Dockerfile diff --git a/gateways/server/slack/cmd/main.go b/gateways/server/slack/cmd/main.go new file mode 100644 index 0000000000..07ba88efb4 --- /dev/null +++ b/gateways/server/slack/cmd/main.go @@ -0,0 +1,39 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/slack" + "k8s.io/client-go/kubernetes" +) + +func main() { + kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig) + restConfig, err := common.GetClientConfig(kubeConfig) + if err != nil { + panic(err) + } + clientset := kubernetes.NewForConfigOrDie(restConfig) + server.StartGateway(&slack.EventListener{ + Logger: common.NewArgoEventsLogger(), + K8sClient: clientset, + }) +} diff --git a/gateways/community/slack/start.go b/gateways/server/slack/start.go similarity index 54% rename from gateways/community/slack/start.go rename to gateways/server/slack/start.go index 2e17250464..43cbbfdb00 100644 --- a/gateways/community/slack/start.go +++ b/gateways/server/slack/start.go @@ -24,49 +24,61 @@ import ( "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/gateways" - gwcommon "github.com/argoproj/argo-events/gateways/common" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/argoproj/argo-events/store" + "github.com/ghodss/yaml" "github.com/nlopes/slack" "github.com/nlopes/slack/slackevents" "github.com/pkg/errors" ) +// controller controls the webhook operations var ( - helper = gwcommon.NewWebhookHelper() + controller = webhook.NewController() ) +// set up the activation and inactivation channels to control the state of routes. func init() { - go gwcommon.InitRouteChannels(helper) + go webhook.ProcessRouteStatus(controller) } -func (rc *RouteConfig) GetRoute() *gwcommon.Route { +// Implement Router +// 1. GetRoute +// 2. HandleRoute +// 3. PostActivate +// 4. PostDeactivate + +// GetRoute returns the route +func (rc *Router) GetRoute() *webhook.Route { return rc.route } -// RouteHandler handles new route -func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Request) { - r := rc.route +// HandleRoute handles incoming requests on the route +func (rc *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { + route := rc.route - log := r.Logger.WithFields( + logger := route.Logger.WithFields( map[string]interface{}{ - common.LabelEventSource: r.EventSource.Name, - common.LabelEndpoint: r.Webhook.Endpoint, - common.LabelPort: r.Webhook.Port, - common.LabelHTTPMethod: r.Webhook.Method, + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelHTTPMethod: route.Context.Method, }) - log.Info("request received") + logger.Info("request a received, processing it...") - if !helper.ActiveEndpoints[r.Webhook.Endpoint].Active { - log.Warn("endpoint is not active") - common.SendErrorResponse(writer, "") + if !route.Active { + logger.Warn("endpoint is not active, won't process it") + common.SendErrorResponse(writer, "endpoint is inactive") return } + logger.Infoln("verifying the request...") err := rc.verifyRequest(request) if err != nil { - log.WithError(err).Error("Failed validating request") - common.SendInternalErrorResponse(writer, "") + logger.WithError(err).Error("failed to validate the request") + common.SendInternalErrorResponse(writer, err.Error()) return } @@ -75,40 +87,54 @@ func (rc *RouteConfig) RouteHandler(writer http.ResponseWriter, request *http.Re // sent as application/x-www-form-urlencoded // If request was generated by an interactive element, it will be a POST form if len(request.Header["Content-Type"]) > 0 && request.Header["Content-Type"][0] == "application/x-www-form-urlencoded" { + logger.Infoln("handling slack interaction...") data, err = rc.handleInteraction(request) if err != nil { - log.WithError(err).Error("Failed processing interaction") - common.SendInternalErrorResponse(writer, "") + logger.WithError(err).Error("failed to process the interaction") + common.SendInternalErrorResponse(writer, err.Error()) return } } else { // If there's no payload in the post body, this is likely an // Event API request. Parse and process if valid. + logger.Infoln("handling slack event...") var response []byte data, response, err = rc.handleEvent(request) if err != nil { - log.WithError(err).Error("Failed processing event") - common.SendInternalErrorResponse(writer, "") + logger.WithError(err).Error("failed to handle the event") + common.SendInternalErrorResponse(writer, err.Error()) return } if response != nil { writer.Header().Set("Content-Type", "text") if _, err := writer.Write(response); err != nil { - log.WithError(err).Error("failed to write the response for url verification") + logger.WithError(err).Error("failed to write the response for url verification") // don't return, we want to keep this running to give user chance to retry } } } if data != nil { - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh <- data + logger.Infoln("dispatching event on route's data channel...") + route.DataCh <- data } - log.Info("request successfully processed") - common.SendSuccessResponse(writer, "") + logger.Info("request successfully processed") + common.SendSuccessResponse(writer, "success") } -func (rc *RouteConfig) handleEvent(request *http.Request) ([]byte, []byte, error) { +// PostActivate performs operations once the route is activated and ready to consume requests +func (rc *Router) PostActivate() error { + return nil +} + +// PostInactivate performs operations after the route is inactivated +func (rc *Router) PostInactivate() error { + return nil +} + +// handleEvent parse the slack notification and validates the event type +func (rc *Router) handleEvent(request *http.Request) ([]byte, []byte, error) { var err error var response []byte var data []byte @@ -141,7 +167,7 @@ func (rc *RouteConfig) handleEvent(request *http.Request) ([]byte, []byte, error return data, response, nil } -func (rc *RouteConfig) handleInteraction(request *http.Request) ([]byte, error) { +func (rc *Router) handleInteraction(request *http.Request) ([]byte, error) { var err error err = request.ParseForm() if err != nil { @@ -178,7 +204,7 @@ func getRequestBody(request *http.Request) ([]byte, error) { // X-Slack-Signature header value. // The signature is a hash generated as per Slack documentation at: // https://api.slack.com/docs/verifying-requests-from-slack -func (rc *RouteConfig) verifyRequest(request *http.Request) error { +func (rc *Router) verifyRequest(request *http.Request) error { signingSecret := rc.signingSecret if len(signingSecret) > 0 { sv, err := slack.NewSecretsVerifier(request.Header, signingSecret) @@ -205,52 +231,43 @@ func (rc *RouteConfig) verifyRequest(request *http.Request) error { return nil } -func (rc *RouteConfig) PostStart() error { - return nil -} +// StartEventSource starts a event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) -func (rc *RouteConfig) PostStop() error { - return nil -} + logger := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) -// StartEventSource starts a event source -func (ese *SlackEventSourceExecutor) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { - defer gateways.Recover(eventSource.Name) + logger.Infoln("started processing the event source...") - log := ese.Log.WithField(common.LabelEventSource, eventSource.Name) - log.Info("operating on event source") + logger.Infoln("parsing slack event source...") - config, err := parseEventSource(eventSource.Data) - if err != nil { - log.WithError(err).Error("failed to parse event source") + var slackEventSource *v1alpha1.SlackEventSource + if err := yaml.Unmarshal(eventSource.Value, &slackEventSource); err != nil { + logger.WithError(err).Errorln("failed to parse the event source") return err } - ses := config.(*slackEventSource) - - token, err := store.GetSecrets(ese.Clientset, ese.Namespace, ses.Token.Name, ses.Token.Key) + logger.Infoln("retrieving the slack token...") + token, err := store.GetSecrets(listener.K8sClient, slackEventSource.Namespace, slackEventSource.Token.Name, slackEventSource.Token.Key) if err != nil { - log.WithError(err).Error("failed to retrieve token") + logger.WithError(err).Error("failed to retrieve the token") return err } - signingSecret, err := store.GetSecrets(ese.Clientset, ese.Namespace, ses.SigningSecret.Name, ses.SigningSecret.Key) + logger.Infoln("retrieving the signing secret...") + signingSecret, err := store.GetSecrets(listener.K8sClient, slackEventSource.Namespace, slackEventSource.SigningSecret.Name, slackEventSource.SigningSecret.Key) if err != nil { - log.WithError(err).Warn("Signing secret not provided. Signature not validated.") - signingSecret = "" + logger.WithError(err).Warn("failed to retrieve the signing secret") + return err } - return gwcommon.ProcessRoute(&RouteConfig{ - route: &gwcommon.Route{ - Logger: ese.Log, - StartCh: make(chan struct{}), - Webhook: ses.Hook, - EventSource: eventSource, - }, - token: token, - signingSecret: signingSecret, - clientset: ese.Clientset, - namespace: ese.Namespace, - ses: ses, - }, helper, eventStream) + route := webhook.NewRoute(slackEventSource.Webhook, listener.Logger, eventSource) + + return webhook.ManageRoute(&Router{ + route: route, + token: token, + signingSecret: signingSecret, + k8sClient: listener.K8sClient, + slackEventSource: slackEventSource, + }, controller, eventStream) } diff --git a/gateways/community/slack/start_test.go b/gateways/server/slack/start_test.go similarity index 75% rename from gateways/community/slack/start_test.go rename to gateways/server/slack/start_test.go index 285f10e9cd..cc555760cb 100644 --- a/gateways/community/slack/start_test.go +++ b/gateways/server/slack/start_test.go @@ -29,7 +29,8 @@ import ( "testing" "time" - gwcommon "github.com/argoproj/argo-events/gateways/common" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" "github.com/ghodss/yaml" "github.com/nlopes/slack/slackevents" "github.com/smartystreets/goconvey/convey" @@ -38,27 +39,25 @@ import ( func TestRouteActiveHandler(t *testing.T) { convey.Convey("Given a route configuration", t, func() { - rc := &RouteConfig{ - route: gwcommon.GetFakeRoute(), - clientset: fake.NewSimpleClientset(), - namespace: "fake", - } - - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), + router := &Router{ + route: webhook.GetFakeRoute(), + k8sClient: fake.NewSimpleClientset(), + slackEventSource: &v1alpha1.SlackEventSource{ + Namespace: "fake", + }, } convey.Convey("Inactive route should return 404", func() { - writer := &gwcommon.FakeHttpWriter{} - rc.RouteHandler(writer, &http.Request{}) + writer := &webhook.FakeHttpWriter{} + router.HandleRoute(writer, &http.Request{}) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) }) - rc.token = "Jhj5dZrVaK7ZwHHjRyZWjbDl" - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].Active = true + router.token = "Jhj5dZrVaK7ZwHHjRyZWjbDl" + router.route.Active = true convey.Convey("Test url verification request", func() { - writer := &gwcommon.FakeHttpWriter{} + writer := &webhook.FakeHttpWriter{} urlVer := slackevents.EventsAPIURLVerificationEvent{ Type: slackevents.URLVerification, Token: "Jhj5dZrVaK7ZwHHjRyZWjbDl", @@ -67,7 +66,7 @@ func TestRouteActiveHandler(t *testing.T) { payload, err := yaml.Marshal(urlVer) convey.So(err, convey.ShouldBeNil) convey.So(payload, convey.ShouldNotBeNil) - rc.RouteHandler(writer, &http.Request{ + router.HandleRoute(writer, &http.Request{ Body: ioutil.NopCloser(bytes.NewReader(payload)), }) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusInternalServerError) @@ -78,20 +77,22 @@ func TestRouteActiveHandler(t *testing.T) { func TestSlackSignature(t *testing.T) { convey.Convey("Given a route that receives a message from Slack", t, func() { - rc := &RouteConfig{ - route: gwcommon.GetFakeRoute(), - clientset: fake.NewSimpleClientset(), - namespace: "fake", + router := &Router{ + route: webhook.GetFakeRoute(), + k8sClient: fake.NewSimpleClientset(), + slackEventSource: &v1alpha1.SlackEventSource{ + Namespace: "fake", + }, } - rc.signingSecret = "abcdefghiklm1234567890" + router.signingSecret = "abcdefghiklm1234567890" convey.Convey("Validate request signature", func() { - writer := &gwcommon.FakeHttpWriter{} + writer := &webhook.FakeHttpWriter{} payload := []byte("payload=%7B%22type%22%3A%22block_actions%22%2C%22team%22%3A%7B%22id%22%3A%22T0CAG%22%2C%22domain%22%3A%22acme-creamery%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U0CA5%22%2C%22username%22%3A%22Amy%20McGee%22%2C%22name%22%3A%22Amy%20McGee%22%2C%22team_id%22%3A%22T3MDE%22%7D%2C%22api_app_id%22%3A%22A0CA5%22%2C%22token%22%3A%22Shh_its_a_seekrit%22%2C%22container%22%3A%7B%22type%22%3A%22message%22%2C%22text%22%3A%22The%20contents%20of%20the%20original%20message%20where%20the%20action%20originated%22%7D%2C%22trigger_id%22%3A%2212466734323.1395872398%22%2C%22response_url%22%3A%22https%3A%2F%2Fwww.postresponsestome.com%2FT123567%2F1509734234%22%2C%22actions%22%3A%5B%7B%22type%22%3A%22button%22%2C%22block_id%22%3A%22actionblock789%22%2C%22action_id%22%3A%2227S%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Link%20Button%22%2C%22emoji%22%3Atrue%7D%2C%22action_ts%22%3A%221564701248.149432%22%7D%5D%7D") h := make(http.Header) rts := int(time.Now().UTC().UnixNano()) - hmac := hmac.New(sha256.New, []byte(rc.signingSecret)) + hmac := hmac.New(sha256.New, []byte(router.signingSecret)) b := strings.Join([]string{"v0", strconv.Itoa(rts), string(payload)}, ":") hmac.Write([]byte(b)) hash := hex.EncodeToString(hmac.Sum(nil)) @@ -100,16 +101,13 @@ func TestSlackSignature(t *testing.T) { h.Add("X-Slack-Signature", genSig) h.Add("X-Slack-Request-Timestamp", strconv.FormatInt(int64(rts), 10)) - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].Active = true + router.route.Active = true go func() { - <-helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh + <-router.route.DataCh }() - rc.RouteHandler(writer, &http.Request{ + router.HandleRoute(writer, &http.Request{ Body: ioutil.NopCloser(bytes.NewReader(payload)), Header: h, Method: "POST", @@ -122,25 +120,23 @@ func TestSlackSignature(t *testing.T) { func TestInteractionHandler(t *testing.T) { convey.Convey("Given a route that receives an interaction event", t, func() { - rc := &RouteConfig{ - route: gwcommon.GetFakeRoute(), - clientset: fake.NewSimpleClientset(), - namespace: "fake", + router := &Router{ + route: webhook.GetFakeRoute(), + k8sClient: fake.NewSimpleClientset(), + slackEventSource: &v1alpha1.SlackEventSource{ + Namespace: "fake", + }, } convey.Convey("Test an interaction action message", func() { - writer := &gwcommon.FakeHttpWriter{} + writer := &webhook.FakeHttpWriter{} actionString := `{"type":"block_actions","team":{"id":"T9TK3CUKW","domain":"example"},"user":{"id":"UA8RXUSPL","username":"jtorrance","team_id":"T9TK3CUKW"},"api_app_id":"AABA1ABCD","token":"9s8d9as89d8as9d8as989","container":{"type":"message_attachment","message_ts":"1548261231.000200","attachment_id":1,"channel_id":"CBR2V3XEX","is_ephemeral":false,"is_app_unfurl":false},"trigger_id":"12321423423.333649436676.d8c1bb837935619ccad0f624c448ffb3","channel":{"id":"CBR2V3XEX","name":"review-updates"},"message":{"bot_id":"BAH5CA16Z","type":"message","text":"This content can't be displayed.","user":"UAJ2RU415","ts":"1548261231.000200"},"response_url":"https://hooks.slack.com/actions/AABA1ABCD/1232321423432/D09sSasdasdAS9091209","actions":[{"action_id":"WaXA","block_id":"=qXel","text":{"type":"plain_text","text":"View","emoji":true},"value":"click_me_123","type":"button","action_ts":"1548426417.840180"}]}` payload := []byte(`payload=` + actionString) out := make(chan []byte) - - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].Active = true + router.route.Active = true go func() { - out <- <-helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh + out <- <-router.route.DataCh }() var buf bytes.Buffer @@ -148,7 +144,7 @@ func TestInteractionHandler(t *testing.T) { headers := make(map[string][]string) headers["Content-Type"] = append(headers["Content-Type"], "application/x-www-form-urlencoded") - rc.RouteHandler(writer, &http.Request{ + router.HandleRoute(writer, &http.Request{ Method: http.MethodPost, Header: headers, Body: ioutil.NopCloser(strings.NewReader(buf.String())), @@ -164,14 +160,16 @@ func TestInteractionHandler(t *testing.T) { func TestEventHandler(t *testing.T) { convey.Convey("Given a route that receives an event", t, func() { - rc := &RouteConfig{ - route: gwcommon.GetFakeRoute(), - clientset: fake.NewSimpleClientset(), - namespace: "fake", + router := &Router{ + route: webhook.GetFakeRoute(), + k8sClient: fake.NewSimpleClientset(), + slackEventSource: &v1alpha1.SlackEventSource{ + Namespace: "fake", + }, } convey.Convey("Test an event notification", func() { - writer := &gwcommon.FakeHttpWriter{} + writer := &webhook.FakeHttpWriter{} event := []byte(` { "type": "name_of_event", @@ -197,16 +195,13 @@ func TestEventHandler(t *testing.T) { payload, err := yaml.Marshal(ce) convey.So(err, convey.ShouldBeNil) - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), - } - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].Active = true + router.route.Active = true go func() { - <-helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh + <-router.route.DataCh }() - rc.RouteHandler(writer, &http.Request{ + router.HandleRoute(writer, &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(payload)), }) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusInternalServerError) diff --git a/gateways/server/slack/types.go b/gateways/server/slack/types.go new file mode 100644 index 0000000000..f171147682 --- /dev/null +++ b/gateways/server/slack/types.go @@ -0,0 +1,46 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package slack + +import ( + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" +) + +// EventListener implements Eventing for slack event source +type EventListener struct { + // K8sClient is kubernetes client + K8sClient kubernetes.Interface + // Logger logs stuff + Logger *logrus.Logger +} + +// Router contains information about a REST endpoint +type Router struct { + // route holds information to process an incoming request + route *webhook.Route + // slackEventSource is the event source which refers to configuration required to consume events from slack + slackEventSource *v1alpha1.SlackEventSource + // token is the slack token + token string + // refer to https://api.slack.com/docs/verifying-requests-from-slack + signingSecret string + // k8sClient is the Kubernetes client + k8sClient kubernetes.Interface +} diff --git a/gateways/server/slack/validate.go b/gateways/server/slack/validate.go new file mode 100644 index 0000000000..2a4fb9b01c --- /dev/null +++ b/gateways/server/slack/validate.go @@ -0,0 +1,70 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package slack + +import ( + "context" + "fmt" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates slack event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.SlackEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.SlackEvent)), + }, nil + } + + var slackEventSource *v1alpha1.SlackEventSource + if err := yaml.Unmarshal(eventSource.Value, &slackEventSource); err != nil { + listener.Logger.WithError(err).Errorln("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(slackEventSource); err != nil { + listener.Logger.WithError(err).Errorln("failed to validate the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.SlackEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + if eventSource.Token == nil { + return fmt.Errorf("token not provided") + } + return webhook.ValidateWebhookContext(eventSource.Webhook) +} diff --git a/gateways/server/slack/validate_test.go b/gateways/server/slack/validate_test.go new file mode 100644 index 0000000000..6337b98fd5 --- /dev/null +++ b/gateways/server/slack/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package slack + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +func TestSlackEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "slack", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("slack"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "slack.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Slack { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "slack", + Value: content, + Type: "slack", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/community/storagegrid/Dockerfile b/gateways/server/storagegrid/Dockerfile similarity index 100% rename from gateways/community/storagegrid/Dockerfile rename to gateways/server/storagegrid/Dockerfile diff --git a/gateways/core/stream/kafka/cmd/main.go b/gateways/server/storagegrid/cmd/main.go similarity index 76% rename from gateways/core/stream/kafka/cmd/main.go rename to gateways/server/storagegrid/cmd/main.go index 865ae21208..59cf7c2a67 100644 --- a/gateways/core/stream/kafka/cmd/main.go +++ b/gateways/server/storagegrid/cmd/main.go @@ -18,12 +18,12 @@ package main import ( "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/core/stream/kafka" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/storagegrid" ) func main() { - gateways.StartGateway(&kafka.KafkaEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), + server.StartGateway(&storagegrid.EventListener{ + Logger: common.NewArgoEventsLogger(), }) } diff --git a/gateways/server/storagegrid/start.go b/gateways/server/storagegrid/start.go new file mode 100644 index 0000000000..fccc71f642 --- /dev/null +++ b/gateways/server/storagegrid/start.go @@ -0,0 +1,199 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storagegrid + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/google/uuid" + "github.com/joncalhoun/qson" +) + +// controller controls the webhook operations +var ( + controller = webhook.NewController() +) + +var ( + respBody = ` + + + ` + generateUUID().String() + ` + + + ` + generateUUID().String() + ` + +` + "\n" +) + +// set up the activation and inactivation channels to control the state of routes. +func init() { + go webhook.ProcessRouteStatus(controller) +} + +// generateUUID returns a new uuid +func generateUUID() uuid.UUID { + return uuid.New() +} + +// filterEvent filters notification based on event filter in a gateway configuration +func filterEvent(notification *storageGridNotification, eventSource *v1alpha1.StorageGridEventSource) bool { + if eventSource.Events == nil { + return true + } + for _, filterEvent := range eventSource.Events { + if notification.Message.Records[0].EventName == filterEvent { + return true + } + } + return false +} + +// filterName filters object key based on configured prefix and/or suffix +func filterName(notification *storageGridNotification, eventSource *v1alpha1.StorageGridEventSource) bool { + if eventSource.Filter == nil { + return true + } + if eventSource.Filter.Prefix != "" && eventSource.Filter.Suffix != "" { + return strings.HasPrefix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Prefix) && strings.HasSuffix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Suffix) + } + if eventSource.Filter.Prefix != "" { + return strings.HasPrefix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Prefix) + } + if eventSource.Filter.Suffix != "" { + return strings.HasSuffix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Suffix) + } + return true +} + +// Implement Router +// 1. GetRoute +// 2. HandleRoute +// 3. PostActivate +// 4. PostDeactivate + +// GetRoute returns the route +func (router *Router) GetRoute() *webhook.Route { + return router.route +} + +// HandleRoute handles new route +func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { + route := router.route + + logger := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelPort: route.Context.Port, + common.LabelHTTPMethod: route.Context.Method, + }) + + logger.Infoln("processing incoming request...") + + if !route.Active { + logger.Warnln("endpoint is inactive, won't process the request") + common.SendErrorResponse(writer, "inactive endpoint") + return + } + + logger.Infoln("parsing the request body...") + body, err := ioutil.ReadAll(request.Body) + if err != nil { + logger.WithError(err).Errorln("failed to parse request body") + common.SendErrorResponse(writer, "") + return + } + + switch request.Method { + case http.MethodHead: + respBody = "" + } + writer.WriteHeader(http.StatusOK) + writer.Header().Add("Content-Type", "text/plain") + writer.Write([]byte(respBody)) + + // notification received from storage grid is url encoded. + parsedURL, err := url.QueryUnescape(string(body)) + if err != nil { + logger.WithError(err).Errorln("failed to unescape request body url") + return + } + b, err := qson.ToJSON(parsedURL) + if err != nil { + logger.WithError(err).Errorln("failed to convert request body in JSON format") + return + } + + logger.Infoln("converting request body to storage grid notification") + var notification *storageGridNotification + err = json.Unmarshal(b, ¬ification) + if err != nil { + logger.WithError(err).Errorln("failed to convert the request body into storage grid notification") + return + } + + if filterEvent(notification, router.storageGridEventSource) && filterName(notification, router.storageGridEventSource) { + logger.WithError(err).Errorln("new event received, dispatching event on route's data channel") + route.DataCh <- b + return + } + + logger.Warnln("discarding notification since it did not pass all filters") +} + +// PostActivate performs operations once the route is activated and ready to consume requests +func (router *Router) PostActivate() error { + return nil +} + +// PostInactivate performs operations after the route is inactivated +func (router *Router) PostInactivate() error { + return nil +} + +// StartConfig runs a configuration +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + log := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + log.Info("started processing the event source...") + + var storagegridEventSource *v1alpha1.StorageGridEventSource + if err := yaml.Unmarshal(eventSource.Value, &storagegridEventSource); err != nil { + log.WithError(err).Errorln("failed to parse the event source") + return err + } + + route := webhook.NewRoute(storagegridEventSource.Webhook, listener.Logger, eventSource) + + return webhook.ManageRoute(&Router{ + route: route, + storageGridEventSource: storagegridEventSource, + }, controller, eventStream) +} diff --git a/gateways/community/storagegrid/start_test.go b/gateways/server/storagegrid/start_test.go similarity index 61% rename from gateways/community/storagegrid/start_test.go rename to gateways/server/storagegrid/start_test.go index e0d7e040e5..bc0031d86d 100644 --- a/gateways/community/storagegrid/start_test.go +++ b/gateways/server/storagegrid/start_test.go @@ -19,12 +19,14 @@ package storagegrid import ( "bytes" "encoding/json" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" "io/ioutil" "net/http" "testing" + + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/smartystreets/goconvey/convey" ) var ( @@ -35,7 +37,7 @@ var ( "Records": [ { "eventName": "ObjectCreated:Put", - "eventSource": "sgws:s3", + "storageGridEventSource": "sgws:s3", "eventTime": "2019-02-27T21:15:09Z", "eventVersion": "2.0", "requestParameters": { @@ -71,40 +73,49 @@ var ( "Version": "2010-03-31" } ` - rc = &RouteConfig{ - route: gwcommon.GetFakeRoute(), + router = &Router{ + route: webhook.GetFakeRoute(), } ) func TestRouteActiveHandler(t *testing.T) { convey.Convey("Given a route configuration", t, func() { - helper.ActiveEndpoints[rc.route.Webhook.Endpoint] = &gwcommon.Endpoint{ - DataCh: make(chan []byte), + storageGridEventSource := &v1alpha1.StorageGridEventSource{ + Webhook: &webhook.Context{ + Endpoint: "/", + URL: "testurl", + Port: "8080", + }, + Events: []string{ + "ObjectCreated:Put", + }, + Filter: &v1alpha1.StorageGridFilter{ + Prefix: "hello-", + Suffix: ".txt", + }, } - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - writer := &gwcommon.FakeHttpWriter{} + writer := &webhook.FakeHttpWriter{} convey.Convey("Inactive route should return error", func() { - pbytes, err := yaml.Marshal(ps.(*storageGridEventSource)) + pbytes, err := yaml.Marshal(storageGridEventSource) convey.So(err, convey.ShouldBeNil) - rc.RouteHandler(writer, &http.Request{ + router.HandleRoute(writer, &http.Request{ Body: ioutil.NopCloser(bytes.NewReader(pbytes)), }) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusBadRequest) }) convey.Convey("Active route should return success", func() { - helper.ActiveEndpoints[rc.route.Webhook.Endpoint].Active = true - rc.sges = ps.(*storageGridEventSource) + router.route.Active = true + router.storageGridEventSource = storageGridEventSource dataCh := make(chan []byte) go func() { - resp := <-helper.ActiveEndpoints[rc.route.Webhook.Endpoint].DataCh + resp := <-router.route.DataCh dataCh <- resp }() - rc.RouteHandler(writer, &http.Request{ + router.HandleRoute(writer, &http.Request{ Body: ioutil.NopCloser(bytes.NewReader([]byte(notification))), }) convey.So(writer.HeaderStatus, convey.ShouldEqual, http.StatusOK) @@ -122,28 +133,52 @@ func TestGenerateUUID(t *testing.T) { func TestFilterEvent(t *testing.T) { convey.Convey("Given a storage grid event, test whether it passes the filter", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - var sg *storageGridNotification - err = json.Unmarshal([]byte(notification), &sg) + storageGridEventSource := &v1alpha1.StorageGridEventSource{ + Webhook: &webhook.Context{ + Endpoint: "/", + URL: "testurl", + Port: "8080", + }, + Events: []string{ + "ObjectCreated:Put", + }, + Filter: &v1alpha1.StorageGridFilter{ + Prefix: "hello-", + Suffix: ".txt", + }, + } + var gridNotification *storageGridNotification + err := json.Unmarshal([]byte(notification), &gridNotification) convey.So(err, convey.ShouldBeNil) - convey.So(sg, convey.ShouldNotBeNil) + convey.So(gridNotification, convey.ShouldNotBeNil) - ok := filterEvent(sg, ps.(*storageGridEventSource)) + ok := filterEvent(gridNotification, storageGridEventSource) convey.So(ok, convey.ShouldEqual, true) }) } func TestFilterName(t *testing.T) { convey.Convey("Given a storage grid event, test whether the object key passes the filter", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - var sg *storageGridNotification - err = json.Unmarshal([]byte(notification), &sg) + storageGridEventSource := &v1alpha1.StorageGridEventSource{ + Webhook: &webhook.Context{ + Endpoint: "/", + URL: "testurl", + Port: "8080", + }, + Events: []string{ + "ObjectCreated:Put", + }, + Filter: &v1alpha1.StorageGridFilter{ + Prefix: "hello-", + Suffix: ".txt", + }, + } + var gridNotification *storageGridNotification + err := json.Unmarshal([]byte(notification), &gridNotification) convey.So(err, convey.ShouldBeNil) - convey.So(sg, convey.ShouldNotBeNil) + convey.So(gridNotification, convey.ShouldNotBeNil) - ok := filterName(sg, ps.(*storageGridEventSource)) + ok := filterName(gridNotification, storageGridEventSource) convey.So(ok, convey.ShouldEqual, true) }) } diff --git a/gateways/community/storagegrid/config.go b/gateways/server/storagegrid/types.go similarity index 57% rename from gateways/community/storagegrid/config.go rename to gateways/server/storagegrid/types.go index e856b29402..e7cd5a6eb2 100644 --- a/gateways/community/storagegrid/config.go +++ b/gateways/server/storagegrid/types.go @@ -17,49 +17,25 @@ limitations under the License. package storagegrid import ( - "github.com/sirupsen/logrus" - "net/http" + "github.com/argoproj/argo-events/gateways/server/common/webhook" "time" - gwcommon "github.com/argoproj/argo-events/gateways/common" - "github.com/ghodss/yaml" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/sirupsen/logrus" ) -const ArgoEventsEventSourceVersion = "v0.11" - -// StorageGridEventSourceExecutor implements Eventing -type StorageGridEventSourceExecutor struct { - Log *logrus.Logger -} - -type RouteConfig struct { - route *gwcommon.Route - sges *storageGridEventSource -} - -// storageGridEventSource contains configuration for storage grid sns -type storageGridEventSource struct { - // Webhook - Hook *gwcommon.Webhook `json:"hook"` - - // Events are s3 bucket notification events. - // For more information on s3 notifications, follow https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-event-types-and-destinations - // Note that storage grid notifications do not contain `s3:` - Events []string `json:"events,omitempty"` - - // Filter on object key which caused the notification. - Filter *Filter `json:"filter,omitempty"` - - // srv holds reference to http server - srv *http.Server - mux *http.ServeMux +// EventListener implements Eventing for storage grid events +type EventListener struct { + // Logger logs stuff + Logger *logrus.Logger } -// Filter represents filters to apply to bucket notifications for specifying constraints on objects -// +k8s:openapi-gen=true -type Filter struct { - Prefix string `json:"prefix"` - Suffix string `json:"suffix"` +// Router manages route +type Router struct { + // route contains configuration of a REST endpoint + route *webhook.Route + // storageGridEventSource refers to event source which contains configuration to consume events from storage grid + storageGridEventSource *v1alpha1.StorageGridEventSource } // storageGridNotification is the bucket notification received from storage grid @@ -68,7 +44,7 @@ type storageGridNotification struct { Message struct { Records []struct { EventVersion string `json:"eventVersion"` - EventSource string `json:"eventSource"` + EventSource string `json:"storageGridEventSource"` EventTime time.Time `json:"eventTime"` EventName string `json:"eventName"` UserIdentity struct { @@ -102,12 +78,3 @@ type storageGridNotification struct { TopicArn string `json:"TopicArn"` Version string `json:"Version"` } - -func parseEventSource(eventSource string) (interface{}, error) { - var s *storageGridEventSource - err := yaml.Unmarshal([]byte(eventSource), &s) - if err != nil { - return nil, err - } - return s, err -} diff --git a/gateways/server/storagegrid/validate.go b/gateways/server/storagegrid/validate.go new file mode 100644 index 0000000000..a1120f3500 --- /dev/null +++ b/gateways/server/storagegrid/validate.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storagegrid + +import ( + "context" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates storage grid event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.StorageGridEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.StorageGridEvent)), + }, nil + } + + var storageGridEventSource *v1alpha1.StorageGridEventSource + if err := yaml.Unmarshal(eventSource.Value, &storageGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(storageGridEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate storage grid event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(eventSource *v1alpha1.StorageGridEventSource) error { + if eventSource == nil { + return common.ErrNilEventSource + } + return webhook.ValidateWebhookContext(eventSource.Webhook) +} diff --git a/gateways/server/storagegrid/validate_test.go b/gateways/server/storagegrid/validate_test.go new file mode 100644 index 0000000000..6de5e82f6c --- /dev/null +++ b/gateways/server/storagegrid/validate_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storagegrid + +import ( + "context" + "fmt" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestEventListener_ValidateEventSource(t *testing.T) { + listener := &EventListener{ + Logger: common.NewArgoEventsLogger(), + } + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "storagegrid", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("storagegrid"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "storage-grid.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.StorageGrid { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "storagegrid", + Value: content, + Type: "storagegrid", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/utils.go b/gateways/server/utils.go similarity index 98% rename from gateways/utils.go rename to gateways/server/utils.go index 2e70adea15..656482ab9a 100644 --- a/gateways/utils.go +++ b/gateways/server/utils.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package server import ( "github.com/argoproj/argo-events/common" diff --git a/gateways/utils_test.go b/gateways/server/utils_test.go similarity index 98% rename from gateways/utils_test.go rename to gateways/server/utils_test.go index 926f26e162..2f59845c12 100644 --- a/gateways/utils_test.go +++ b/gateways/server/utils_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package gateways +package server import ( "github.com/smartystreets/goconvey/convey" diff --git a/gateways/core/webhook/Dockerfile b/gateways/server/webhook/Dockerfile similarity index 100% rename from gateways/core/webhook/Dockerfile rename to gateways/server/webhook/Dockerfile diff --git a/gateways/server/webhook/cmd/main.go b/gateways/server/webhook/cmd/main.go new file mode 100644 index 0000000000..c949687750 --- /dev/null +++ b/gateways/server/webhook/cmd/main.go @@ -0,0 +1,29 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/webhook" +) + +func main() { + server.StartGateway(&webhook.EventListener{ + Logger: common.NewArgoEventsLogger(), + }) +} diff --git a/gateways/server/webhook/start.go b/gateways/server/webhook/start.go new file mode 100644 index 0000000000..77d2e8e4f3 --- /dev/null +++ b/gateways/server/webhook/start.go @@ -0,0 +1,145 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + "github.com/ghodss/yaml" + "github.com/sirupsen/logrus" +) + +// EventListener implements Eventing for webhook events +type EventListener struct { + // Logger logs stuff + Logger *logrus.Logger +} + +// Router contains the configuration information for a route +type Router struct { + // route contains information about a API endpoint + route *webhook.Route +} + +// controller controls the webhook operations +var ( + controller = webhook.NewController() +) + +// set up the activation and inactivation channels to control the state of routes. +func init() { + go webhook.ProcessRouteStatus(controller) +} + +// webhook event payload +type payload struct { + // Header is the http request header + Header http.Header `json:"header"` + // Body is http request body + Body []byte `json:"body"` +} + +// Implement Router +// 1. GetRoute +// 2. HandleRoute +// 3. PostActivate +// 4. PostDeactivate + +// GetRoute returns the route +func (router *Router) GetRoute() *webhook.Route { + return router.route +} + +// HandleRoute handles incoming requests on the route +func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { + route := router.route + + logger := route.Logger.WithFields( + map[string]interface{}{ + common.LabelEventSource: route.EventSource.Name, + common.LabelEndpoint: route.Context.Endpoint, + common.LabelPort: route.Context.Port, + common.LabelHTTPMethod: route.Context.Method, + }) + + logger.Info("a request received, processing it...") + + if !route.Active { + logger.Info("endpoint is not active, wont't process the request") + common.SendErrorResponse(writer, "endpoint is inactive") + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + logger.WithError(err).Error("failed to parse request body") + common.SendErrorResponse(writer, err.Error()) + return + } + + data, err := json.Marshal(&payload{ + Header: request.Header, + Body: body, + }) + if err != nil { + logger.WithError(err).Error("failed to construct the event payload") + common.SendErrorResponse(writer, err.Error()) + return + } + + logger.Infoln("dispatching event on route's data channel...") + route.DataCh <- data + logger.Info("successfully processed the request") + common.SendSuccessResponse(writer, "success") +} + +// PostActivate performs operations once the route is activated and ready to consume requests +func (router *Router) PostActivate() error { + return nil +} + +// PostInactivate performs operations after the route is inactivated +func (router *Router) PostInactivate() error { + return nil +} + +// StartEventSource starts a event source +func (listener *EventListener) StartEventSource(eventSource *gateways.EventSource, eventStream gateways.Eventing_StartEventSourceServer) error { + defer server.Recover(eventSource.Name) + + log := listener.Logger.WithField(common.LabelEventSource, eventSource.Name) + + log.Info("started operating on the event source...") + + var webhookEventSource *webhook.Context + if err := yaml.Unmarshal(eventSource.Value, &webhookEventSource); err != nil { + log.WithError(err).Error("failed to parse the event source") + return err + } + + route := webhook.NewRoute(webhookEventSource, listener.Logger, eventSource) + + return webhook.ManageRoute(&Router{ + route: route, + }, controller, eventStream) +} diff --git a/gateways/server/webhook/validate.go b/gateways/server/webhook/validate.go new file mode 100644 index 0000000000..51d946def6 --- /dev/null +++ b/gateways/server/webhook/validate.go @@ -0,0 +1,74 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "context" + "fmt" + "net/http" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + "github.com/ghodss/yaml" +) + +// ValidateEventSource validates webhook event source +func (listener *EventListener) ValidateEventSource(ctx context.Context, eventSource *gateways.EventSource) (*gateways.ValidEventSource, error) { + if apicommon.EventSourceType(eventSource.Type) != apicommon.WebhookEvent { + return &gateways.ValidEventSource{ + IsValid: false, + Reason: common.ErrEventSourceTypeMismatch(string(apicommon.WebhookEvent)), + }, nil + } + + var webhookEventSource *webhook.Context + if err := yaml.Unmarshal(eventSource.Value, &webhookEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to parse the event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + if err := validate(webhookEventSource); err != nil { + listener.Logger.WithError(err).Error("failed to validate the webhook event source") + return &gateways.ValidEventSource{ + IsValid: false, + Reason: err.Error(), + }, nil + } + + return &gateways.ValidEventSource{ + IsValid: true, + }, nil +} + +func validate(webhookEventSource *webhook.Context) error { + if webhookEventSource == nil { + return common.ErrNilEventSource + } + + switch webhookEventSource.Method { + case http.MethodHead, http.MethodPut, http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodOptions, http.MethodPatch, http.MethodPost, http.MethodTrace: + default: + return fmt.Errorf("unknown HTTP method %s", webhookEventSource.Method) + } + + return webhook.ValidateWebhookContext(webhookEventSource) +} diff --git a/gateways/server/webhook/validate_test.go b/gateways/server/webhook/validate_test.go new file mode 100644 index 0000000000..2e4d1f7512 --- /dev/null +++ b/gateways/server/webhook/validate_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways" + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/ghodss/yaml" +) + +func TestValidateEventSource(t *testing.T) { + listener := &EventListener{} + + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "webhook", + Value: nil, + Type: "sq", + }) + assert.Equal(t, false, valid.IsValid) + assert.Equal(t, common.ErrEventSourceTypeMismatch("webhook"), valid.Reason) + + content, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", gateways.EventSourceDir, "webhook.yaml")) + assert.Nil(t, err) + + var eventSource *v1alpha1.EventSource + err = yaml.Unmarshal(content, &eventSource) + assert.Nil(t, err) + + for name, value := range eventSource.Spec.Webhook { + fmt.Println(name) + content, err := yaml.Marshal(value) + assert.Nil(t, err) + valid, _ := listener.ValidateEventSource(context.Background(), &gateways.EventSource{ + Id: "1", + Name: "webhook", + Value: content, + Type: "webhook", + }) + fmt.Println(valid.Reason) + assert.Equal(t, true, valid.IsValid) + } +} diff --git a/gateways/transformer.go b/gateways/transformer.go deleted file mode 100644 index 55433938bc..0000000000 --- a/gateways/transformer.go +++ /dev/null @@ -1,175 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateways - -import ( - "bytes" - "encoding/json" - "fmt" - "net" - "net/http" - "time" - - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - - "github.com/argoproj/argo-events/common" - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - pc "github.com/argoproj/argo-events/pkg/apis/common" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/google/uuid" -) - -// TransformerPayload contains payload of cloudevents. -type TransformerPayload struct { - // Src contains information about which specific configuration in gateway generated the event - Src string `json:"src"` - // Payload is event data - Payload []byte `json:"payload"` -} - -// DispatchEvent dispatches event to gateway transformer for further processing -func (gc *GatewayConfig) DispatchEvent(gatewayEvent *Event) error { - transformedEvent, err := gc.transformEvent(gatewayEvent) - if err != nil { - return err - } - - payload, err := json.Marshal(transformedEvent) - if err != nil { - return fmt.Errorf("failed to dispatch event to watchers over http. marshalling failed. err: %+v", err) - } - - switch gc.gw.Spec.EventProtocol.Type { - case pc.HTTP: - if err = gc.dispatchEventOverHttp(transformedEvent.Context.Source.Host, payload); err != nil { - return err - } - case pc.NATS: - if err = gc.dispatchEventOverNats(transformedEvent.Context.Source.Host, payload); err != nil { - return err - } - default: - return fmt.Errorf("unknown dispatch mechanism %s", gc.gw.Spec.EventProtocol.Type) - } - return nil -} - -// transformEvent transforms an event from event source into a CloudEvents specification compliant event -// See https://github.com/cloudevents/spec for more info. -func (gc *GatewayConfig) transformEvent(gatewayEvent *Event) (*apicommon.Event, error) { - // Generate an event id - eventId := uuid.New() - - gc.Log.WithField(common.LabelEventSource, gatewayEvent.Name).Info("converting gateway event into cloudevents specification compliant event") - - // Create an CloudEvent - ce := &apicommon.Event{ - Context: apicommon.EventContext{ - CloudEventsVersion: common.CloudEventsVersion, - EventID: eventId.String(), - ContentType: "application/json", - EventTime: metav1.MicroTime{Time: time.Now().UTC()}, - EventType: gc.gw.Spec.Type, - EventTypeVersion: v1alpha1.ArgoEventsGatewayVersion, - Source: &apicommon.URI{ - Host: common.DefaultEventSourceName(gc.gw.Name, gatewayEvent.Name), - }, - }, - Payload: gatewayEvent.Payload, - } - - gc.Log.WithField(common.LabelGatewayName, gatewayEvent.Name).Info("event has been transformed into cloud event") - return ce, nil -} - -// dispatchEventOverHttp dispatches event to watchers over http. -func (gc *GatewayConfig) dispatchEventOverHttp(source string, eventPayload []byte) error { - gc.Log.WithField(common.LabelEventSource, source).Info("dispatching event to watchers") - - completeSuccess := true - - for _, sensor := range gc.gw.Spec.Watchers.Sensors { - namespace := gc.Namespace - if sensor.Namespace != "" { - namespace = sensor.Namespace - } - if err := gc.postCloudEventToWatcher(common.ServiceDNSName(sensor.Name, namespace), gc.gw.Spec.EventProtocol.Http.Port, common.SensorServiceEndpoint, eventPayload); err != nil { - gc.Log.WithField(common.LabelSensorName, sensor.Name).WithError(err).Warn("failed to dispatch event to sensor watcher over http. communication error") - completeSuccess = false - } - } - for _, gateway := range gc.gw.Spec.Watchers.Gateways { - namespace := gc.Namespace - if gateway.Namespace != "" { - namespace = gateway.Namespace - } - if err := gc.postCloudEventToWatcher(common.ServiceDNSName(gateway.Name, namespace), gateway.Port, gateway.Endpoint, eventPayload); err != nil { - gc.Log.WithField(common.LabelGatewayName, gateway.Name).WithError(err).Warn("failed to dispatch event to gateway watcher over http. communication error") - completeSuccess = false - } - } - - response := "dispatched event to all watchers" - if !completeSuccess { - response = fmt.Sprintf("%s.%s", response, " although some of the dispatch operations failed, check logs for more info") - } - - gc.Log.Info(response) - return nil -} - -// dispatchEventOverNats dispatches event over nats -func (gc *GatewayConfig) dispatchEventOverNats(source string, eventPayload []byte) error { - var err error - - switch gc.gw.Spec.EventProtocol.Nats.Type { - case pc.Standard: - err = gc.natsConn.Publish(source, eventPayload) - case pc.Streaming: - err = gc.natsStreamingConn.Publish(source, eventPayload) - } - - if err != nil { - gc.Log.WithField(common.LabelEventSource, source).WithError(err).Error("failed to publish event") - return err - } - - gc.Log.WithField(common.LabelEventSource, source).Info("event published successfully") - return nil -} - -// postCloudEventToWatcher makes a HTTP POST call to watcher's service -func (gc *GatewayConfig) postCloudEventToWatcher(host string, port string, endpoint string, payload []byte) error { - req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%s%s", host, port, endpoint), bytes.NewBuffer(payload)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{ - Timeout: 20 * time.Second, - Transport: &http.Transport{ - Dial: (&net.Dialer{ - KeepAlive: 600 * time.Second, - }).Dial, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 50, - }, - } - _, err = client.Do(req) - return err -} diff --git a/hack/e2e/manifests/gateway-controller-deployment.yaml b/hack/e2e/manifests/gateway-controller-deployment.yaml index bf41cf1acc..f712815efd 100755 --- a/hack/e2e/manifests/gateway-controller-deployment.yaml +++ b/hack/e2e/manifests/gateway-controller-deployment.yaml @@ -15,13 +15,13 @@ spec: spec: serviceAccountName: argo-events-sa containers: - - name: gateway-controller - image: argoproj/gateway-controller:v0.11 - imagePullPolicy: Always - env: - - name: GATEWAY_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: GATEWAY_CONTROLLER_CONFIG_MAP - value: gateway-controller-configmap + - name: gateway-controller + image: argoproj/gateway-controller:v0.10-test + imagePullPolicy: Always + env: + - name: GATEWAY_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GATEWAY_CONTROLLER_CONFIG_MAP + value: gateway-controller-configmap diff --git a/hack/e2e/manifests/sensor-controller-deployment.yaml b/hack/e2e/manifests/sensor-controller-deployment.yaml index ea8b8af6d0..c47734b884 100755 --- a/hack/e2e/manifests/sensor-controller-deployment.yaml +++ b/hack/e2e/manifests/sensor-controller-deployment.yaml @@ -15,13 +15,13 @@ spec: spec: serviceAccountName: argo-events-sa containers: - - name: sensor-controller - image: argoproj/sensor-controller:v0.11 - imagePullPolicy: Always - env: - - name: SENSOR_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: SENSOR_CONFIG_MAP - value: sensor-controller-configmap + - name: sensor-controller + image: argoproj/sensor-controller:v0.10-test + imagePullPolicy: Always + env: + - name: SENSOR_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SENSOR_CONFIG_MAP + value: sensor-controller-configmap diff --git a/hack/k8s/manifests/argo-events-cluster-roles.yaml b/hack/k8s/manifests/argo-events-cluster-roles.yaml index d485ac3a7f..d562529df5 100644 --- a/hack/k8s/manifests/argo-events-cluster-roles.yaml +++ b/hack/k8s/manifests/argo-events-cluster-roles.yaml @@ -7,9 +7,9 @@ roleRef: kind: ClusterRole name: argo-events-role subjects: -- kind: ServiceAccount - name: argo-events-sa - namespace: argo-events + - kind: ServiceAccount + name: argo-events-sa + namespace: argo-events --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -44,10 +44,14 @@ rules: resources: - workflows - workflows/finalizers + - workflowtemplates + - workflowtemplates/finalizers - gateways - gateways/finalizers - sensors - sensors/finalizers + - eventsources + - eventsources/finalizers - apiGroups: - "" resources: diff --git a/hack/k8s/manifests/argo-events-role.yaml b/hack/k8s/manifests/argo-events-role.yaml new file mode 100644 index 0000000000..089c803bd7 --- /dev/null +++ b/hack/k8s/manifests/argo-events-role.yaml @@ -0,0 +1,82 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: argo-events-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argo-events-role +subjects: + - kind: ServiceAccount + name: argo-events-sa + namespace: argo-events +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argo-events-role +rules: + - apiGroups: + - argoproj.io + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + resources: + - workflows + - workflows/finalizers + - workflowtemplates + - workflowtemplates/finalizers + - gateways + - gateways/finalizers + - sensors + - sensors/finalizers + - eventsources + - eventsources/finalizers + - apiGroups: + - "" + resources: + - pods + - pods/exec + - configmaps + - secrets + - services + - events + - persistentvolumeclaims + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "apps" + resources: + - deployments + verbs: + - create + - get + - list + - watch + - update + - patch + - delete diff --git a/hack/k8s/manifests/event-source-crd.yaml b/hack/k8s/manifests/event-source-crd.yaml new file mode 100644 index 0000000000..5f79fc2d57 --- /dev/null +++ b/hack/k8s/manifests/event-source-crd.yaml @@ -0,0 +1,16 @@ +# Define a "event source" custom resource definition +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: eventsources.argoproj.io +spec: + group: argoproj.io + scope: Namespaced + names: + kind: EventSource + plural: eventsources + singular: eventsource + listKind: EventSourceList + shortNames: + - es + version: "v1alpha1" diff --git a/hack/k8s/manifests/gateway-controller-deployment.yaml b/hack/k8s/manifests/gateway-controller-deployment.yaml index 0b4f37231c..e7d3b25d51 100644 --- a/hack/k8s/manifests/gateway-controller-deployment.yaml +++ b/hack/k8s/manifests/gateway-controller-deployment.yaml @@ -15,13 +15,13 @@ spec: spec: serviceAccountName: argo-events-sa containers: - - name: gateway-controller - image: argoproj/gateway-controller - imagePullPolicy: Always - env: - - name: GATEWAY_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: GATEWAY_CONTROLLER_CONFIG_MAP - value: gateway-controller-configmap + - name: gateway-controller + image: argoproj/gateway-controller + imagePullPolicy: Always + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONTROLLER_CONFIG_MAP + value: gateway-controller-configmap diff --git a/hack/k8s/manifests/gateway-crd.yaml b/hack/k8s/manifests/gateway-crd.yaml index 6cab34ba89..7da5597b25 100644 --- a/hack/k8s/manifests/gateway-crd.yaml +++ b/hack/k8s/manifests/gateway-crd.yaml @@ -10,5 +10,7 @@ spec: listKind: GatewayList plural: gateways singular: gateway + shortNames: + - gw scope: Namespaced - version: v1alpha1 + version: "v1alpha1" \ No newline at end of file diff --git a/hack/k8s/manifests/installation.yaml b/hack/k8s/manifests/installation.yaml new file mode 100644 index 0000000000..cd4bf0afce --- /dev/null +++ b/hack/k8s/manifests/installation.yaml @@ -0,0 +1,217 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: gateways.argoproj.io +spec: + group: argoproj.io + names: + kind: Gateway + listKind: GatewayList + plural: gateways + singular: gateway + shortNames: + - gw + scope: Namespaced + version: "v1alpha1" +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: sensors.argoproj.io +spec: + group: argoproj.io + names: + kind: Sensor + listKind: SensorList + plural: sensors + singular: sensor + shortNames: + - sn + scope: Namespaced + version: "v1alpha1" +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: eventsources.argoproj.io +spec: + group: argoproj.io + scope: Namespaced + names: + kind: EventSource + plural: eventsources + singular: eventsource + listKind: EventSourceList + shortNames: + - es + version: "v1alpha1" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: argo-events +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argo-events-sa + namespace: argo-events +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: argo-events-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argo-events-role +subjects: + - kind: ServiceAccount + name: argo-events-sa + namespace: argo-events +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argo-events-role +rules: + - apiGroups: + - argoproj.io + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + resources: + - workflows + - workflows/finalizers + - workflowtemplates + - workflowtemplates/finalizers + - gateways + - gateways/finalizers + - sensors + - sensors/finalizers + - eventsources + - eventsources/finalizers + - apiGroups: + - "" + resources: + - pods + - pods/exec + - configmaps + - secrets + - services + - events + - persistentvolumeclaims + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "apps" + resources: + - deployments + verbs: + - create + - get + - list + - watch + - update + - patch + - delete +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: gateway-controller-configmap +data: + config: | + instanceID: argo-events + namespace: argo-events +--- +# The gateway-controller listens for changes on the gateway CRD and creates gateway +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gateway-controller +spec: + replicas: 1 + selector: + matchLabels: + app: gateway-controller + template: + metadata: + labels: + app: gateway-controller + spec: + serviceAccountName: argo-events-sa + containers: + - name: gateway-controller + image: argoproj/gateway-controller:v0.12-test + imagePullPolicy: Always + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONTROLLER_CONFIG_MAP + value: gateway-controller-configmap +--- +# The sensor-controller configmap includes configuration information for the sensor-controller +# To watch sensors created in different namespace than the controller is deployed in, remove the namespace: argo-events. +# Similarly to watch sensors created in specific namespace, change to namespace: +apiVersion: v1 +kind: ConfigMap +metadata: + name: sensor-controller-configmap +data: + config: | + instanceID: argo-events + namespace: argo-events +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sensor-controller +spec: + replicas: 1 + selector: + matchLabels: + app: sensor-controller + template: + metadata: + labels: + app: sensor-controller + spec: + serviceAccountName: argo-events-sa + containers: + - name: sensor-controller + image: argoproj/sensor-controller:v0.12-test + imagePullPolicy: Always + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONTROLLER_CONFIG_MAP + value: sensor-controller-configmap diff --git a/hack/k8s/manifests/sensor-controller-deployment.yaml b/hack/k8s/manifests/sensor-controller-deployment.yaml index 43886f61e6..e22a03ab76 100644 --- a/hack/k8s/manifests/sensor-controller-deployment.yaml +++ b/hack/k8s/manifests/sensor-controller-deployment.yaml @@ -1,4 +1,3 @@ -# The sensor-controller listens for changes on the sensor CRD and creates sensor executor jobs apiVersion: apps/v1 kind: Deployment metadata: @@ -15,13 +14,13 @@ spec: spec: serviceAccountName: argo-events-sa containers: - - name: sensor-controller - image: argoproj/sensor-controller - imagePullPolicy: Always - env: - - name: SENSOR_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: SENSOR_CONFIG_MAP - value: sensor-controller-configmap + - name: sensor-controller + image: argoproj/sensor-controller + imagePullPolicy: Always + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONTROLLER_CONFIG_MAP + value: sensor-controller-configmap diff --git a/hack/k8s/manifests/sensor-crd.yaml b/hack/k8s/manifests/sensor-crd.yaml index 5b50b24062..4808186ab5 100644 --- a/hack/k8s/manifests/sensor-crd.yaml +++ b/hack/k8s/manifests/sensor-crd.yaml @@ -10,5 +10,7 @@ spec: listKind: SensorList plural: sensors singular: sensor + shortNames: + - sn scope: Namespaced - version: v1alpha1 + version: "v1alpha1" \ No newline at end of file diff --git a/hack/k8s/manifests/workflow-crd.yaml b/hack/k8s/manifests/workflow-crd.yaml deleted file mode 100644 index 230e6b1153..0000000000 --- a/hack/k8s/manifests/workflow-crd.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: "apiextensions.k8s.io/v1beta1" -kind: "CustomResourceDefinition" -metadata: - name: "workflows.argoproj.io" -spec: - group: "argoproj.io" - names: - kind: "Workflow" - plural: "workflows" - shortNames: ["wf"] - scope: "Namespaced" - version: "v1alpha1" \ No newline at end of file diff --git a/hack/update-api-docs.sh b/hack/update-api-docs.sh new file mode 100644 index 0000000000..351ad01106 --- /dev/null +++ b/hack/update-api-docs.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +# Setup at https://github.com/ahmetb/gen-crd-api-reference-docs + +# Event Source +${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/gen-crd-api-reference-docs \ + -config "${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/example-config.json" \ + -api-dir "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" \ + -out-file "${GOPATH}/src/github.com/argoproj/argo-events/api/event-source.html" \ + -template-dir "${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/template" + +# Gateway +${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/gen-crd-api-reference-docs \ + -config "${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/example-config.json" \ + -api-dir "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" \ + -out-file "${GOPATH}/src/github.com/argoproj/argo-events/api/gateway.html" \ + -template-dir "${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/template" + +# Sensor +${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/gen-crd-api-reference-docs \ + -config "${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/example-config.json" \ + -api-dir "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" \ + -out-file "${GOPATH}/src/github.com/argoproj/argo-events/api/sensor.html" \ + -template-dir "${GOPATH}/src/github.com/ahmetb/gen-crd-api-reference-docs/template" + +# Setup at https://pandoc.org/installing.html + +pandoc --from markdown --to gfm ${GOPATH}/src/github.com/argoproj/argo-events/api/event-source.html > ${GOPATH}/src/github.com/argoproj/argo-events/api/event-source.md +pandoc --from markdown --to gfm ${GOPATH}/src/github.com/argoproj/argo-events/api/gateway.html > ${GOPATH}/src/github.com/argoproj/argo-events/api/gateway.md +pandoc --from markdown --to gfm ${GOPATH}/src/github.com/argoproj/argo-events/api/sensor.html > ${GOPATH}/src/github.com/argoproj/argo-events/api/sensor.md diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index ca8c1ae002..0f9f6678d4 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -1,20 +1,5 @@ #!/bin/bash -# Copyright 2017 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - set -o errexit set -o nounset set -o pipefail @@ -32,4 +17,9 @@ bash -x ${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \ "gateway:v1alpha1" \ --go-header-file $SCRIPT_ROOT/hack/custom-boilerplate.go.txt +bash -x ${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \ + github.com/argoproj/argo-events/pkg/client/eventsources github.com/argoproj/argo-events/pkg/apis \ + "eventsources:v1alpha1" \ + --go-header-file $SCRIPT_ROOT/hack/custom-boilerplate.go.txt + go run $SCRIPT_ROOT/vendor/k8s.io/gengo/examples/deepcopy-gen/main.go -i github.com/argoproj/argo-events/pkg/apis/common -p github.com/argoproj/argo-events/pkg/apis/common --go-header-file $SCRIPT_ROOT/vendor/k8s.io/gengo/boilerplate/boilerplate.go.txt diff --git a/hack/update-openapigen.sh b/hack/update-openapigen.sh index 2103907052..92fc82f823 100755 --- a/hack/update-openapigen.sh +++ b/hack/update-openapigen.sh @@ -21,3 +21,10 @@ go run ${CODEGEN_PKG}/cmd/openapi-gen/openapi-gen.go \ --input-dirs github.com/argoproj/argo-events/pkg/apis/gateway/${VERSION} \ --output-package github.com/argoproj/argo-events/pkg/apis/gateway/${VERSION} \ $@ + +# EventSource +go run ${CODEGEN_PKG}/cmd/openapi-gen/openapi-gen.go \ + --go-header-file ${PROJECT_ROOT}/hack/custom-boilerplate.go.txt \ + --input-dirs github.com/argoproj/argo-events/pkg/apis/eventsources/${VERSION} \ + --output-package github.com/argoproj/argo-events/pkg/apis/eventsources/${VERSION} \ + $@ diff --git a/mkdocs.yml b/mkdocs.yml index ce2f31bf4f..cbeceb667e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,19 +9,19 @@ theme: text: 'Work Sans' logo: 'assets/logo.png' google_analytics: -- 'UA-105170809-2' -- 'auto' + - 'UA-105170809-2' + - 'auto' markdown_extensions: -- codehilite -- admonition -- toc: - permalink: true + - codehilite + - admonition + - toc: + permalink: true nav: - Overview: 'index.md' - 'installation.md' - 'getting_started.md' - Setup: - - gateways/artifact.md + - gateways/minio.md - gateways/aws-sns.md - gateways/aws-sqs.md - gateways/calendar.md diff --git a/pkg/apis/common/deepcopy_generated.go b/pkg/apis/common/deepcopy_generated.go index 2f98673114..2438d5d7e0 100644 --- a/pkg/apis/common/deepcopy_generated.go +++ b/pkg/apis/common/deepcopy_generated.go @@ -203,24 +203,6 @@ func (in *S3Filter) DeepCopy() *S3Filter { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceTemplateSpec) DeepCopyInto(out *ServiceTemplateSpec) { - *out = *in - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTemplateSpec. -func (in *ServiceTemplateSpec) DeepCopy() *ServiceTemplateSpec { - if in == nil { - return nil - } - out := new(ServiceTemplateSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *URI) DeepCopyInto(out *URI) { *out = *in diff --git a/pkg/apis/common/event-sources.go b/pkg/apis/common/event-sources.go new file mode 100644 index 0000000000..9e45d62aa9 --- /dev/null +++ b/pkg/apis/common/event-sources.go @@ -0,0 +1,41 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +// EventSourceType is the type of event source supported by the gateway +type EventSourceType string + +// possible event source types +var ( + MinioEvent EventSourceType = "minio" + CalendarEvent EventSourceType = "calendar" + FileEvent EventSourceType = "file" + ResourceEvent EventSourceType = "resource" + WebhookEvent EventSourceType = "webhook" + AMQPEvent EventSourceType = "amqp" + KafkaEvent EventSourceType = "kafka" + MQTTEvent EventSourceType = "mqtt" + NATSEvent EventSourceType = "nats" + SNSEvent EventSourceType = "sns" + SQSEvent EventSourceType = "sqs" + PubSubEvent EventSourceType = "pubsub" + GitHubEvent EventSourceType = "github" + GitLabEvent EventSourceType = "gitlab" + HDFSEvent EventSourceType = "hdfs" + SlackEvent EventSourceType = "slack" + StorageGridEvent EventSourceType = "storagegrid" +) diff --git a/pkg/apis/common/event.go b/pkg/apis/common/event.go index f40e86cec7..ef7af8fdfb 100644 --- a/pkg/apis/common/event.go +++ b/pkg/apis/common/event.go @@ -17,7 +17,6 @@ limitations under the License. package common import ( - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -104,10 +103,8 @@ type URI struct { // Dispatch protocol contains configuration necessary to dispatch an event to sensor over different communication protocols type EventProtocol struct { Type EventProtocolType `json:"type" protobuf:"bytes,1,opt,name=type"` - - Http Http `json:"http" protobuf:"bytes,2,opt,name=http"` - - Nats Nats `json:"nats" protobuf:"bytes,3,opt,name=nats"` + Http Http `json:"http" protobuf:"bytes,2,opt,name=http"` + Nats Nats `json:"nats" protobuf:"bytes,3,opt,name=nats"` } // Http contains the information required to setup a http server and listen to incoming events @@ -148,16 +145,3 @@ type Nats struct { // Type of the connection. either standard or streaming Type NatsType `json:"type" protobuf:"bytes,10,opt,name=type"` } - -// ServiceTemplateSpec is the template spec contains metadata and service spec. -type ServiceTemplateSpec struct { - // Standard object's metadata. - // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata - // +optional - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - // Specification of the desired behavior of the pod. - // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status - // +optional - Spec corev1.ServiceSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` -} diff --git a/pkg/apis/common/s3.go b/pkg/apis/common/s3.go index c296745d63..58aa6674dd 100644 --- a/pkg/apis/common/s3.go +++ b/pkg/apis/common/s3.go @@ -20,7 +20,7 @@ import ( corev1 "k8s.io/api/core/v1" ) -// S3Artifact contains information about an artifact in S3 +// S3Artifact contains information about an S3 connection and bucket type S3Artifact struct { Endpoint string `json:"endpoint" protobuf:"bytes,1,opt,name=endpoint"` Bucket *S3Bucket `json:"bucket" protobuf:"bytes,2,opt,name=bucket"` diff --git a/gateways/common/validate_test.go b/pkg/apis/eventsources/register.go similarity index 68% rename from gateways/common/validate_test.go rename to pkg/apis/eventsources/register.go index a944870c80..da5e971df9 100644 --- a/gateways/common/validate_test.go +++ b/pkg/apis/eventsources/register.go @@ -14,18 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package common +package eventsources -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) - -type fakeEventSource struct { - Msg string `json:"msg"` -} +const ( + // Group is the API Group + Group string = "argoproj.io" -func TestValidateGatewayEventSource(t *testing.T) { - convey.Convey("Given an event source, validate it", t, func() { - }) -} + // EventSource constants + Kind string = "EventSource" + Singular string = "eventsource" + Plural string = "eventsources" + FullName string = Plural + "." + Group +) diff --git a/pkg/apis/eventsources/v1alpha1/doc.go b/pkg/apis/eventsources/v1alpha1/doc.go new file mode 100644 index 0000000000..3556a921ff --- /dev/null +++ b/pkg/apis/eventsources/v1alpha1/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 is the v1alpha1 version of the API. +// +groupName=argoproj.io +// +k8s:deepcopy-gen=package,register +// +k8s:openapi-gen=true +package v1alpha1 diff --git a/pkg/apis/eventsources/v1alpha1/openapi_generated.go b/pkg/apis/eventsources/v1alpha1/openapi_generated.go new file mode 100644 index 0000000000..dde589f1c0 --- /dev/null +++ b/pkg/apis/eventsources/v1alpha1/openapi_generated.go @@ -0,0 +1,1391 @@ +// +build !ignore_autogenerated + +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by openapi-gen. DO NOT EDIT. + +// This file was autogenerated by openapi-gen. Do not edit it manually! + +package v1alpha1 + +import ( + spec "github.com/go-openapi/spec" + common "k8s.io/kube-openapi/pkg/common" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.AMQPEventSource": schema_pkg_apis_eventsources_v1alpha1_AMQPEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.CalendarEventSource": schema_pkg_apis_eventsources_v1alpha1_CalendarEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSource": schema_pkg_apis_eventsources_v1alpha1_EventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceList": schema_pkg_apis_eventsources_v1alpha1_EventSourceList(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceSpec": schema_pkg_apis_eventsources_v1alpha1_EventSourceSpec(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceStatus": schema_pkg_apis_eventsources_v1alpha1_EventSourceStatus(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.FileEventSource": schema_pkg_apis_eventsources_v1alpha1_FileEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GithubEventSource": schema_pkg_apis_eventsources_v1alpha1_GithubEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GitlabEventSource": schema_pkg_apis_eventsources_v1alpha1_GitlabEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.HDFSEventSource": schema_pkg_apis_eventsources_v1alpha1_HDFSEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.KafkaEventSource": schema_pkg_apis_eventsources_v1alpha1_KafkaEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.MQTTEventSource": schema_pkg_apis_eventsources_v1alpha1_MQTTEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.NATSEventsSource": schema_pkg_apis_eventsources_v1alpha1_NATSEventsSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.PubSubEventSource": schema_pkg_apis_eventsources_v1alpha1_PubSubEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceEventSource": schema_pkg_apis_eventsources_v1alpha1_ResourceEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceFilter": schema_pkg_apis_eventsources_v1alpha1_ResourceFilter(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SNSEventSource": schema_pkg_apis_eventsources_v1alpha1_SNSEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SQSEventSource": schema_pkg_apis_eventsources_v1alpha1_SQSEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SlackEventSource": schema_pkg_apis_eventsources_v1alpha1_SlackEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridEventSource": schema_pkg_apis_eventsources_v1alpha1_StorageGridEventSource(ref), + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridFilter": schema_pkg_apis_eventsources_v1alpha1_StorageGridFilter(ref), + } +} + +func schema_pkg_apis_eventsources_v1alpha1_AMQPEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "AMQPEventSource refers to an event-source for AMQP stream events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "URL for rabbitmq service", + Type: []string{"string"}, + Format: "", + }, + }, + "exchangeName": { + SchemaProps: spec.SchemaProps{ + Description: "ExchangeName is the exchange name For more information, visit https://www.rabbitmq.com/tutorials/amqp-concepts.html", + Type: []string{"string"}, + Format: "", + }, + }, + "exchangeType": { + SchemaProps: spec.SchemaProps{ + Description: "ExchangeType is rabbitmq exchange type", + Type: []string{"string"}, + Format: "", + }, + }, + "routingKey": { + SchemaProps: spec.SchemaProps{ + Description: "Routing key for bindings", + Type: []string{"string"}, + Format: "", + }, + }, + "connectionBackoff": { + SchemaProps: spec.SchemaProps{ + Description: "Backoff holds parameters applied to connection.", + Ref: ref("github.com/argoproj/argo-events/common.Backoff"), + }, + }, + }, + Required: []string{"url", "exchangeName", "exchangeType", "routingKey"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/common.Backoff"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_CalendarEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "CalendarEventSource describes a time based dependency. One of the fields (schedule, interval, or recurrence) must be passed. Schedule takes precedence over interval; interval takes precedence over recurrence", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "schedule": { + SchemaProps: spec.SchemaProps{ + Description: "Schedule is a cron-like expression. For reference, see: https://en.wikipedia.org/wiki/Cron", + Type: []string{"string"}, + Format: "", + }, + }, + "interval": { + SchemaProps: spec.SchemaProps{ + Description: "Interval is a string that describes an interval duration, e.g. 1s, 30m, 2h...", + Type: []string{"string"}, + Format: "", + }, + }, + "exclusionDates": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "string", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "ExclusionDates defines the list of DATE-TIME exceptions for recurring events.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "timezone": { + SchemaProps: spec.SchemaProps{ + Description: "Timezone in which to run the schedule", + Type: []string{"string"}, + Format: "", + }, + }, + "userPayload": { + SchemaProps: spec.SchemaProps{ + Description: "UserPayload will be sent to sensor as extra data once the event is triggered", + Type: []string{"string"}, + Format: "byte", + }, + }, + }, + Required: []string{"schedule", "interval"}, + }, + }, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_EventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "EventSource is the definition of a eventsource resource", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceStatus"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceSpec"), + }, + }, + }, + Required: []string{"metadata", "status", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceSpec", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_EventSourceList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "EventSourceList is the list of eventsource resources", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "eventsource", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSource"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.EventSource", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_EventSourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "EventSourceSpec refers to specification of event-source resource", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "minio": { + SchemaProps: spec.SchemaProps{ + Description: "Minio event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/common.S3Artifact"), + }, + }, + }, + }, + }, + "calendar": { + SchemaProps: spec.SchemaProps{ + Description: "Calendar event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.CalendarEventSource"), + }, + }, + }, + }, + }, + "file": { + SchemaProps: spec.SchemaProps{ + Description: "File event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.FileEventSource"), + }, + }, + }, + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Description: "Resource event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceEventSource"), + }, + }, + }, + }, + }, + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Webhook event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/webhook.Context"), + }, + }, + }, + }, + }, + "amqp": { + SchemaProps: spec.SchemaProps{ + Description: "AMQP event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.AMQPEventSource"), + }, + }, + }, + }, + }, + "kafka": { + SchemaProps: spec.SchemaProps{ + Description: "Kafka event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.KafkaEventSource"), + }, + }, + }, + }, + }, + "mqtt": { + SchemaProps: spec.SchemaProps{ + Description: "MQTT event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.MQTTEventSource"), + }, + }, + }, + }, + }, + "nats": { + SchemaProps: spec.SchemaProps{ + Description: "NATS event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.NATSEventsSource"), + }, + }, + }, + }, + }, + "sns": { + SchemaProps: spec.SchemaProps{ + Description: "SNS event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SNSEventSource"), + }, + }, + }, + }, + }, + "sqs": { + SchemaProps: spec.SchemaProps{ + Description: "SQS event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SQSEventSource"), + }, + }, + }, + }, + }, + "pubSub": { + SchemaProps: spec.SchemaProps{ + Description: "PubSub eevnt sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.PubSubEventSource"), + }, + }, + }, + }, + }, + "github": { + SchemaProps: spec.SchemaProps{ + Description: "Github event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GithubEventSource"), + }, + }, + }, + }, + }, + "gitlab": { + SchemaProps: spec.SchemaProps{ + Description: "Gitlab event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GitlabEventSource"), + }, + }, + }, + }, + }, + "hdfs": { + SchemaProps: spec.SchemaProps{ + Description: "HDFS event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.HDFSEventSource"), + }, + }, + }, + }, + }, + "slack": { + SchemaProps: spec.SchemaProps{ + Description: "Slack event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SlackEventSource"), + }, + }, + }, + }, + }, + "storageGrid": { + SchemaProps: spec.SchemaProps{ + Description: "StorageGrid event sources", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridEventSource"), + }, + }, + }, + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of the event source", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/webhook.Context", "github.com/argoproj/argo-events/pkg/apis/common.S3Artifact", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.AMQPEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.CalendarEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.FileEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GithubEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.GitlabEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.HDFSEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.KafkaEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.MQTTEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.NATSEventsSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.PubSubEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SNSEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SQSEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.SlackEventSource", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridEventSource"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_EventSourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "EventSourceStatus holds the status of the event-source resource", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "createdAt": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_FileEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "FileEventSource describes an event-source for file related events.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "eventType": { + SchemaProps: spec.SchemaProps{ + Description: "Type of file operations to watch Refer https://github.com/fsnotify/fsnotify/blob/master/fsnotify.go for more information", + Type: []string{"string"}, + Format: "", + }, + }, + "watchPathConfig": { + SchemaProps: spec.SchemaProps{ + Description: "WatchPathConfig contains configuration about the file path to watch", + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/fsevent.WatchPathConfig"), + }, + }, + }, + Required: []string{"eventType", "watchPathConfig"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/fsevent.WatchPathConfig"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_GithubEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GithubEventSource refers to event-source for github related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + SchemaProps: spec.SchemaProps{ + Description: "Id is the webhook's id", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Webhook refers to the configuration required to run a http server", + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/webhook.Context"), + }, + }, + "owner": { + SchemaProps: spec.SchemaProps{ + Description: "Owner refers to GitHub owner name i.e. argoproj", + Type: []string{"string"}, + Format: "", + }, + }, + "repository": { + SchemaProps: spec.SchemaProps{ + Description: "Repository refers to GitHub repo name i.e. argo-events", + Type: []string{"string"}, + Format: "", + }, + }, + "events": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "string", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Events refer to Github events to subscribe to which the gateway will subscribe", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "apiToken": { + SchemaProps: spec.SchemaProps{ + Description: "APIToken refers to a K8s secret containing github api token", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "webhookSecret": { + SchemaProps: spec.SchemaProps{ + Description: "WebhookSecret refers to K8s secret containing GitHub webhook secret https://developer.github.com/webhooks/securing/", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "insecure": { + SchemaProps: spec.SchemaProps{ + Description: "Insecure tls verification", + Type: []string{"boolean"}, + Format: "", + }, + }, + "active": { + SchemaProps: spec.SchemaProps{ + Description: "Active refers to status of the webhook for event deliveries. https://developer.github.com/webhooks/creating/#active", + Type: []string{"boolean"}, + Format: "", + }, + }, + "contentType": { + SchemaProps: spec.SchemaProps{ + Description: "ContentType of the event delivery", + Type: []string{"string"}, + Format: "", + }, + }, + "githubBaseURL": { + SchemaProps: spec.SchemaProps{ + Description: "GitHub base URL (for GitHub Enterprise)", + Type: []string{"string"}, + Format: "", + }, + }, + "githubUploadURL": { + SchemaProps: spec.SchemaProps{ + Description: "GitHub upload URL (for GitHub Enterprise)", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace refers to Kubernetes namespace which is used to retrieve webhook secret and api token from.", + Type: []string{"string"}, + Format: "", + }, + }, + "deleteHookOnFinish": { + SchemaProps: spec.SchemaProps{ + Description: "DeleteHookOnFinish determines whether to delete the GitHub hook for the repository once the event source is stopped.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"id", "webhook", "owner", "repository", "events", "apiToken", "namespace"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/webhook.Context", "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_GitlabEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GitlabEventSource refers to event-source related to Gitlab events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Webhook holds configuration to run a http server", + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/webhook.Context"), + }, + }, + "projectId": { + SchemaProps: spec.SchemaProps{ + Description: "ProjectId is the id of project for which integration needs to setup", + Type: []string{"string"}, + Format: "", + }, + }, + "event": { + SchemaProps: spec.SchemaProps{ + Description: "Event is a gitlab event to listen to. Refer https://github.com/xanzy/go-gitlab/blob/bf34eca5d13a9f4c3f501d8a97b8ac226d55e4d9/projects.go#L794.", + Type: []string{"string"}, + Format: "", + }, + }, + "accessToken": { + SchemaProps: spec.SchemaProps{ + Description: "AccessToken is reference to k8 secret which holds the gitlab api access information", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "enableSSLVerification": { + SchemaProps: spec.SchemaProps{ + Description: "EnableSSLVerification to enable ssl verification", + Type: []string{"boolean"}, + Format: "", + }, + }, + "gitlabBaseURL": { + SchemaProps: spec.SchemaProps{ + Description: "GitlabBaseURL is the base URL for API requests to a custom endpoint", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace refers to Kubernetes namespace which is used to retrieve access token from.", + Type: []string{"string"}, + Format: "", + }, + }, + "deleteHookOnFinish": { + SchemaProps: spec.SchemaProps{ + Description: "DeleteHookOnFinish determines whether to delete the GitLab hook for the project once the event source is stopped.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"webhook", "projectId", "event", "accessToken", "gitlabBaseURL", "namespace"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/webhook.Context", "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_HDFSEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HDFSEventSource refers to event-source for HDFS related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "directory": { + SchemaProps: spec.SchemaProps{ + Description: "Directory to watch for events", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "Path is relative path of object to watch with respect to the directory", + Type: []string{"string"}, + Format: "", + }, + }, + "pathRegexp": { + SchemaProps: spec.SchemaProps{ + Description: "PathRegexp is regexp of relative path of object to watch with respect to the directory", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of file operations to watch", + Type: []string{"string"}, + Format: "", + }, + }, + "checkInterval": { + SchemaProps: spec.SchemaProps{ + Description: "CheckInterval is a string that describes an interval duration to check the directory state, e.g. 1s, 30m, 2h... (defaults to 1m)", + Type: []string{"string"}, + Format: "", + }, + }, + "addresses": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "string", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Addresses is accessible addresses of HDFS name nodes", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "hdfsUser": { + SchemaProps: spec.SchemaProps{ + Description: "HDFSUser is the user to access HDFS file system. It is ignored if either ccache or keytab is used.", + Type: []string{"string"}, + Format: "", + }, + }, + "krbCCacheSecret": { + SchemaProps: spec.SchemaProps{ + Description: "KrbCCacheSecret is the secret selector for Kerberos ccache Either ccache or keytab can be set to use Kerberos.", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "krbKeytabSecret": { + SchemaProps: spec.SchemaProps{ + Description: "KrbKeytabSecret is the secret selector for Kerberos keytab Either ccache or keytab can be set to use Kerberos.", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "krbUsername": { + SchemaProps: spec.SchemaProps{ + Description: "KrbUsername is the Kerberos username used with Kerberos keytab It must be set if keytab is used.", + Type: []string{"string"}, + Format: "", + }, + }, + "krbRealm": { + SchemaProps: spec.SchemaProps{ + Description: "KrbRealm is the Kerberos realm used with Kerberos keytab It must be set if keytab is used.", + Type: []string{"string"}, + Format: "", + }, + }, + "krbConfigConfigMap": { + SchemaProps: spec.SchemaProps{ + Description: "KrbConfig is the configmap selector for Kerberos config as string It must be set if either ccache or keytab is used.", + Ref: ref("k8s.io/api/core/v1.ConfigMapKeySelector"), + }, + }, + "krbServicePrincipalName": { + SchemaProps: spec.SchemaProps{ + Description: "KrbServicePrincipalName is the principal name of Kerberos service It must be set if either ccache or keytab is used.", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace refers to Kubernetes namespace which is used to retrieve cache secret and ket tab secret from.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"directory", "type", "addresses", "namespace"}, + }, + }, + Dependencies: []string{ + "k8s.io/api/core/v1.ConfigMapKeySelector", "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_KafkaEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "KafkaEventSource refers to event-source for Kafka related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "URL to kafka cluster", + Type: []string{"string"}, + Format: "", + }, + }, + "partition": { + SchemaProps: spec.SchemaProps{ + Description: "Partition name", + Type: []string{"string"}, + Format: "", + }, + }, + "topic": { + SchemaProps: spec.SchemaProps{ + Description: "Topic name", + Type: []string{"string"}, + Format: "", + }, + }, + "connectionBackoff": { + SchemaProps: spec.SchemaProps{ + Description: "Backoff holds parameters applied to connection.", + Ref: ref("github.com/argoproj/argo-events/common.Backoff"), + }, + }, + }, + Required: []string{"url", "partition", "topic"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/common.Backoff"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_MQTTEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MQTTEventSource refers to event-source for MQTT related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "URL to connect to broker", + Type: []string{"string"}, + Format: "", + }, + }, + "topic": { + SchemaProps: spec.SchemaProps{ + Description: "Topic name", + Type: []string{"string"}, + Format: "", + }, + }, + "clientId": { + SchemaProps: spec.SchemaProps{ + Description: "ClientID is the id of the client", + Type: []string{"string"}, + Format: "", + }, + }, + "connectionBackoff": { + SchemaProps: spec.SchemaProps{ + Description: "ConnectionBackoff holds backoff applied to connection.", + Ref: ref("github.com/argoproj/argo-events/common.Backoff"), + }, + }, + }, + Required: []string{"url", "topic", "clientId"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/common.Backoff"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_NATSEventsSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "NATSEventSource refers to event-source for NATS related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "URL to connect to NATS cluster", + Type: []string{"string"}, + Format: "", + }, + }, + "subject": { + SchemaProps: spec.SchemaProps{ + Description: "Subject holds the name of the subject onto which messages are published", + Type: []string{"string"}, + Format: "", + }, + }, + "connectionBackoff": { + SchemaProps: spec.SchemaProps{ + Description: "ConnectionBackoff holds backoff applied to connection.", + Ref: ref("github.com/argoproj/argo-events/common.Backoff"), + }, + }, + }, + Required: []string{"url", "subject"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/common.Backoff"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_PubSubEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PubSubEventSource refers to event-source for GCP PubSub related events.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "projectID": { + SchemaProps: spec.SchemaProps{ + Description: "ProjectID is the unique identifier for your project on GCP", + Type: []string{"string"}, + Format: "", + }, + }, + "topicProjectID": { + SchemaProps: spec.SchemaProps{ + Description: "TopicProjectID identifies the project where the topic should exist or be created (assumed to be the same as ProjectID by default)", + Type: []string{"string"}, + Format: "", + }, + }, + "topic": { + SchemaProps: spec.SchemaProps{ + Description: "Topic on which a subscription will be created", + Type: []string{"string"}, + Format: "", + }, + }, + "credentialsFile": { + SchemaProps: spec.SchemaProps{ + Description: "CredentialsFile is the file that contains credentials to authenticate for GCP", + Type: []string{"string"}, + Format: "", + }, + }, + "deleteSubscriptionOnFinish": { + SchemaProps: spec.SchemaProps{ + Description: "DeleteSubscriptionOnFinish determines whether to delete the GCP PubSub subscription once the event source is stopped.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"projectID", "topicProjectID", "topic", "credentialsFile"}, + }, + }, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_ResourceEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ResourceEventSource refers to a event-source for K8s resource related events.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace where resource is deployed", + Type: []string{"string"}, + Format: "", + }, + }, + "filter": { + SchemaProps: spec.SchemaProps{ + Description: "Filter is applied on the metadata of the resource", + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceFilter"), + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "eventType": { + SchemaProps: spec.SchemaProps{ + Description: "Type is the event type. If not provided, the gateway will watch all events for a resource.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"namespace", "group", "version", "resource"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.ResourceFilter"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_ResourceFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ResourceFilter contains K8 ObjectMeta information to further filter resource event objects", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "prefix": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "labels": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "fields": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "createdBy": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_SNSEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SNSEventSource refers to event-source for AWS SNS related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Webhook configuration for http server", + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/webhook.Context"), + }, + }, + "topicArn": { + SchemaProps: spec.SchemaProps{ + Description: "TopicArn", + Type: []string{"string"}, + Format: "", + }, + }, + "accessKey": { + SchemaProps: spec.SchemaProps{ + Description: "AccessKey refers K8 secret containing aws access key", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "secretKey": { + SchemaProps: spec.SchemaProps{ + Description: "SecretKey refers K8 secret containing aws secret key", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace refers to Kubernetes namespace to read access related secret from.", + Type: []string{"string"}, + Format: "", + }, + }, + "region": { + SchemaProps: spec.SchemaProps{ + Description: "Region is AWS region", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"webhook", "topicArn", "region"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/webhook.Context", "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_SQSEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SQSEventSource refers to event-source for AWS SQS related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "accessKey": { + SchemaProps: spec.SchemaProps{ + Description: "AccessKey refers K8 secret containing aws access key", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "secretKey": { + SchemaProps: spec.SchemaProps{ + Description: "SecretKey refers K8 secret containing aws secret key", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "region": { + SchemaProps: spec.SchemaProps{ + Description: "Region is AWS region", + Type: []string{"string"}, + Format: "", + }, + }, + "queue": { + SchemaProps: spec.SchemaProps{ + Description: "Queue is AWS SQS queue to listen to for messages", + Type: []string{"string"}, + Format: "", + }, + }, + "waitTimeSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "WaitTimeSeconds is The duration (in seconds) for which the call waits for a message to arrive in the queue before returning.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace refers to Kubernetes namespace to read access related secret from.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"region", "queue", "waitTimeSeconds"}, + }, + }, + Dependencies: []string{ + "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_SlackEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SlackEventSource refers to event-source for Slack related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "signingSecret": { + SchemaProps: spec.SchemaProps{ + Description: "Slack App signing secret", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "token": { + SchemaProps: spec.SchemaProps{ + Description: "Token for URL verification handshake", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Webhook holds configuration for a REST endpoint", + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/webhook.Context"), + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace refers to Kubernetes namespace which is used to retrieve token and signing secret from.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"webhook", "namespace"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/webhook.Context", "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_StorageGridEventSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StorageGridEventSource refers to event-source for StorageGrid related events", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Webhook holds configuration for a REST endpoint", + Ref: ref("github.com/argoproj/argo-events/gateways/server/common/webhook.Context"), + }, + }, + "events": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "string", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Events are s3 bucket notification events. For more information on s3 notifications, follow https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-event-types-and-destinations Note that storage grid notifications do not contain `s3:`", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "filter": { + SchemaProps: spec.SchemaProps{ + Description: "Filter on object key which caused the notification.", + Ref: ref("github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridFilter"), + }, + }, + }, + Required: []string{"webhook"}, + }, + }, + Dependencies: []string{ + "github.com/argoproj/argo-events/gateways/server/common/webhook.Context", "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1.StorageGridFilter"}, + } +} + +func schema_pkg_apis_eventsources_v1alpha1_StorageGridFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Filter represents filters to apply to bucket notifications for specifying constraints on objects", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "prefix": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "suffix": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"prefix", "suffix"}, + }, + }, + } +} diff --git a/pkg/apis/eventsources/v1alpha1/register.go b/pkg/apis/eventsources/v1alpha1/register.go new file mode 100644 index 0000000000..8eebe9a21a --- /dev/null +++ b/pkg/apis/eventsources/v1alpha1/register.go @@ -0,0 +1,56 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v1alpha1 + +import ( + event_sources "github.com/argoproj/argo-events/pkg/apis/eventsources" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is a group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: event_sources.Group, Version: "v1alpha1"} + +// SchemaGroupVersionKind is a group version kind used to attach owner references to gateway-controller +var SchemaGroupVersionKind = schema.GroupVersionKind{Group: event_sources.Group, Version: "v1alpha1", Kind: event_sources.Kind} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes unqualified resource and returns Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder is the builder for this scheme + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds this + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &EventSource{}, + &EventSourceList{}, + ) + v1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/eventsources/v1alpha1/types.go b/pkg/apis/eventsources/v1alpha1/types.go new file mode 100644 index 0000000000..7800046f27 --- /dev/null +++ b/pkg/apis/eventsources/v1alpha1/types.go @@ -0,0 +1,387 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v1alpha1 + +import ( + "encoding/json" + + "github.com/argoproj/argo-events/common" + "github.com/argoproj/argo-events/gateways/server/common/fsevent" + "github.com/argoproj/argo-events/gateways/server/common/webhook" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EventSource is the definition of a eventsource resource +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:openapi-gen=true +type EventSource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + Status EventSourceStatus `json:"status" protobuf:"bytes,2,opt,name=status"` + Spec *EventSourceSpec `json:"spec" protobuf:"bytes,3,opt,name=spec"` +} + +// EventSourceList is the list of eventsource resources +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type EventSourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + // +listType=eventsource + Items []EventSource `json:"items" protobuf:"bytes,2,opt,name=items"` +} + +// EventSourceSpec refers to specification of event-source resource +type EventSourceSpec struct { + // Minio event sources + Minio map[string]apicommon.S3Artifact `json:"minio,omitempty" protobuf:"bytes,1,opt,name=minio"` + // Calendar event sources + Calendar map[string]CalendarEventSource `json:"calendar,omitempty" protobuf:"bytes,2,opt,name=calendar"` + // File event sources + File map[string]FileEventSource `json:"file,omitempty" protobuf:"bytes,3,opt,name=file"` + // Resource event sources + Resource map[string]ResourceEventSource `json:"resource,omitempty" protobuf:"bytes,4,opt,name=resource"` + // Webhook event sources + Webhook map[string]webhook.Context `json:"webhook,omitempty" protobuf:"bytes,5,opt,name=webhook"` + // AMQP event sources + AMQP map[string]AMQPEventSource `json:"amqp,omitempty" protobuf:"bytes,6,opt,name=amqp"` + // Kafka event sources + Kafka map[string]KafkaEventSource `json:"kafka,omitempty" protobuf:"bytes,7,opt,name=kafka"` + // MQTT event sources + MQTT map[string]MQTTEventSource `json:"mqtt,omitempty" protobuf:"bytes,8,opt,name=mqtt"` + // NATS event sources + NATS map[string]NATSEventsSource `json:"nats,omitempty" protobuf:"bytes,9,opt,name=nats"` + // SNS event sources + SNS map[string]SNSEventSource `json:"sns,omitempty" protobuf:"bytes,10,opt,name=sns"` + // SQS event sources + SQS map[string]SQSEventSource `json:"sqs,omitempty" protobuf:"bytes,11,opt,name=sqs"` + // PubSub eevnt sources + PubSub map[string]PubSubEventSource `json:"pubSub,omitempty" protobuf:"bytes,12,opt,name=pubSub"` + // Github event sources + Github map[string]GithubEventSource `json:"github,omitempty" protobuf:"bytes,13,opt,name=github"` + // Gitlab event sources + Gitlab map[string]GitlabEventSource `json:"gitlab,omitempty" protobuf:"bytes,14,opt,name=gitlab"` + // HDFS event sources + HDFS map[string]HDFSEventSource `json:"hdfs,omitempty" protobuf:"bytes,15,opt,name=hdfs"` + // Slack event sources + Slack map[string]SlackEventSource `json:"slack,omitempty" protobuf:"bytes,16,opt,name=slack"` + // StorageGrid event sources + StorageGrid map[string]StorageGridEventSource `json:"storageGrid,omitempty" protobuf:"bytes,17,opt,name=storageGrid"` + // Type of the event source + Type apicommon.EventSourceType `json:"type" protobuf:"bytes,19,name=type"` +} + +// CalendarEventSource describes a time based dependency. One of the fields (schedule, interval, or recurrence) must be passed. +// Schedule takes precedence over interval; interval takes precedence over recurrence +type CalendarEventSource struct { + // Schedule is a cron-like expression. For reference, see: https://en.wikipedia.org/wiki/Cron + Schedule string `json:"schedule" protobuf:"bytes,1,name=schedule"` + // Interval is a string that describes an interval duration, e.g. 1s, 30m, 2h... + Interval string `json:"interval" protobuf:"bytes,2,name=interval"` + // ExclusionDates defines the list of DATE-TIME exceptions for recurring events. + // +listType=string + ExclusionDates []string `json:"exclusionDates,omitempty" protobuf:"bytes,3,opt,name=exclusionDates"` + // Timezone in which to run the schedule + // +optional + Timezone string `json:"timezone,omitempty" protobuf:"bytes,4,opt,name=timezone"` + // UserPayload will be sent to sensor as extra data once the event is triggered + // +optional + UserPayload *json.RawMessage `json:"userPayload,omitempty" protobuf:"bytes,5,opt,name=userPayload"` +} + +// FileEventSource describes an event-source for file related events. +type FileEventSource struct { + // Type of file operations to watch + // Refer https://github.com/fsnotify/fsnotify/blob/master/fsnotify.go for more information + EventType string `json:"eventType" protobuf:"bytes,1,name=eventType"` + // WatchPathConfig contains configuration about the file path to watch + WatchPathConfig fsevent.WatchPathConfig `json:"watchPathConfig" protobuf:"bytes,2,name=watchPathConfig"` +} + +// ResourceEventType is the type of event for the K8s resource mutation +type ResourceEventType string + +// possible values of ResourceEventType +const ( + ADD ResourceEventType = "ADD" + UPDATE ResourceEventType = "UPDATE" + DELETE ResourceEventType = "DELETE" +) + +// ResourceEventSource refers to a event-source for K8s resource related events. +type ResourceEventSource struct { + // Namespace where resource is deployed + Namespace string `json:"namespace" protobuf:"bytes,1,name=namespace"` + // Filter is applied on the metadata of the resource + // +optional + Filter *ResourceFilter `json:"filter,omitempty" protobuf:"bytes,2,opt,name=filter"` + // Group of the resource + metav1.GroupVersionResource `json:",inline"` + // Type is the event type. + // If not provided, the gateway will watch all events for a resource. + // +optional + EventType ResourceEventType `json:"eventType,omitempty" protobuf:"bytes,3,opt,name=eventType"` +} + +// ResourceFilter contains K8 ObjectMeta information to further filter resource event objects +type ResourceFilter struct { + // +optional + Prefix string `json:"prefix,omitempty" protobuf:"bytes,1,opt,name=prefix"` + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,2,opt,name=labels"` + // +optional + Fields map[string]string `json:"fields,omitempty" protobuf:"bytes,3,opt,name=fields"` + // +optional + CreatedBy metav1.Time `json:"createdBy,omitempty" protobuf:"bytes,4,opt,name=createdBy"` +} + +// AMQPEventSource refers to an event-source for AMQP stream events +type AMQPEventSource struct { + // URL for rabbitmq service + URL string `json:"url" protobuf:"bytes,1,name=url"` + // ExchangeName is the exchange name + // For more information, visit https://www.rabbitmq.com/tutorials/amqp-concepts.html + ExchangeName string `json:"exchangeName" protobuf:"bytes,2,name=exchangeName"` + // ExchangeType is rabbitmq exchange type + ExchangeType string `json:"exchangeType" protobuf:"bytes,3,name=exchangeType"` + // Routing key for bindings + RoutingKey string `json:"routingKey" protobuf:"bytes,4,name=routingKey"` + // Backoff holds parameters applied to connection. + // +optional + ConnectionBackoff *common.Backoff `json:"connectionBackoff,omitempty" protobuf:"bytes,5,opt,name=connectionBackoff"` +} + +// KafkaEventSource refers to event-source for Kafka related events +type KafkaEventSource struct { + // URL to kafka cluster + URL string `json:"url" protobuf:"bytes,1,name=url"` + // Partition name + Partition string `json:"partition" protobuf:"bytes,2,name=partition"` + // Topic name + Topic string `json:"topic" protobuf:"bytes,3,name=topic"` + // Backoff holds parameters applied to connection. + ConnectionBackoff *common.Backoff `json:"connectionBackoff,omitempty" protobuf:"bytes,4,opt,name=connectionBackoff"` +} + +// MQTTEventSource refers to event-source for MQTT related events +type MQTTEventSource struct { + // URL to connect to broker + URL string `json:"url" protobuf:"bytes,1,name=url"` + // Topic name + Topic string `json:"topic" protobuf:"bytes,2,name=topic"` + // ClientID is the id of the client + ClientId string `json:"clientId" protobuf:"bytes,3,name=clientId"` + // ConnectionBackoff holds backoff applied to connection. + ConnectionBackoff *common.Backoff `json:"connectionBackoff,omitempty" protobuf:"bytes,4,opt,name=connectionBackoff"` +} + +// NATSEventSource refers to event-source for NATS related events +type NATSEventsSource struct { + // URL to connect to NATS cluster + URL string `json:"url" protobuf:"bytes,1,name=url"` + // Subject holds the name of the subject onto which messages are published + Subject string `json:"subject" protobuf:"bytes,2,name=2"` + // ConnectionBackoff holds backoff applied to connection. + ConnectionBackoff *common.Backoff `json:"connectionBackoff,omitempty" protobuf:"bytes,3,opt,name=connectionBackoff"` +} + +// SNSEventSource refers to event-source for AWS SNS related events +type SNSEventSource struct { + // Webhook configuration for http server + Webhook *webhook.Context `json:"webhook" protobuf:"bytes,1,name=webhook"` + // TopicArn + TopicArn string `json:"topicArn" protobuf:"bytes,2,name=topicArn"` + // AccessKey refers K8 secret containing aws access key + AccessKey *corev1.SecretKeySelector `json:"accessKey,omitempty" protobuf:"bytes,3,opt,name=accessKey"` + // SecretKey refers K8 secret containing aws secret key + SecretKey *corev1.SecretKeySelector `json:"secretKey,omitempty" protobuf:"bytes,4,opt,name=secretKey"` + // Namespace refers to Kubernetes namespace to read access related secret from. + // +optional + Namespace string `json:"namespace,omitempty" protobuf:"bytes,5,opt,name=namespace"` + // Region is AWS region + Region string `json:"region" protobuf:"bytes,6,name=region"` +} + +// SQSEventSource refers to event-source for AWS SQS related events +type SQSEventSource struct { + // AccessKey refers K8 secret containing aws access key + AccessKey *corev1.SecretKeySelector `json:"accessKey,omitempty" protobuf:"bytes,1,opt,name=accessKey"` + // SecretKey refers K8 secret containing aws secret key + SecretKey *corev1.SecretKeySelector `json:"secretKey,omitempty" protobuf:"bytes,2,opt,name=accessKey"` + // Region is AWS region + Region string `json:"region" protobuf:"bytes,3,name=region"` + // Queue is AWS SQS queue to listen to for messages + Queue string `json:"queue" protobuf:"bytes,4,name=queue"` + // WaitTimeSeconds is The duration (in seconds) for which the call waits for a message to arrive + // in the queue before returning. + WaitTimeSeconds int64 `json:"waitTimeSeconds" protobuf:"bytes,5,name=waitTimeSeconds"` + // Namespace refers to Kubernetes namespace to read access related secret from. + // +optional + Namespace string `json:"namespace,omitempty" protobuf:"bytes,6,opt,name=namespace"` +} + +// PubSubEventSource refers to event-source for GCP PubSub related events. +type PubSubEventSource struct { + // ProjectID is the unique identifier for your project on GCP + ProjectID string `json:"projectID" protobuf:"bytes,1,name=projectID"` + // TopicProjectID identifies the project where the topic should exist or be created + // (assumed to be the same as ProjectID by default) + TopicProjectID string `json:"topicProjectID" protobuf:"bytes,2,name=topicProjectID"` + // Topic on which a subscription will be created + Topic string `json:"topic" protobuf:"bytes,3,name=topic"` + // CredentialsFile is the file that contains credentials to authenticate for GCP + CredentialsFile string `json:"credentialsFile" protobuf:"bytes,4,name=credentialsFile"` + // DeleteSubscriptionOnFinish determines whether to delete the GCP PubSub subscription once the event source is stopped. + // +optional + DeleteSubscriptionOnFinish bool `json:"deleteSubscriptionOnFinish,omitempty" protobuf:"bytes,1,opt,name=deleteSubscriptionOnFinish"` +} + +// GithubEventSource refers to event-source for github related events +type GithubEventSource struct { + // Id is the webhook's id + Id int64 `json:"id" protobuf:"bytes,1,name=id"` + // Webhook refers to the configuration required to run a http server + Webhook *webhook.Context `json:"webhook" protobuf:"bytes,2,name=webhook"` + // Owner refers to GitHub owner name i.e. argoproj + Owner string `json:"owner" protobuf:"bytes,3,name=owner"` + // Repository refers to GitHub repo name i.e. argo-events + Repository string `json:"repository" protobuf:"bytes,4,name=repository"` + // Events refer to Github events to subscribe to which the gateway will subscribe + // +listType=string + Events []string `json:"events" protobuf:"bytes,5,rep,name=events"` + // APIToken refers to a K8s secret containing github api token + APIToken *corev1.SecretKeySelector `json:"apiToken"` + // WebhookSecret refers to K8s secret containing GitHub webhook secret + // https://developer.github.com/webhooks/securing/ + // +optional + WebhookSecret *corev1.SecretKeySelector `json:"webhookSecret,omitempty" protobuf:"bytes,7,opt,name=webhookSecret"` + // Insecure tls verification + Insecure bool `json:"insecure,omitempty" protobuf:"bytes,8,opt,name=insecure"` + // Active refers to status of the webhook for event deliveries. + // https://developer.github.com/webhooks/creating/#active + // +optional + Active bool `json:"active,omitempty" protobuf:"bytes,9,opt,name=active"` + // ContentType of the event delivery + ContentType string `json:"contentType,omitempty" protobuf:"bytes,10,opt,name=contentType"` + // GitHub base URL (for GitHub Enterprise) + // +optional + GithubBaseURL string `json:"githubBaseURL,omitempty" protobuf:"bytes,11,opt,name=githubBaseURL"` + // GitHub upload URL (for GitHub Enterprise) + // +optional + GithubUploadURL string `json:"githubUploadURL,omitempty" protobuf:"bytes,12,opt,name=githubUploadURL"` + // Namespace refers to Kubernetes namespace which is used to retrieve webhook secret and api token from. + Namespace string `json:"namespace" protobuf:"bytes,13,name=namespace"` + // DeleteHookOnFinish determines whether to delete the GitHub hook for the repository once the event source is stopped. + // +optional + DeleteHookOnFinish bool `json:"deleteHookOnFinish,omitempty" protobuf:"bytes,14,opt,name=deleteHookOnFinish"` +} + +// GitlabEventSource refers to event-source related to Gitlab events +type GitlabEventSource struct { + // Webhook holds configuration to run a http server + Webhook *webhook.Context `json:"webhook" protobuf:"bytes,1,name=webhook"` + // ProjectId is the id of project for which integration needs to setup + ProjectId string `json:"projectId" protobuf:"bytes,2,name=projectId"` + // Event is a gitlab event to listen to. + // Refer https://github.com/xanzy/go-gitlab/blob/bf34eca5d13a9f4c3f501d8a97b8ac226d55e4d9/projects.go#L794. + Event string `json:"event" protobuf:"bytes,3,name=event"` + // AccessToken is reference to k8 secret which holds the gitlab api access information + AccessToken *corev1.SecretKeySelector `json:"accessToken" protobuf:"bytes,4,name=accessToken"` + // EnableSSLVerification to enable ssl verification + // +optional + EnableSSLVerification bool `json:"enableSSLVerification,omitempty" protobuf:"bytes,5,opt,name=enableSSLVerification"` + // GitlabBaseURL is the base URL for API requests to a custom endpoint + GitlabBaseURL string `json:"gitlabBaseURL" protobuf:"bytes,6,name=gitlabBaseURL"` + // Namespace refers to Kubernetes namespace which is used to retrieve access token from. + Namespace string `json:"namespace" protobuf:"bytes,7,name=namespace"` + // DeleteHookOnFinish determines whether to delete the GitLab hook for the project once the event source is stopped. + // +optional + DeleteHookOnFinish bool `json:"deleteHookOnFinish,omitempty" protobuf:"bytes,8,opt,name=deleteHookOnFinish"` +} + +// HDFSEventSource refers to event-source for HDFS related events +type HDFSEventSource struct { + fsevent.WatchPathConfig `json:",inline"` + // Type of file operations to watch + Type string `json:"type"` + // CheckInterval is a string that describes an interval duration to check the directory state, e.g. 1s, 30m, 2h... (defaults to 1m) + CheckInterval string `json:"checkInterval,omitempty"` + // Addresses is accessible addresses of HDFS name nodes + // +listType=string + Addresses []string `json:"addresses"` + // HDFSUser is the user to access HDFS file system. + // It is ignored if either ccache or keytab is used. + HDFSUser string `json:"hdfsUser,omitempty"` + // KrbCCacheSecret is the secret selector for Kerberos ccache + // Either ccache or keytab can be set to use Kerberos. + KrbCCacheSecret *corev1.SecretKeySelector `json:"krbCCacheSecret,omitempty"` + // KrbKeytabSecret is the secret selector for Kerberos keytab + // Either ccache or keytab can be set to use Kerberos. + KrbKeytabSecret *corev1.SecretKeySelector `json:"krbKeytabSecret,omitempty"` + // KrbUsername is the Kerberos username used with Kerberos keytab + // It must be set if keytab is used. + KrbUsername string `json:"krbUsername,omitempty"` + // KrbRealm is the Kerberos realm used with Kerberos keytab + // It must be set if keytab is used. + KrbRealm string `json:"krbRealm,omitempty"` + // KrbConfig is the configmap selector for Kerberos config as string + // It must be set if either ccache or keytab is used. + KrbConfigConfigMap *corev1.ConfigMapKeySelector `json:"krbConfigConfigMap,omitempty"` + // KrbServicePrincipalName is the principal name of Kerberos service + // It must be set if either ccache or keytab is used. + KrbServicePrincipalName string `json:"krbServicePrincipalName,omitempty"` + // Namespace refers to Kubernetes namespace which is used to retrieve cache secret and ket tab secret from. + Namespace string `json:"namespace" protobuf:"bytes,1,name=namespace"` +} + +// SlackEventSource refers to event-source for Slack related events +type SlackEventSource struct { + // Slack App signing secret + SigningSecret *corev1.SecretKeySelector `json:"signingSecret,omitempty" protobuf:"bytes,1,opt,name=signingSecret"` + // Token for URL verification handshake + Token *corev1.SecretKeySelector `json:"token,omitempty" protobuf:"bytes,2,name=token"` + // Webhook holds configuration for a REST endpoint + Webhook *webhook.Context `json:"webhook" protobuf:"bytes,3,name=webhook"` + // Namespace refers to Kubernetes namespace which is used to retrieve token and signing secret from. + Namespace string `json:"namespace" protobuf:"bytes,4,name=namespace"` +} + +// StorageGridEventSource refers to event-source for StorageGrid related events +type StorageGridEventSource struct { + // Webhook holds configuration for a REST endpoint + Webhook *webhook.Context `json:"webhook" protobuf:"bytes,1,name=webhook"` + // Events are s3 bucket notification events. + // For more information on s3 notifications, follow https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-event-types-and-destinations + // Note that storage grid notifications do not contain `s3:` + // +listType=string + Events []string `json:"events,omitempty" protobuf:"bytes,2,opt,name=events"` + // Filter on object key which caused the notification. + Filter *StorageGridFilter `json:"filter,omitempty" protobuf:"bytes,3,opt,name=filter"` +} + +// Filter represents filters to apply to bucket notifications for specifying constraints on objects +// +k8s:openapi-gen=true +type StorageGridFilter struct { + Prefix string `json:"prefix"` + Suffix string `json:"suffix"` +} + +// EventSourceStatus holds the status of the event-source resource +type EventSourceStatus struct { + CreatedAt metav1.Time `json:"createdAt,omitempty" protobuf:"bytes,1,opt,name=createdAt"` +} diff --git a/gateways/community/gcp-pubsub/cmd/main.go b/pkg/apis/eventsources/v1alpha1/validate.go similarity index 62% rename from gateways/community/gcp-pubsub/cmd/main.go rename to pkg/apis/eventsources/v1alpha1/validate.go index 53dc11c4bd..46c1112d29 100644 --- a/gateways/community/gcp-pubsub/cmd/main.go +++ b/pkg/apis/eventsources/v1alpha1/validate.go @@ -13,17 +13,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - -package main +package v1alpha1 import ( - "github.com/argoproj/argo-events/common" - "github.com/argoproj/argo-events/gateways" - "github.com/argoproj/argo-events/gateways/community/gcp-pubsub" + "github.com/pkg/errors" ) -func main() { - gateways.StartGateway(&pubsub.GcpPubSubEventSourceExecutor{ - Log: common.NewArgoEventsLogger(), - }) +// ValidateEventSource validates a generic event source +func ValidateEventSource(eventSource *EventSource) error { + if eventSource == nil { + return errors.New("event source can't be nil") + } + if eventSource.Spec == nil { + return errors.New("event source specification can't be nil") + } + return nil } diff --git a/pkg/apis/eventsources/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/eventsources/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..78cd2fd942 --- /dev/null +++ b/pkg/apis/eventsources/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,681 @@ +// +build !ignore_autogenerated + +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + json "encoding/json" + + common "github.com/argoproj/argo-events/common" + webhook "github.com/argoproj/argo-events/gateways/server/common/webhook" + apiscommon "github.com/argoproj/argo-events/pkg/apis/common" + v1 "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AMQPEventSource) DeepCopyInto(out *AMQPEventSource) { + *out = *in + if in.ConnectionBackoff != nil { + in, out := &in.ConnectionBackoff, &out.ConnectionBackoff + *out = new(common.Backoff) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AMQPEventSource. +func (in *AMQPEventSource) DeepCopy() *AMQPEventSource { + if in == nil { + return nil + } + out := new(AMQPEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CalendarEventSource) DeepCopyInto(out *CalendarEventSource) { + *out = *in + if in.ExclusionDates != nil { + in, out := &in.ExclusionDates, &out.ExclusionDates + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UserPayload != nil { + in, out := &in.UserPayload, &out.UserPayload + *out = new(json.RawMessage) + if **in != nil { + in, out := *in, *out + *out = make([]byte, len(*in)) + copy(*out, *in) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CalendarEventSource. +func (in *CalendarEventSource) DeepCopy() *CalendarEventSource { + if in == nil { + return nil + } + out := new(CalendarEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EventSource) DeepCopyInto(out *EventSource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Status.DeepCopyInto(&out.Status) + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(EventSourceSpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventSource. +func (in *EventSource) DeepCopy() *EventSource { + if in == nil { + return nil + } + out := new(EventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *EventSource) 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 *EventSourceList) DeepCopyInto(out *EventSourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]EventSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventSourceList. +func (in *EventSourceList) DeepCopy() *EventSourceList { + if in == nil { + return nil + } + out := new(EventSourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *EventSourceList) 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 *EventSourceSpec) DeepCopyInto(out *EventSourceSpec) { + *out = *in + if in.Minio != nil { + in, out := &in.Minio, &out.Minio + *out = make(map[string]apiscommon.S3Artifact, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Calendar != nil { + in, out := &in.Calendar, &out.Calendar + *out = make(map[string]CalendarEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.File != nil { + in, out := &in.File, &out.File + *out = make(map[string]FileEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Resource != nil { + in, out := &in.Resource, &out.Resource + *out = make(map[string]ResourceEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = make(map[string]webhook.Context, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.AMQP != nil { + in, out := &in.AMQP, &out.AMQP + *out = make(map[string]AMQPEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Kafka != nil { + in, out := &in.Kafka, &out.Kafka + *out = make(map[string]KafkaEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.MQTT != nil { + in, out := &in.MQTT, &out.MQTT + *out = make(map[string]MQTTEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.NATS != nil { + in, out := &in.NATS, &out.NATS + *out = make(map[string]NATSEventsSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.SNS != nil { + in, out := &in.SNS, &out.SNS + *out = make(map[string]SNSEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.SQS != nil { + in, out := &in.SQS, &out.SQS + *out = make(map[string]SQSEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.PubSub != nil { + in, out := &in.PubSub, &out.PubSub + *out = make(map[string]PubSubEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Github != nil { + in, out := &in.Github, &out.Github + *out = make(map[string]GithubEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Gitlab != nil { + in, out := &in.Gitlab, &out.Gitlab + *out = make(map[string]GitlabEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.HDFS != nil { + in, out := &in.HDFS, &out.HDFS + *out = make(map[string]HDFSEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Slack != nil { + in, out := &in.Slack, &out.Slack + *out = make(map[string]SlackEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.StorageGrid != nil { + in, out := &in.StorageGrid, &out.StorageGrid + *out = make(map[string]StorageGridEventSource, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventSourceSpec. +func (in *EventSourceSpec) DeepCopy() *EventSourceSpec { + if in == nil { + return nil + } + out := new(EventSourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EventSourceStatus) DeepCopyInto(out *EventSourceStatus) { + *out = *in + in.CreatedAt.DeepCopyInto(&out.CreatedAt) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventSourceStatus. +func (in *EventSourceStatus) DeepCopy() *EventSourceStatus { + if in == nil { + return nil + } + out := new(EventSourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileEventSource) DeepCopyInto(out *FileEventSource) { + *out = *in + out.WatchPathConfig = in.WatchPathConfig + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileEventSource. +func (in *FileEventSource) DeepCopy() *FileEventSource { + if in == nil { + return nil + } + out := new(FileEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GithubEventSource) DeepCopyInto(out *GithubEventSource) { + *out = *in + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(webhook.Context) + **out = **in + } + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.APIToken != nil { + in, out := &in.APIToken, &out.APIToken + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.WebhookSecret != nil { + in, out := &in.WebhookSecret, &out.WebhookSecret + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubEventSource. +func (in *GithubEventSource) DeepCopy() *GithubEventSource { + if in == nil { + return nil + } + out := new(GithubEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitlabEventSource) DeepCopyInto(out *GitlabEventSource) { + *out = *in + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(webhook.Context) + **out = **in + } + if in.AccessToken != nil { + in, out := &in.AccessToken, &out.AccessToken + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabEventSource. +func (in *GitlabEventSource) DeepCopy() *GitlabEventSource { + if in == nil { + return nil + } + out := new(GitlabEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HDFSEventSource) DeepCopyInto(out *HDFSEventSource) { + *out = *in + out.WatchPathConfig = in.WatchPathConfig + if in.Addresses != nil { + in, out := &in.Addresses, &out.Addresses + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KrbCCacheSecret != nil { + in, out := &in.KrbCCacheSecret, &out.KrbCCacheSecret + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.KrbKeytabSecret != nil { + in, out := &in.KrbKeytabSecret, &out.KrbKeytabSecret + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.KrbConfigConfigMap != nil { + in, out := &in.KrbConfigConfigMap, &out.KrbConfigConfigMap + *out = new(v1.ConfigMapKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HDFSEventSource. +func (in *HDFSEventSource) DeepCopy() *HDFSEventSource { + if in == nil { + return nil + } + out := new(HDFSEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaEventSource) DeepCopyInto(out *KafkaEventSource) { + *out = *in + if in.ConnectionBackoff != nil { + in, out := &in.ConnectionBackoff, &out.ConnectionBackoff + *out = new(common.Backoff) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaEventSource. +func (in *KafkaEventSource) DeepCopy() *KafkaEventSource { + if in == nil { + return nil + } + out := new(KafkaEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MQTTEventSource) DeepCopyInto(out *MQTTEventSource) { + *out = *in + if in.ConnectionBackoff != nil { + in, out := &in.ConnectionBackoff, &out.ConnectionBackoff + *out = new(common.Backoff) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MQTTEventSource. +func (in *MQTTEventSource) DeepCopy() *MQTTEventSource { + if in == nil { + return nil + } + out := new(MQTTEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NATSEventsSource) DeepCopyInto(out *NATSEventsSource) { + *out = *in + if in.ConnectionBackoff != nil { + in, out := &in.ConnectionBackoff, &out.ConnectionBackoff + *out = new(common.Backoff) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NATSEventsSource. +func (in *NATSEventsSource) DeepCopy() *NATSEventsSource { + if in == nil { + return nil + } + out := new(NATSEventsSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PubSubEventSource) DeepCopyInto(out *PubSubEventSource) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PubSubEventSource. +func (in *PubSubEventSource) DeepCopy() *PubSubEventSource { + if in == nil { + return nil + } + out := new(PubSubEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceEventSource) DeepCopyInto(out *ResourceEventSource) { + *out = *in + if in.Filter != nil { + in, out := &in.Filter, &out.Filter + *out = new(ResourceFilter) + (*in).DeepCopyInto(*out) + } + out.GroupVersionResource = in.GroupVersionResource + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceEventSource. +func (in *ResourceEventSource) DeepCopy() *ResourceEventSource { + if in == nil { + return nil + } + out := new(ResourceEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceFilter) DeepCopyInto(out *ResourceFilter) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Fields != nil { + in, out := &in.Fields, &out.Fields + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.CreatedBy.DeepCopyInto(&out.CreatedBy) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceFilter. +func (in *ResourceFilter) DeepCopy() *ResourceFilter { + if in == nil { + return nil + } + out := new(ResourceFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SNSEventSource) DeepCopyInto(out *SNSEventSource) { + *out = *in + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(webhook.Context) + **out = **in + } + if in.AccessKey != nil { + in, out := &in.AccessKey, &out.AccessKey + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.SecretKey != nil { + in, out := &in.SecretKey, &out.SecretKey + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SNSEventSource. +func (in *SNSEventSource) DeepCopy() *SNSEventSource { + if in == nil { + return nil + } + out := new(SNSEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQSEventSource) DeepCopyInto(out *SQSEventSource) { + *out = *in + if in.AccessKey != nil { + in, out := &in.AccessKey, &out.AccessKey + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.SecretKey != nil { + in, out := &in.SecretKey, &out.SecretKey + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQSEventSource. +func (in *SQSEventSource) DeepCopy() *SQSEventSource { + if in == nil { + return nil + } + out := new(SQSEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SlackEventSource) DeepCopyInto(out *SlackEventSource) { + *out = *in + if in.SigningSecret != nil { + in, out := &in.SigningSecret, &out.SigningSecret + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.Token != nil { + in, out := &in.Token, &out.Token + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(webhook.Context) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SlackEventSource. +func (in *SlackEventSource) DeepCopy() *SlackEventSource { + if in == nil { + return nil + } + out := new(SlackEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageGridEventSource) DeepCopyInto(out *StorageGridEventSource) { + *out = *in + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(webhook.Context) + **out = **in + } + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Filter != nil { + in, out := &in.Filter, &out.Filter + *out = new(StorageGridFilter) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageGridEventSource. +func (in *StorageGridEventSource) DeepCopy() *StorageGridEventSource { + if in == nil { + return nil + } + out := new(StorageGridEventSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageGridFilter) DeepCopyInto(out *StorageGridFilter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageGridFilter. +func (in *StorageGridFilter) DeepCopy() *StorageGridFilter { + if in == nil { + return nil + } + out := new(StorageGridFilter) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/gateway/v1alpha1/openapi_generated.go b/pkg/apis/gateway/v1alpha1/openapi_generated.go index 58aadfc89b..33be14ca54 100644 --- a/pkg/apis/gateway/v1alpha1/openapi_generated.go +++ b/pkg/apis/gateway/v1alpha1/openapi_generated.go @@ -22,15 +22,17 @@ limitations under the License. package v1alpha1 import ( - "github.com/go-openapi/spec" - "k8s.io/kube-openapi/pkg/common" + spec "github.com/go-openapi/spec" + common "k8s.io/kube-openapi/pkg/common" ) func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.EventSourceRef": schema_pkg_apis_gateway_v1alpha1_EventSourceRef(ref), "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.Gateway": schema_pkg_apis_gateway_v1alpha1_Gateway(ref), "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewayList": schema_pkg_apis_gateway_v1alpha1_GatewayList(ref), "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewayNotificationWatcher": schema_pkg_apis_gateway_v1alpha1_GatewayNotificationWatcher(ref), + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewayResource": schema_pkg_apis_gateway_v1alpha1_GatewayResource(ref), "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewaySpec": schema_pkg_apis_gateway_v1alpha1_GatewaySpec(ref), "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewayStatus": schema_pkg_apis_gateway_v1alpha1_GatewayStatus(ref), "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NodeStatus": schema_pkg_apis_gateway_v1alpha1_NodeStatus(ref), @@ -39,6 +41,34 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA } } +func schema_pkg_apis_gateway_v1alpha1_EventSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "EventSourceRef holds information about the EventSourceRef custom resource", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name of the event source", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace of the event source Default value is the namespace where referencing gateway is deployed", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + func schema_pkg_apis_gateway_v1alpha1_Gateway(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -178,6 +208,34 @@ func schema_pkg_apis_gateway_v1alpha1_GatewayNotificationWatcher(ref common.Refe } } +func schema_pkg_apis_gateway_v1alpha1_GatewayResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GatewayResource holds the metadata about the gateway resources", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "deployment": { + SchemaProps: spec.SchemaProps{ + Description: "Metadata of the deployment for the gateway", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "service": { + SchemaProps: spec.SchemaProps{ + Description: "Metadata of the service for the gateway", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + }, + Required: []string{"deployment"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + func schema_pkg_apis_gateway_v1alpha1_GatewaySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -191,11 +249,10 @@ func schema_pkg_apis_gateway_v1alpha1_GatewaySpec(ref common.ReferenceCallback) Ref: ref("k8s.io/api/core/v1.PodTemplateSpec"), }, }, - "eventSource": { + "eventSourceRef": { SchemaProps: spec.SchemaProps{ - Description: "EventSource is name of the configmap that stores event source configurations for the gateway", - Type: []string{"string"}, - Format: "", + Description: "EventSourceRef refers to event-source that stores event source configurations for the gateway", + Ref: ref("github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.EventSourceRef"), }, }, "type": { @@ -208,7 +265,7 @@ func schema_pkg_apis_gateway_v1alpha1_GatewaySpec(ref common.ReferenceCallback) "service": { SchemaProps: spec.SchemaProps{ Description: "Service is the specifications of the service to expose the gateway Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#service-v1-core", - Ref: ref("github.com/argoproj/argo-events/pkg/apis/common.ServiceTemplateSpec"), + Ref: ref("k8s.io/api/core/v1.Service"), }, }, "watchers": { @@ -230,12 +287,19 @@ func schema_pkg_apis_gateway_v1alpha1_GatewaySpec(ref common.ReferenceCallback) Ref: ref("github.com/argoproj/argo-events/pkg/apis/common.EventProtocol"), }, }, + "replica": { + SchemaProps: spec.SchemaProps{ + Description: "Replica is the gateway deployment replicas", + Type: []string{"integer"}, + Format: "int32", + }, + }, }, Required: []string{"template", "type", "processorPort", "eventProtocol"}, }, }, Dependencies: []string{ - "github.com/argoproj/argo-events/pkg/apis/common.EventProtocol", "github.com/argoproj/argo-events/pkg/apis/common.ServiceTemplateSpec", "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NotificationWatchers", "k8s.io/api/core/v1.PodTemplateSpec"}, + "github.com/argoproj/argo-events/pkg/apis/common.EventProtocol", "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.EventSourceRef", "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NotificationWatchers", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/api/core/v1.Service"}, } } @@ -280,12 +344,18 @@ func schema_pkg_apis_gateway_v1alpha1_GatewayStatus(ref common.ReferenceCallback }, }, }, + "resources": { + SchemaProps: spec.SchemaProps{ + Description: "Resources refers to the metadata about the gateway resources", + Ref: ref("github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewayResource"), + }, + }, }, - Required: []string{"phase"}, + Required: []string{"phase", "resources"}, }, }, Dependencies: []string{ - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NodeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.GatewayResource", "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1.NodeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/pkg/apis/gateway/v1alpha1/types.go b/pkg/apis/gateway/v1alpha1/types.go index 3e1c652f4f..7f4a69befe 100644 --- a/pkg/apis/gateway/v1alpha1/types.go +++ b/pkg/apis/gateway/v1alpha1/types.go @@ -17,14 +17,11 @@ limitations under the License. package v1alpha1 import ( - "github.com/argoproj/argo-events/pkg/apis/common" + apicommon "github.com/argoproj/argo-events/pkg/apis/common" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// Gateway version -const ArgoEventsGatewayVersion = "v0.11" - // NodePhase is the label for the condition of a node. type NodePhase string @@ -63,44 +60,58 @@ type GatewaySpec struct { // Template is the pod specification for the gateway // Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core Template *corev1.PodTemplateSpec `json:"template" protobuf:"bytes,1,opt,name=template"` - - // EventSource is name of the configmap that stores event source configurations for the gateway - EventSource string `json:"eventSource,omitempty" protobuf:"bytes,2,opt,name=eventSource"` - + // EventSourceRef refers to event-source that stores event source configurations for the gateway + EventSourceRef *EventSourceRef `json:"eventSourceRef,omitempty" protobuf:"bytes,2,opt,name=eventSourceRef"` // Type is the type of gateway. Used as metadata. - Type string `json:"type" protobuf:"bytes,3,opt,name=type"` - + Type apicommon.EventSourceType `json:"type" protobuf:"bytes,3,opt,name=type"` // Service is the specifications of the service to expose the gateway // Refer https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#service-v1-core - Service *common.ServiceTemplateSpec `json:"service,omitempty" protobuf:"bytes,4,opt,name=service"` - + Service *corev1.Service `json:"service,omitempty" protobuf:"bytes,4,opt,name=service"` // Watchers are components which are interested listening to notifications from this gateway // These only need to be specified when gateway dispatch mechanism is through HTTP POST notifications. // In future, support for NATS, KAFKA will be added as a means to dispatch notifications in which case // specifying watchers would be unnecessary. Watchers *NotificationWatchers `json:"watchers,omitempty" protobuf:"bytes,5,opt,name=watchers"` - // Port on which the gateway event source processor is running on. ProcessorPort string `json:"processorPort" protobuf:"bytes,6,opt,name=processorPort"` - // EventProtocol is the underlying protocol used to send events from gateway to watchers(components interested in listening to event from this gateway) - EventProtocol *common.EventProtocol `json:"eventProtocol" protobuf:"bytes,7,opt,name=eventProtocol"` + EventProtocol *apicommon.EventProtocol `json:"eventProtocol" protobuf:"bytes,7,opt,name=eventProtocol"` + // Replica is the gateway deployment replicas + Replica int `json:"replica,omitempty" protobuf:"bytes,9,opt,name=replica"` +} + +// EventSourceRef holds information about the EventSourceRef custom resource +type EventSourceRef struct { + // Name of the event source + Name string `json:"name" protobuf:"bytes,1,name=name"` + // Namespace of the event source + // Default value is the namespace where referencing gateway is deployed + // +optional + Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` +} + +// GatewayResource holds the metadata about the gateway resources +type GatewayResource struct { + // Metadata of the deployment for the gateway + Deployment *metav1.ObjectMeta `json:"deployment" protobuf:"bytes,1,name=deployment"` + // Metadata of the service for the gateway + // +optional + Service *metav1.ObjectMeta `json:"service,omitempty" protobuf:"bytes,2,opt,name=service"` } // GatewayStatus contains information about the status of a gateway. type GatewayStatus struct { // Phase is the high-level summary of the gateway Phase NodePhase `json:"phase" protobuf:"bytes,1,opt,name=phase"` - // StartedAt is the time at which this gateway was initiated StartedAt metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,2,opt,name=startedAt"` - // Message is a human readable string indicating details about a gateway in its phase Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"` - // Nodes is a mapping between a node ID and the node's status // it records the states for the configurations of gateway. Nodes map[string]NodeStatus `json:"nodes,omitempty" protobuf:"bytes,5,rep,name=nodes"` + // Resources refers to the metadata about the gateway resources + Resources *GatewayResource `json:"resources" protobuf:"bytes,6,opt,name=resources"` } // NodeStatus describes the status for an individual node in the gateway configurations. @@ -109,23 +120,17 @@ type NodeStatus struct { // ID is a unique identifier of a node within a sensor // It is a hash of the node name ID string `json:"id" protobuf:"bytes,1,opt,name=id"` - // Name is a unique name in the node tree used to generate the node ID Name string `json:"name" protobuf:"bytes,3,opt,name=name"` - // DisplayName is the human readable representation of the node DisplayName string `json:"displayName" protobuf:"bytes,5,opt,name=displayName"` - // Phase of the node Phase NodePhase `json:"phase" protobuf:"bytes,6,opt,name=phase"` - // StartedAt is the time at which this node started // +k8s:openapi-gen=false StartedAt metav1.MicroTime `json:"startedAt,omitempty" protobuf:"bytes,7,opt,name=startedAt"` - // Message store data or something to save for configuration Message string `json:"message,omitempty" protobuf:"bytes,8,opt,name=message"` - // UpdateTime is the time when node(gateway configuration) was updated UpdateTime metav1.MicroTime `json:"updateTime,omitempty" protobuf:"bytes,9,opt,name=updateTime"` } @@ -135,7 +140,6 @@ type NotificationWatchers struct { // +listType=gateways // Gateways is the list of gateways interested in listening to notifications from this gateway Gateways []GatewayNotificationWatcher `json:"gateways,omitempty" protobuf:"bytes,1,opt,name=gateways"` - // +listType=sensors // Sensors is the list of sensors interested in listening to notifications from this gateway Sensors []SensorNotificationWatcher `json:"sensors,omitempty" protobuf:"bytes,2,rep,name=sensors"` @@ -145,14 +149,11 @@ type NotificationWatchers struct { type GatewayNotificationWatcher struct { // Name is the gateway name Name string `json:"name" protobuf:"bytes,1,name=name"` - // Port is http server port on which gateway is running Port string `json:"port" protobuf:"bytes,2,name=port"` - // Endpoint is REST API endpoint to post event to. // Events are sent using HTTP POST method to this endpoint. Endpoint string `json:"endpoint" protobuf:"bytes,3,name=endpoint"` - // Namespace of the gateway // +Optional Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"` @@ -162,7 +163,6 @@ type GatewayNotificationWatcher struct { type SensorNotificationWatcher struct { // Name is the name of the sensor Name string `json:"name" protobuf:"bytes,1,name=name"` - // Namespace of the sensor // +Optional Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` diff --git a/pkg/apis/gateway/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/gateway/v1alpha1/zz_generated.deepcopy.go index 5b006677b8..c0a8582ecf 100644 --- a/pkg/apis/gateway/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/gateway/v1alpha1/zz_generated.deepcopy.go @@ -20,11 +20,28 @@ limitations under the License. package v1alpha1 import ( - "github.com/argoproj/argo-events/pkg/apis/common" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" + common "github.com/argoproj/argo-events/pkg/apis/common" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EventSourceRef) DeepCopyInto(out *EventSourceRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventSourceRef. +func (in *EventSourceRef) DeepCopy() *EventSourceRef { + if in == nil { + return nil + } + out := new(EventSourceRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Gateway) DeepCopyInto(out *Gateway) { *out = *in @@ -102,17 +119,48 @@ func (in *GatewayNotificationWatcher) DeepCopy() *GatewayNotificationWatcher { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayResource) DeepCopyInto(out *GatewayResource) { + *out = *in + if in.Deployment != nil { + in, out := &in.Deployment, &out.Deployment + *out = new(v1.ObjectMeta) + (*in).DeepCopyInto(*out) + } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(v1.ObjectMeta) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayResource. +func (in *GatewayResource) DeepCopy() *GatewayResource { + if in == nil { + return nil + } + out := new(GatewayResource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewaySpec) DeepCopyInto(out *GatewaySpec) { *out = *in if in.Template != nil { in, out := &in.Template, &out.Template - *out = new(v1.PodTemplateSpec) + *out = new(corev1.PodTemplateSpec) (*in).DeepCopyInto(*out) } + if in.EventSourceRef != nil { + in, out := &in.EventSourceRef, &out.EventSourceRef + *out = new(EventSourceRef) + **out = **in + } if in.Service != nil { in, out := &in.Service, &out.Service - *out = new(common.ServiceTemplateSpec) + *out = new(corev1.Service) (*in).DeepCopyInto(*out) } if in.Watchers != nil { @@ -149,6 +197,11 @@ func (in *GatewayStatus) DeepCopyInto(out *GatewayStatus) { (*out)[key] = *val.DeepCopy() } } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(GatewayResource) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/sensor/v1alpha1/openapi_generated.go b/pkg/apis/sensor/v1alpha1/openapi_generated.go index e167bf7c61..281f67055b 100644 --- a/pkg/apis/sensor/v1alpha1/openapi_generated.go +++ b/pkg/apis/sensor/v1alpha1/openapi_generated.go @@ -42,6 +42,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.NodeStatus": schema_pkg_apis_sensor_v1alpha1_NodeStatus(ref), "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.Sensor": schema_pkg_apis_sensor_v1alpha1_Sensor(ref), "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.SensorList": schema_pkg_apis_sensor_v1alpha1_SensorList(ref), + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.SensorResources": schema_pkg_apis_sensor_v1alpha1_SensorResources(ref), "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.SensorSpec": schema_pkg_apis_sensor_v1alpha1_SensorSpec(ref), "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.SensorStatus": schema_pkg_apis_sensor_v1alpha1_SensorStatus(ref), "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.TimeFilter": schema_pkg_apis_sensor_v1alpha1_TimeFilter(ref), @@ -60,43 +61,43 @@ func schema_pkg_apis_sensor_v1alpha1_ArtifactLocation(ref common.ReferenceCallba return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "ArtifactLocation describes the source location for an external artifact", + Description: "ArtifactLocation describes the source location for an external minio", Type: []string{"object"}, Properties: map[string]spec.Schema{ "s3": { SchemaProps: spec.SchemaProps{ - Description: "S3 compliant artifact", + Description: "S3 compliant minio", Ref: ref("github.com/argoproj/argo-events/pkg/apis/common.S3Artifact"), }, }, "inline": { SchemaProps: spec.SchemaProps{ - Description: "Inline artifact is embedded in sensor spec as a string", + Description: "Inline minio is embedded in sensor spec as a string", Type: []string{"string"}, Format: "", }, }, "file": { SchemaProps: spec.SchemaProps{ - Description: "File artifact is artifact stored in a file", + Description: "File minio is minio stored in a file", Ref: ref("github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.FileArtifact"), }, }, "url": { SchemaProps: spec.SchemaProps{ - Description: "URL to fetch the artifact from", + Description: "URL to fetch the minio from", Ref: ref("github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.URLArtifact"), }, }, "configmap": { SchemaProps: spec.SchemaProps{ - Description: "Configmap that stores the artifact", + Description: "Configmap that stores the minio", Ref: ref("github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.ConfigmapArtifact"), }, }, "git": { SchemaProps: spec.SchemaProps{ - Description: "Git repository hosting the artifact", + Description: "Git repository hosting the minio", Ref: ref("github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.GitArtifact"), }, }, @@ -160,7 +161,7 @@ func schema_pkg_apis_sensor_v1alpha1_ConfigmapArtifact(ref common.ReferenceCallb return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "ConfigmapArtifact contains information about artifact in k8 configmap", + Description: "ConfigmapArtifact contains information about minio in k8 configmap", Type: []string{"object"}, Properties: map[string]spec.Schema{ "name": { @@ -371,7 +372,7 @@ func schema_pkg_apis_sensor_v1alpha1_FileArtifact(ref common.ReferenceCallback) return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "FileArtifact contains information about an artifact in a filesystem", + Description: "FileArtifact contains information about an minio in a filesystem", Type: []string{"object"}, Properties: map[string]spec.Schema{ "path": { @@ -390,7 +391,7 @@ func schema_pkg_apis_sensor_v1alpha1_GitArtifact(ref common.ReferenceCallback) c return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "GitArtifact contains information about an artifact stored in git", + Description: "GitArtifact contains information about an minio stored in git", Type: []string{"object"}, Properties: map[string]spec.Schema{ "url": { @@ -709,6 +710,34 @@ func schema_pkg_apis_sensor_v1alpha1_SensorList(ref common.ReferenceCallback) co } } +func schema_pkg_apis_sensor_v1alpha1_SensorResources(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SensorResources holds the metadata of the resources created for the sensor", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "deployment": { + SchemaProps: spec.SchemaProps{ + Description: "Deployment holds the metadata of the deployment for the sensor", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "service": { + SchemaProps: spec.SchemaProps{ + Description: "Service holds the metadata of the service for the sensor", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + }, + Required: []string{"deployment"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + func schema_pkg_apis_sensor_v1alpha1_SensorSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -872,12 +901,18 @@ func schema_pkg_apis_sensor_v1alpha1_SensorStatus(ref common.ReferenceCallback) Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), }, }, + "resources": { + SchemaProps: spec.SchemaProps{ + Description: "Resources refers to metadata of the resources created for the sensor", + Ref: ref("github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.SensorResources"), + }, + }, }, - Required: []string{"phase", "triggerCycleStatus", "lastCycleTime"}, + Required: []string{"phase", "triggerCycleStatus", "lastCycleTime", "resources"}, }, }, Dependencies: []string{ - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.NodeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.NodeStatus", "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1.SensorResources", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } @@ -1212,7 +1247,7 @@ func schema_pkg_apis_sensor_v1alpha1_URLArtifact(ref common.ReferenceCallback) c return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "URLArtifact contains information about an artifact at an http endpoint.", + Description: "URLArtifact contains information about an minio at an http endpoint.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "path": { diff --git a/pkg/apis/sensor/v1alpha1/types.go b/pkg/apis/sensor/v1alpha1/types.go index 0182c247a3..be482ca4f1 100644 --- a/pkg/apis/sensor/v1alpha1/types.go +++ b/pkg/apis/sensor/v1alpha1/types.go @@ -28,8 +28,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const ArgoEventsSensorVersion = "v0.11" - // NotificationType represent a type of notifications that are handled by a sensor type NotificationType string @@ -98,24 +96,18 @@ type SensorSpec struct { // +listType=dependencies // Dependencies is a list of the events that this sensor is dependent on. Dependencies []EventDependency `json:"dependencies" protobuf:"bytes,1,rep,name=dependencies"` - // +listType=triggers // Triggers is a list of the things that this sensor evokes. These are the outputs from this sensor. Triggers []Trigger `json:"triggers" protobuf:"bytes,2,rep,name=triggers"` - // Template contains sensor pod specification. For more information, read https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core Template *corev1.PodTemplateSpec `json:"template" protobuf:"bytes,3,name=template"` - // EventProtocol is the protocol through which sensor receives events from gateway EventProtocol *apicommon.EventProtocol `json:"eventProtocol" protobuf:"bytes,4,name=eventProtocol"` - // Circuit is a boolean expression of dependency groups Circuit string `json:"circuit,omitempty" protobuf:"bytes,5,rep,name=circuit"` - // +listType=dependencyGroups // DependencyGroups is a list of the groups of events. DependencyGroups []DependencyGroup `json:"dependencyGroups,omitempty" protobuf:"bytes,6,rep,name=dependencyGroups"` - // ErrorOnFailedRound if set to true, marks sensor state as `error` if the previous trigger round fails. // Once sensor state is set to `error`, no further triggers will be processed. ErrorOnFailedRound bool `json:"errorOnFailedRound,omitempty" protobuf:"bytes,7,opt,name=errorOnFailedRound"` @@ -125,10 +117,8 @@ type SensorSpec struct { type EventDependency struct { // Name is a unique name of this dependency Name string `json:"name" protobuf:"bytes,1,name=name"` - // Filters and rules governing tolerations of success and constraints on the context and data of an event Filters EventDependencyFilter `json:"filters,omitempty" protobuf:"bytes,2,opt,name=filters"` - // Connected tells if subscription is already setup in case of nats protocol. Connected bool `json:"connected,omitempty" protobuf:"bytes,3,opt,name=connected"` } @@ -146,13 +136,10 @@ type DependencyGroup struct { type EventDependencyFilter struct { // Name is the name of event filter Name string `json:"name" protobuf:"bytes,1,name=name"` - // Time filter on the event with escalation Time *TimeFilter `json:"time,omitempty" protobuf:"bytes,2,opt,name=time"` - // Context filter constraints with escalation Context *common.EventContext `json:"context,omitempty" protobuf:"bytes,3,opt,name=context"` - // +listType=data // Data filter constraints with escalation Data []DataFilter `json:"data,omitempty" protobuf:"bytes,4,opt,name=data"` @@ -167,7 +154,6 @@ type TimeFilter struct { // Before this time, events for this event are ignored and // format is hh:mm:ss Start string `json:"start,omitempty" protobuf:"bytes,1,opt,name=start"` - // StopPattern is the end of a time window. // After this time, events for this event are ignored and // format is hh:mm:ss @@ -193,15 +179,13 @@ type DataFilter struct { // To access an array value use the index as the key. The dot and wildcard characters can be escaped with '\\'. // See https://github.com/tidwall/gjson#path-syntax for more information on how to use this. Path string `json:"path" protobuf:"bytes,1,opt,name=path"` - // Type contains the JSON type of the data Type JSONType `json:"type" protobuf:"bytes,2,opt,name=type"` - // +listType=value // Value is the allowed string values for this key // Booleans are passed using strconv.ParseBool() // Numbers are parsed using as float64 using strconv.ParseFloat() - // Strings are treated as regular expressions + // Strings are taken as is // Nils this value is ignored Value []string `json:"value" protobuf:"bytes,3,rep,name=value"` } @@ -210,15 +194,12 @@ type DataFilter struct { type Trigger struct { // Template describes the trigger specification. Template *TriggerTemplate `json:"template" protobuf:"bytes,1,name=template"` - // +listType=templateParameters // TemplateParameters is the list of resource parameters to pass to the template object TemplateParameters []TriggerParameter `json:"templateParameters,omitempty" protobuf:"bytes,2,rep,name=templateParameters"` - // +listType=resourceParameters // ResourceParameters is the list of resource parameters to pass to resolved resource object in template object ResourceParameters []TriggerParameter `json:"resourceParameters,omitempty" protobuf:"bytes,3,rep,name=resourceParameters"` - // Policy to configure backoff and execution criteria for the trigger Policy *TriggerPolicy `json:"policy" protobuf:"bytes,4,opt,name=policy"` } @@ -227,13 +208,10 @@ type Trigger struct { type TriggerTemplate struct { // Name is a unique name of the action to take Name string `json:"name" protobuf:"bytes,1,name=name"` - // When is the condition to execute the trigger When *TriggerCondition `json:"when,omitempty" protobuf:"bytes,2,opt,name=when"` - // The unambiguous kind of this object - used in order to retrieve the appropriate kubernetes api client for this resource *metav1.GroupVersionResource `json:",inline" protobuf:"bytes,3,opt,name=groupVersionResource"` - // Source of the K8 resource file(s) Source *ArtifactLocation `json:"source" protobuf:"bytes,4,opt,name=source"` } @@ -244,7 +222,6 @@ type TriggerCondition struct { // +listType=any // Any acts as a OR operator between dependencies Any []string `json:"any,omitempty" protobuf:"bytes,1,rep,name=any"` - // +listType=all // All acts as a AND operator between dependencies All []string `json:"all,omitempty" protobuf:"bytes,2,rep,name=all"` @@ -269,13 +246,11 @@ const ( type TriggerParameter struct { // Src contains a source reference to the value of the parameter from a event event Src *TriggerParameterSource `json:"src" protobuf:"bytes,1,name=src"` - // Dest is the JSONPath of a resource key. // A path is a series of keys separated by a dot. The colon character can be escaped with '.' // The -1 key can be used to append a value to an existing array. // See https://github.com/tidwall/sjson#path-syntax for more information about how this is used. Dest string `json:"dest" protobuf:"bytes,2,name=dest"` - // Operation is what to do with the existing value at Dest, whether to // 'prepend', 'overwrite', or 'append' it. Operation TriggerParameterOperation `json:"operation,omitempty" protobuf:"bytes,3,opt,name=operation"` @@ -285,13 +260,11 @@ type TriggerParameter struct { type TriggerParameterSource struct { // Event is the name of the event for which to retrieve this event Event string `json:"event" protobuf:"bytes,1,opt,name=event"` - // Path is the JSONPath of the event's (JSON decoded) data key // Path is a series of keys separated by a dot. A key may contain wildcard characters '*' and '?'. // To access an array value use the index as the key. The dot and wildcard characters can be escaped with '\\'. // See https://github.com/tidwall/gjson#path-syntax for more information on how to use this. Path string `json:"path" protobuf:"bytes,2,opt,name=path"` - // Value is the default literal value to use for this parameter source // This is only used if the path is invalid. // If the path is invalid and this is not defined, this param source will produce an error. @@ -302,10 +275,8 @@ type TriggerParameterSource struct { type TriggerPolicy struct { // Backoff before checking resource state Backoff Backoff `json:"backoff" protobuf:"bytes,1,opt,name=backoff"` - // State refers to labels used to check the resource state State *TriggerStateLabels `json:"state" protobuf:"bytes,2,opt,name=state"` - // ErrorOnBackoffTimeout determines whether sensor should transition to error state if the backoff times out and yet the resource neither transitioned into success or failure. ErrorOnBackoffTimeout bool `json:"errorOnBackoffTimeout" protobuf:"bytes,3,opt,name=errorOnBackoffTimeout"` } @@ -314,13 +285,10 @@ type TriggerPolicy struct { type Backoff struct { // Duration is the duration in nanoseconds Duration time.Duration `json:"duration" protobuf:"bytes,1,opt,name=duration"` - // Duration is multiplied by factor each iteration Factor float64 `json:"factor" protobuf:"bytes,2,opt,name=factor"` - // The amount of jitter applied each iteration Jitter float64 `json:"jitter" protobuf:"bytes,3,opt,name=jitter"` - // Exit with error after this many steps Steps int `json:"steps" protobuf:"bytes,4,opt,name=steps"` } @@ -329,37 +297,40 @@ type Backoff struct { type TriggerStateLabels struct { // Success defines labels required to identify a resource in success state Success map[string]string `json:"success" protobuf:"bytes,1,opt,name=success"` - // Failure defines labels required to identify a resource in failed state Failure map[string]string `json:"failure" protobuf:"bytes,2,opt,name=failure"` } +// SensorResources holds the metadata of the resources created for the sensor +type SensorResources struct { + // Deployment holds the metadata of the deployment for the sensor + Deployment *metav1.ObjectMeta `json:"deployment" protobuf:"bytes,1,name=deployment"` + // Service holds the metadata of the service for the sensor + // +optional + Service *metav1.ObjectMeta `json:"service,omitempty" protobuf:"bytes,2,opt,name=service"` +} + // SensorStatus contains information about the status of a sensor. type SensorStatus struct { // Phase is the high-level summary of the sensor Phase NodePhase `json:"phase" protobuf:"bytes,1,opt,name=phase"` - // StartedAt is the time at which this sensor was initiated StartedAt metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,2,opt,name=startedAt"` - // CompletedAt is the time at which this sensor was completed CompletedAt metav1.Time `json:"completedAt,omitempty" protobuf:"bytes,3,opt,name=completedAt"` - // Message is a human readable string indicating details about a sensor in its phase Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"` - // Nodes is a mapping between a node ID and the node's status // it records the states for the FSM of this sensor. Nodes map[string]NodeStatus `json:"nodes,omitempty" protobuf:"bytes,5,rep,name=nodes"` - // TriggerCycleCount is the count of sensor's trigger cycle runs. TriggerCycleCount int32 `json:"triggerCycleCount,omitempty" protobuf:"varint,6,opt,name=triggerCycleCount"` - // TriggerCycleState is the status from last cycle of triggers execution. TriggerCycleStatus TriggerCycleState `json:"triggerCycleStatus" protobuf:"bytes,7,opt,name=triggerCycleStatus"` - // LastCycleTime is the time when last trigger cycle completed LastCycleTime metav1.Time `json:"lastCycleTime" protobuf:"bytes,8,opt,name=lastCycleTime"` + // Resources refers to metadata of the resources created for the sensor + Resources *SensorResources `json:"resources" protobuf:"bytes,9,name=resources"` } // NodeStatus describes the status for an individual node in the sensor's FSM. @@ -368,120 +339,94 @@ type NodeStatus struct { // ID is a unique identifier of a node within a sensor // It is a hash of the node name ID string `json:"id" protobuf:"bytes,1,opt,name=id"` - // Name is a unique name in the node tree used to generate the node ID Name string `json:"name" protobuf:"bytes,2,opt,name=name"` - // DisplayName is the human readable representation of the node DisplayName string `json:"displayName" protobuf:"bytes,3,opt,name=displayName"` - // Type is the type of the node Type NodeType `json:"type" protobuf:"bytes,4,opt,name=type"` - // Phase of the node Phase NodePhase `json:"phase" protobuf:"bytes,5,opt,name=phase"` - // StartedAt is the time at which this node started StartedAt metav1.MicroTime `json:"startedAt,omitempty" protobuf:"bytes,6,opt,name=startedAt"` - // CompletedAt is the time at which this node completed CompletedAt metav1.MicroTime `json:"completedAt,omitempty" protobuf:"bytes,7,opt,name=completedAt"` - // store data or something to save for event notifications or trigger events Message string `json:"message,omitempty" protobuf:"bytes,8,opt,name=message"` - // Event stores the last seen event for this node Event *apicommon.Event `json:"event,omitempty" protobuf:"bytes,9,opt,name=event"` } -// ArtifactLocation describes the source location for an external artifact +// ArtifactLocation describes the source location for an external minio type ArtifactLocation struct { - // S3 compliant artifact + // S3 compliant minio S3 *apicommon.S3Artifact `json:"s3,omitempty" protobuf:"bytes,1,opt,name=s3"` - - // Inline artifact is embedded in sensor spec as a string + // Inline minio is embedded in sensor spec as a string Inline *string `json:"inline,omitempty" protobuf:"bytes,2,opt,name=inline"` - - // File artifact is artifact stored in a file + // File minio is minio stored in a file File *FileArtifact `json:"file,omitempty" protobuf:"bytes,3,opt,name=file"` - - // URL to fetch the artifact from + // URL to fetch the minio from URL *URLArtifact `json:"url,omitempty" protobuf:"bytes,4,opt,name=url"` - - // Configmap that stores the artifact + // Configmap that stores the minio Configmap *ConfigmapArtifact `json:"configmap,omitempty" protobuf:"bytes,5,opt,name=configmap"` - - // Git repository hosting the artifact + // Git repository hosting the minio Git *GitArtifact `json:"git,omitempty" protobuf:"bytes,6,opt,name=git"` - // Resource is generic template for K8s resource Resource *unstructured.Unstructured `json:"resource,omitempty" protobuf:"bytes,7,opt,name=resource"` } -// ConfigmapArtifact contains information about artifact in k8 configmap +// ConfigmapArtifact contains information about minio in k8 configmap type ConfigmapArtifact struct { // Name of the configmap Name string `json:"name" protobuf:"bytes,1,name=name"` - // Namespace where configmap is deployed Namespace string `json:"namespace" protobuf:"bytes,2,name=namespace"` - // Key within configmap data which contains trigger resource definition Key string `json:"key" protobuf:"bytes,3,name=key"` } -// FileArtifact contains information about an artifact in a filesystem +// FileArtifact contains information about an minio in a filesystem type FileArtifact struct { Path string `json:"path,omitempty" protobuf:"bytes,1,opt,name=path"` } -// URLArtifact contains information about an artifact at an http endpoint. +// URLArtifact contains information about an minio at an http endpoint. type URLArtifact struct { // Path is the complete URL Path string `json:"path" protobuf:"bytes,1,name=path"` - // VerifyCert decides whether the connection is secure or not VerifyCert bool `json:"verifyCert,omitempty" protobuf:"bytes,2,opt,name=verifyCert"` } -// GitArtifact contains information about an artifact stored in git +// GitArtifact contains information about an minio stored in git type GitArtifact struct { // Git URL URL string `json:"url" protobuf:"bytes,1,name=url"` - // Directory to clone the repository. We clone complete directory because GitArtifact is not limited to any specific Git service providers. // Hence we don't use any specific git provider client. CloneDirectory string `json:"cloneDirectory" protobuf:"bytes,2,name=cloneDirectory"` - // Creds contain reference to git username and password // +optional Creds *GitCreds `json:"creds,omitempty" protobuf:"bytes,3,opt,name=creds"` - // Namespace where creds are stored. // +optional Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"` - // SSHKeyPath is path to your ssh key path. Use this if you don't want to provide username and password. // ssh key path must be mounted in sensor pod. // +optional SSHKeyPath string `json:"sshKeyPath,omitempty" protobuf:"bytes,5,opt,name=sshKeyPath"` - // Path to file that contains trigger resource definition FilePath string `json:"filePath" protobuf:"bytes,6,name=filePath"` - // Branch to use to pull trigger resource // +optional Branch string `json:"branch,omitempty" protobuf:"bytes,7,opt,name=branch"` - // Tag to use to pull trigger resource // +optional Tag string `json:"tag,omitempty" protobuf:"bytes,8,opt,name=tag"` - // Ref to use to pull trigger resource. Will result in a shallow clone and // fetch. // +optional Ref string `json:"ref,omitempty" protobuf:"bytes,9,opt,name=ref"` - // Remote to manage set of tracked repositories. Defaults to "origin". // Refer https://git-scm.com/docs/git-remote // +optional @@ -492,7 +437,6 @@ type GitArtifact struct { type GitRemoteConfig struct { // Name of the remote to fetch from. Name string `json:"name" protobuf:"bytes,1,name=name"` - // +listType=urls // URLs the URLs of a remote repository. It must be non-empty. Fetch will // always use the first URL, while push will use all of them. @@ -505,7 +449,7 @@ type GitCreds struct { Password *corev1.SecretKeySelector `json:"password" protobuf:"bytes,2,opt,name=password"` } -// HasLocation whether or not an artifact has a location defined +// HasLocation whether or not an minio has a location defined func (a *ArtifactLocation) HasLocation() bool { return a.S3 != nil || a.Inline != nil || a.File != nil || a.URL != nil } diff --git a/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go index e638866a17..89167cf8c8 100644 --- a/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go @@ -20,10 +20,10 @@ limitations under the License. package v1alpha1 import ( - "github.com/argoproj/argo-events/pkg/apis/common" + common "github.com/argoproj/argo-events/pkg/apis/common" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -373,6 +373,32 @@ func (in *SensorList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SensorResources) DeepCopyInto(out *SensorResources) { + *out = *in + if in.Deployment != nil { + in, out := &in.Deployment, &out.Deployment + *out = new(metav1.ObjectMeta) + (*in).DeepCopyInto(*out) + } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(metav1.ObjectMeta) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SensorResources. +func (in *SensorResources) DeepCopy() *SensorResources { + if in == nil { + return nil + } + out := new(SensorResources) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SensorSpec) DeepCopyInto(out *SensorSpec) { *out = *in @@ -433,6 +459,11 @@ func (in *SensorStatus) DeepCopyInto(out *SensorStatus) { } } in.LastCycleTime.DeepCopyInto(&out.LastCycleTime) + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(SensorResources) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/client/eventsources/clientset/versioned/clientset.go b/pkg/client/eventsources/clientset/versioned/clientset.go new file mode 100644 index 0000000000..334969a3f4 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/clientset.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + ArgoprojV1alpha1() argoprojv1alpha1.ArgoprojV1alpha1Interface +} + +// Clientset contains the clients for groups. Each group has exactly one +// version included in a Clientset. +type Clientset struct { + *discovery.DiscoveryClient + argoprojV1alpha1 *argoprojv1alpha1.ArgoprojV1alpha1Client +} + +// ArgoprojV1alpha1 retrieves the ArgoprojV1alpha1Client +func (c *Clientset) ArgoprojV1alpha1() argoprojv1alpha1.ArgoprojV1alpha1Interface { + return c.argoprojV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + var cs Clientset + var err error + cs.argoprojV1alpha1, err = argoprojv1alpha1.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + var cs Clientset + cs.argoprojV1alpha1 = argoprojv1alpha1.NewForConfigOrDie(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) + return &cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.argoprojV1alpha1 = argoprojv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/pkg/client/eventsources/clientset/versioned/doc.go b/pkg/client/eventsources/clientset/versioned/doc.go new file mode 100644 index 0000000000..4fe3ab760c --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated clientset. +package versioned diff --git a/pkg/client/eventsources/clientset/versioned/fake/clientset_generated.go b/pkg/client/eventsources/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 0000000000..2ea0313bdc --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,81 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned" + argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1" + fakeargoprojv1alpha1 "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var _ clientset.Interface = &Clientset{} + +// ArgoprojV1alpha1 retrieves the ArgoprojV1alpha1Client +func (c *Clientset) ArgoprojV1alpha1() argoprojv1alpha1.ArgoprojV1alpha1Interface { + return &fakeargoprojv1alpha1.FakeArgoprojV1alpha1{Fake: &c.Fake} +} diff --git a/pkg/client/eventsources/clientset/versioned/fake/doc.go b/pkg/client/eventsources/clientset/versioned/fake/doc.go new file mode 100644 index 0000000000..3695dbac63 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/fake/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/pkg/client/eventsources/clientset/versioned/fake/register.go b/pkg/client/eventsources/clientset/versioned/fake/register.go new file mode 100644 index 0000000000..5eba7f7f46 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/fake/register.go @@ -0,0 +1,55 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) +var parameterCodec = runtime.NewParameterCodec(scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + argoprojv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/pkg/client/eventsources/clientset/versioned/scheme/doc.go b/pkg/client/eventsources/clientset/versioned/scheme/doc.go new file mode 100644 index 0000000000..3ca1386b63 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/scheme/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/pkg/client/eventsources/clientset/versioned/scheme/register.go b/pkg/client/eventsources/clientset/versioned/scheme/register.go new file mode 100644 index 0000000000..3db4d70d1e --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/scheme/register.go @@ -0,0 +1,55 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + argoprojv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/doc.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/doc.go new file mode 100644 index 0000000000..7f038ec4b0 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsource.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsource.go new file mode 100644 index 0000000000..a6f5049dfc --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsource.go @@ -0,0 +1,190 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "time" + + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + scheme "github.com/argoproj/argo-events/pkg/client/eventsources/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" +) + +// EventSourcesGetter has a method to return a EventSourceInterface. +// A group's client should implement this interface. +type EventSourcesGetter interface { + EventSources(namespace string) EventSourceInterface +} + +// EventSourceInterface has methods to work with EventSource resources. +type EventSourceInterface interface { + Create(*v1alpha1.EventSource) (*v1alpha1.EventSource, error) + Update(*v1alpha1.EventSource) (*v1alpha1.EventSource, error) + UpdateStatus(*v1alpha1.EventSource) (*v1alpha1.EventSource, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.EventSource, error) + List(opts v1.ListOptions) (*v1alpha1.EventSourceList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.EventSource, err error) + EventSourceExpansion +} + +// eventSources implements EventSourceInterface +type eventSources struct { + client rest.Interface + ns string +} + +// newEventSources returns a EventSources +func newEventSources(c *ArgoprojV1alpha1Client, namespace string) *eventSources { + return &eventSources{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the eventSource, and returns the corresponding eventSource object, and an error if there is any. +func (c *eventSources) Get(name string, options v1.GetOptions) (result *v1alpha1.EventSource, err error) { + result = &v1alpha1.EventSource{} + err = c.client.Get(). + Namespace(c.ns). + Resource("eventsources"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of EventSources that match those selectors. +func (c *eventSources) List(opts v1.ListOptions) (result *v1alpha1.EventSourceList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.EventSourceList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("eventsources"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested eventSources. +func (c *eventSources) Watch(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("eventsources"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a eventSource and creates it. Returns the server's representation of the eventSource, and an error, if there is any. +func (c *eventSources) Create(eventSource *v1alpha1.EventSource) (result *v1alpha1.EventSource, err error) { + result = &v1alpha1.EventSource{} + err = c.client.Post(). + Namespace(c.ns). + Resource("eventsources"). + Body(eventSource). + Do(). + Into(result) + return +} + +// Update takes the representation of a eventSource and updates it. Returns the server's representation of the eventSource, and an error, if there is any. +func (c *eventSources) Update(eventSource *v1alpha1.EventSource) (result *v1alpha1.EventSource, err error) { + result = &v1alpha1.EventSource{} + err = c.client.Put(). + Namespace(c.ns). + Resource("eventsources"). + Name(eventSource.Name). + Body(eventSource). + Do(). + 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 *eventSources) UpdateStatus(eventSource *v1alpha1.EventSource) (result *v1alpha1.EventSource, err error) { + result = &v1alpha1.EventSource{} + err = c.client.Put(). + Namespace(c.ns). + Resource("eventsources"). + Name(eventSource.Name). + SubResource("status"). + Body(eventSource). + Do(). + Into(result) + return +} + +// Delete takes name of the eventSource and deletes it. Returns an error if one occurs. +func (c *eventSources) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("eventsources"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *eventSources) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("eventsources"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched eventSource. +func (c *eventSources) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.EventSource, err error) { + result = &v1alpha1.EventSource{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("eventsources"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsources_client.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsources_client.go new file mode 100644 index 0000000000..a9979b1a67 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/eventsources_client.go @@ -0,0 +1,88 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type ArgoprojV1alpha1Interface interface { + RESTClient() rest.Interface + EventSourcesGetter +} + +// ArgoprojV1alpha1Client is used to interact with features provided by the argoproj.io group. +type ArgoprojV1alpha1Client struct { + restClient rest.Interface +} + +func (c *ArgoprojV1alpha1Client) EventSources(namespace string) EventSourceInterface { + return newEventSources(c, namespace) +} + +// NewForConfig creates a new ArgoprojV1alpha1Client for the given config. +func NewForConfig(c *rest.Config) (*ArgoprojV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &ArgoprojV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new ArgoprojV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *ArgoprojV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new ArgoprojV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *ArgoprojV1alpha1Client { + return &ArgoprojV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *ArgoprojV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/doc.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/doc.go new file mode 100644 index 0000000000..eb8791ce4f --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsource.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsource.go new file mode 100644 index 0000000000..82b995586b --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsource.go @@ -0,0 +1,139 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/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" +) + +// FakeEventSources implements EventSourceInterface +type FakeEventSources struct { + Fake *FakeArgoprojV1alpha1 + ns string +} + +var eventsourcesResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "eventsources"} + +var eventsourcesKind = schema.GroupVersionKind{Group: "argoproj.io", Version: "v1alpha1", Kind: "EventSource"} + +// Get takes name of the eventSource, and returns the corresponding eventSource object, and an error if there is any. +func (c *FakeEventSources) Get(name string, options v1.GetOptions) (result *v1alpha1.EventSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(eventsourcesResource, c.ns, name), &v1alpha1.EventSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.EventSource), err +} + +// List takes label and field selectors, and returns the list of EventSources that match those selectors. +func (c *FakeEventSources) List(opts v1.ListOptions) (result *v1alpha1.EventSourceList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(eventsourcesResource, eventsourcesKind, c.ns, opts), &v1alpha1.EventSourceList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.EventSourceList{ListMeta: obj.(*v1alpha1.EventSourceList).ListMeta} + for _, item := range obj.(*v1alpha1.EventSourceList).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 eventSources. +func (c *FakeEventSources) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(eventsourcesResource, c.ns, opts)) + +} + +// Create takes the representation of a eventSource and creates it. Returns the server's representation of the eventSource, and an error, if there is any. +func (c *FakeEventSources) Create(eventSource *v1alpha1.EventSource) (result *v1alpha1.EventSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(eventsourcesResource, c.ns, eventSource), &v1alpha1.EventSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.EventSource), err +} + +// Update takes the representation of a eventSource and updates it. Returns the server's representation of the eventSource, and an error, if there is any. +func (c *FakeEventSources) Update(eventSource *v1alpha1.EventSource) (result *v1alpha1.EventSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(eventsourcesResource, c.ns, eventSource), &v1alpha1.EventSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.EventSource), 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 *FakeEventSources) UpdateStatus(eventSource *v1alpha1.EventSource) (*v1alpha1.EventSource, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(eventsourcesResource, "status", c.ns, eventSource), &v1alpha1.EventSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.EventSource), err +} + +// Delete takes name of the eventSource and deletes it. Returns an error if one occurs. +func (c *FakeEventSources) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(eventsourcesResource, c.ns, name), &v1alpha1.EventSource{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeEventSources) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(eventsourcesResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.EventSourceList{}) + return err +} + +// Patch applies the patch and returns the patched eventSource. +func (c *FakeEventSources) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.EventSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(eventsourcesResource, c.ns, name, pt, data, subresources...), &v1alpha1.EventSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.EventSource), err +} diff --git a/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsources_client.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsources_client.go new file mode 100644 index 0000000000..59140484c1 --- /dev/null +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/fake/fake_eventsources_client.go @@ -0,0 +1,39 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeArgoprojV1alpha1 struct { + *testing.Fake +} + +func (c *FakeArgoprojV1alpha1) EventSources(namespace string) v1alpha1.EventSourceInterface { + return &FakeEventSources{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeArgoprojV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/gateways/common/doc.go b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/generated_expansion.go similarity index 77% rename from gateways/common/doc.go rename to pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/generated_expansion.go index f333b2d6dd..a6f68bb95f 100644 --- a/gateways/common/doc.go +++ b/pkg/client/eventsources/clientset/versioned/typed/eventsources/v1alpha1/generated_expansion.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +// Code generated by client-gen. DO NOT EDIT. -// Package common contains structs and methods that are shared across different gateways. -package common +package v1alpha1 + +type EventSourceExpansion interface{} diff --git a/pkg/client/eventsources/informers/externalversions/eventsources/interface.go b/pkg/client/eventsources/informers/externalversions/eventsources/interface.go new file mode 100644 index 0000000000..6a4faf1ffb --- /dev/null +++ b/pkg/client/eventsources/informers/externalversions/eventsources/interface.go @@ -0,0 +1,45 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package argoproj + +import ( + v1alpha1 "github.com/argoproj/argo-events/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1" + internalinterfaces "github.com/argoproj/argo-events/pkg/client/eventsources/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/eventsource.go b/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/eventsource.go new file mode 100644 index 0000000000..51250a5936 --- /dev/null +++ b/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/eventsource.go @@ -0,0 +1,88 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + eventsourcesv1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + versioned "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned" + internalinterfaces "github.com/argoproj/argo-events/pkg/client/eventsources/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/argoproj/argo-events/pkg/client/eventsources/listers/eventsources/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" +) + +// EventSourceInformer provides access to a shared informer and lister for +// EventSources. +type EventSourceInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.EventSourceLister +} + +type eventSourceInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewEventSourceInformer constructs a new informer for EventSource 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 NewEventSourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredEventSourceInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredEventSourceInformer constructs a new informer for EventSource 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 NewFilteredEventSourceInformer(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.ArgoprojV1alpha1().EventSources(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ArgoprojV1alpha1().EventSources(namespace).Watch(options) + }, + }, + &eventsourcesv1alpha1.EventSource{}, + resyncPeriod, + indexers, + ) +} + +func (f *eventSourceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredEventSourceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *eventSourceInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&eventsourcesv1alpha1.EventSource{}, f.defaultInformer) +} + +func (f *eventSourceInformer) Lister() v1alpha1.EventSourceLister { + return v1alpha1.NewEventSourceLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/interface.go b/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/interface.go new file mode 100644 index 0000000000..28e6a79888 --- /dev/null +++ b/pkg/client/eventsources/informers/externalversions/eventsources/v1alpha1/interface.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/argoproj/argo-events/pkg/client/eventsources/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // EventSources returns a EventSourceInformer. + EventSources() EventSourceInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// EventSources returns a EventSourceInformer. +func (v *version) EventSources() EventSourceInformer { + return &eventSourceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/eventsources/informers/externalversions/factory.go b/pkg/client/eventsources/informers/externalversions/factory.go new file mode 100644 index 0000000000..e16a4f16b9 --- /dev/null +++ b/pkg/client/eventsources/informers/externalversions/factory.go @@ -0,0 +1,179 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned" + eventsources "github.com/argoproj/argo-events/pkg/client/eventsources/informers/externalversions/eventsources" + internalinterfaces "github.com/argoproj/argo-events/pkg/client/eventsources/informers/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +// Start initializes all requested informers. +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InternalInformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + Argoproj() eventsources.Interface +} + +func (f *sharedInformerFactory) Argoproj() eventsources.Interface { + return eventsources.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/client/eventsources/informers/externalversions/generic.go b/pkg/client/eventsources/informers/externalversions/generic.go new file mode 100644 index 0000000000..df803f8fbb --- /dev/null +++ b/pkg/client/eventsources/informers/externalversions/generic.go @@ -0,0 +1,61 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + "fmt" + + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=argoproj.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("eventsources"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Argoproj().V1alpha1().EventSources().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/pkg/client/eventsources/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/client/eventsources/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 0000000000..652fe39056 --- /dev/null +++ b/pkg/client/eventsources/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,39 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/argoproj/argo-events/pkg/client/eventsources/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/pkg/client/eventsources/listers/eventsources/v1alpha1/eventsource.go b/pkg/client/eventsources/listers/eventsources/v1alpha1/eventsource.go new file mode 100644 index 0000000000..37b5fd3316 --- /dev/null +++ b/pkg/client/eventsources/listers/eventsources/v1alpha1/eventsource.go @@ -0,0 +1,93 @@ +/* +Copyright 2018 BlackRock, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsources/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// EventSourceLister helps list EventSources. +type EventSourceLister interface { + // List lists all EventSources in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.EventSource, err error) + // EventSources returns an object that can list and get EventSources. + EventSources(namespace string) EventSourceNamespaceLister + EventSourceListerExpansion +} + +// eventSourceLister implements the EventSourceLister interface. +type eventSourceLister struct { + indexer cache.Indexer +} + +// NewEventSourceLister returns a new EventSourceLister. +func NewEventSourceLister(indexer cache.Indexer) EventSourceLister { + return &eventSourceLister{indexer: indexer} +} + +// List lists all EventSources in the indexer. +func (s *eventSourceLister) List(selector labels.Selector) (ret []*v1alpha1.EventSource, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.EventSource)) + }) + return ret, err +} + +// EventSources returns an object that can list and get EventSources. +func (s *eventSourceLister) EventSources(namespace string) EventSourceNamespaceLister { + return eventSourceNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// EventSourceNamespaceLister helps list and get EventSources. +type EventSourceNamespaceLister interface { + // List lists all EventSources in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.EventSource, err error) + // Get retrieves the EventSource from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.EventSource, error) + EventSourceNamespaceListerExpansion +} + +// eventSourceNamespaceLister implements the EventSourceNamespaceLister +// interface. +type eventSourceNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all EventSources in the indexer for a given namespace. +func (s eventSourceNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.EventSource, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.EventSource)) + }) + return ret, err +} + +// Get retrieves the EventSource from the indexer for a given namespace and name. +func (s eventSourceNamespaceLister) Get(name string) (*v1alpha1.EventSource, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("eventsource"), name) + } + return obj.(*v1alpha1.EventSource), nil +} diff --git a/gateways/core/stream/mqtt/config_test.go b/pkg/client/eventsources/listers/eventsources/v1alpha1/expansion_generated.go similarity index 56% rename from gateways/core/stream/mqtt/config_test.go rename to pkg/client/eventsources/listers/eventsources/v1alpha1/expansion_generated.go index f4282092c0..b2400b06c7 100644 --- a/gateways/core/stream/mqtt/config_test.go +++ b/pkg/client/eventsources/listers/eventsources/v1alpha1/expansion_generated.go @@ -13,26 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +// Code generated by lister-gen. DO NOT EDIT. -package mqtt +package v1alpha1 -import ( - "github.com/smartystreets/goconvey/convey" - "testing" -) +// EventSourceListerExpansion allows custom methods to be added to +// EventSourceLister. +type EventSourceListerExpansion interface{} -var es = ` -url: tcp://mqtt.argo-events:1883 -topic: foo -clientId: 1 -` - -func TestParseConfig(t *testing.T) { - convey.Convey("Given a mqtt event source, parse it", t, func() { - ps, err := parseEventSource(es) - convey.So(err, convey.ShouldBeNil) - convey.So(ps, convey.ShouldNotBeNil) - _, ok := ps.(*mqtt) - convey.So(ok, convey.ShouldEqual, true) - }) -} +// EventSourceNamespaceListerExpansion allows custom methods to be added to +// EventSourceNamespaceLister. +type EventSourceNamespaceListerExpansion interface{} diff --git a/pkg/client/gateway/clientset/versioned/clientset.go b/pkg/client/gateway/clientset/versioned/clientset.go index c5bdecc855..6f1c0474a6 100644 --- a/pkg/client/gateway/clientset/versioned/clientset.go +++ b/pkg/client/gateway/clientset/versioned/clientset.go @@ -19,9 +19,9 @@ package versioned import ( argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1" - "k8s.io/client-go/discovery" - "k8s.io/client-go/rest" - "k8s.io/client-go/util/flowcontrol" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" ) type Interface interface { diff --git a/pkg/client/gateway/clientset/versioned/fake/register.go b/pkg/client/gateway/clientset/versioned/fake/register.go index 1c161557ca..482cd3ed6c 100644 --- a/pkg/client/gateway/clientset/versioned/fake/register.go +++ b/pkg/client/gateway/clientset/versioned/fake/register.go @@ -20,9 +20,9 @@ package fake import ( argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) diff --git a/pkg/client/gateway/clientset/versioned/scheme/register.go b/pkg/client/gateway/clientset/versioned/scheme/register.go index a898aa64a3..c00d416561 100644 --- a/pkg/client/gateway/clientset/versioned/scheme/register.go +++ b/pkg/client/gateway/clientset/versioned/scheme/register.go @@ -20,9 +20,9 @@ package scheme import ( argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) diff --git a/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/fake/fake_gateway.go b/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/fake/fake_gateway.go index d45336c1c4..1b40704496 100644 --- a/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/fake/fake_gateway.go +++ b/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/fake/fake_gateway.go @@ -18,13 +18,13 @@ limitations under the License. package fake import ( - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/testing" + 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" ) // FakeGateways implements GatewayInterface diff --git a/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway.go b/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway.go index 8c865d51e8..0ab97cac9d 100644 --- a/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway.go +++ b/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway.go @@ -20,12 +20,12 @@ package v1alpha1 import ( "time" - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/scheme" + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + scheme "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/rest" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" ) // GatewaysGetter has a method to return a GatewayInterface. diff --git a/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway_client.go b/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway_client.go index 0c12e48a37..46429675de 100644 --- a/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway_client.go +++ b/pkg/client/gateway/clientset/versioned/typed/gateway/v1alpha1/gateway_client.go @@ -18,9 +18,9 @@ limitations under the License. package v1alpha1 import ( - "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned/scheme" - "k8s.io/client-go/rest" + rest "k8s.io/client-go/rest" ) type ArgoprojV1alpha1Interface interface { diff --git a/pkg/client/gateway/informers/externalversions/factory.go b/pkg/client/gateway/informers/externalversions/factory.go index a934e6b03d..b1ab6e593a 100644 --- a/pkg/client/gateway/informers/externalversions/factory.go +++ b/pkg/client/gateway/informers/externalversions/factory.go @@ -18,17 +18,17 @@ limitations under the License. package externalversions import ( - "reflect" - "sync" - "time" + reflect "reflect" + sync "sync" + time "time" - "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" + versioned "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" gateway "github.com/argoproj/argo-events/pkg/client/gateway/informers/externalversions/gateway" - "github.com/argoproj/argo-events/pkg/client/gateway/informers/externalversions/internalinterfaces" + internalinterfaces "github.com/argoproj/argo-events/pkg/client/gateway/informers/externalversions/internalinterfaces" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/tools/cache" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. diff --git a/pkg/client/gateway/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/client/gateway/informers/externalversions/internalinterfaces/factory_interfaces.go index 4e31536877..75a4850299 100644 --- a/pkg/client/gateway/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/pkg/client/gateway/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -18,12 +18,12 @@ limitations under the License. package internalinterfaces import ( - "time" + time "time" - "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" + versioned "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/cache" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. diff --git a/pkg/client/sensor/clientset/versioned/clientset.go b/pkg/client/sensor/clientset/versioned/clientset.go index 4fb577b381..420432dda1 100644 --- a/pkg/client/sensor/clientset/versioned/clientset.go +++ b/pkg/client/sensor/clientset/versioned/clientset.go @@ -19,9 +19,9 @@ package versioned import ( argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1" - "k8s.io/client-go/discovery" - "k8s.io/client-go/rest" - "k8s.io/client-go/util/flowcontrol" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" ) type Interface interface { diff --git a/pkg/client/sensor/clientset/versioned/fake/register.go b/pkg/client/sensor/clientset/versioned/fake/register.go index 58e5e71371..57e50f01da 100644 --- a/pkg/client/sensor/clientset/versioned/fake/register.go +++ b/pkg/client/sensor/clientset/versioned/fake/register.go @@ -20,9 +20,9 @@ package fake import ( argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) diff --git a/pkg/client/sensor/clientset/versioned/scheme/register.go b/pkg/client/sensor/clientset/versioned/scheme/register.go index 9bbfa27750..f73b48c8c0 100644 --- a/pkg/client/sensor/clientset/versioned/scheme/register.go +++ b/pkg/client/sensor/clientset/versioned/scheme/register.go @@ -20,9 +20,9 @@ package scheme import ( argoprojv1alpha1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) diff --git a/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/fake/fake_sensor.go b/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/fake/fake_sensor.go index 7f411564f4..00b05959d4 100644 --- a/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/fake/fake_sensor.go +++ b/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/fake/fake_sensor.go @@ -18,13 +18,13 @@ limitations under the License. package fake import ( - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/testing" + 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" ) // FakeSensors implements SensorInterface diff --git a/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor.go b/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor.go index a350fe7d97..5ec1dd07a5 100644 --- a/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor.go +++ b/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor.go @@ -20,12 +20,12 @@ package v1alpha1 import ( "time" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/scheme" + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + scheme "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/rest" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" ) // SensorsGetter has a method to return a SensorInterface. diff --git a/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor_client.go b/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor_client.go index 14a2132a08..72e79ea81f 100644 --- a/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor_client.go +++ b/pkg/client/sensor/clientset/versioned/typed/sensor/v1alpha1/sensor_client.go @@ -18,9 +18,9 @@ limitations under the License. package v1alpha1 import ( - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" + v1alpha1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/scheme" - "k8s.io/client-go/rest" + rest "k8s.io/client-go/rest" ) type ArgoprojV1alpha1Interface interface { diff --git a/pkg/client/sensor/informers/externalversions/factory.go b/pkg/client/sensor/informers/externalversions/factory.go index c96c58dbf0..32c44340f6 100644 --- a/pkg/client/sensor/informers/externalversions/factory.go +++ b/pkg/client/sensor/informers/externalversions/factory.go @@ -18,17 +18,17 @@ limitations under the License. package externalversions import ( - "reflect" - "sync" - "time" + reflect "reflect" + sync "sync" + time "time" - "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" - "github.com/argoproj/argo-events/pkg/client/sensor/informers/externalversions/internalinterfaces" + versioned "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" + internalinterfaces "github.com/argoproj/argo-events/pkg/client/sensor/informers/externalversions/internalinterfaces" sensor "github.com/argoproj/argo-events/pkg/client/sensor/informers/externalversions/sensor" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/tools/cache" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. diff --git a/pkg/client/sensor/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/client/sensor/informers/externalversions/internalinterfaces/factory_interfaces.go index 85a9b4dbcd..38a4b245eb 100644 --- a/pkg/client/sensor/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/pkg/client/sensor/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -18,12 +18,12 @@ limitations under the License. package internalinterfaces import ( - "time" + time "time" - "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" + versioned "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/cache" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. diff --git a/sensors/cmd/client.go b/sensors/cmd/client.go index 3cd6182049..027d55e30d 100644 --- a/sensors/cmd/client.go +++ b/sensors/cmd/client.go @@ -41,7 +41,7 @@ func main() { if !ok { panic("sensor namespace is not provided") } - controllerInstanceID, ok := os.LookupEnv(common.EnvVarSensorControllerInstanceID) + controllerInstanceID, ok := os.LookupEnv(common.EnvVarControllerInstanceID) if !ok { panic("sensor controller instance ID is not provided") } diff --git a/sensors/event-handler.go b/sensors/event-handler.go index eb056c3492..f19d17ba8d 100644 --- a/sensors/event-handler.go +++ b/sensors/event-handler.go @@ -37,14 +37,14 @@ func (sec *sensorExecutionCtx) processUpdateNotification(ew *updateNotification) defer func() { // persist updates to sensor resource labels := map[string]string{ - common.LabelSensorName: sec.sensor.Name, - common.LabelSensorKeyPhase: string(sec.sensor.Status.Phase), - common.LabelKeySensorControllerInstanceID: sec.controllerInstanceID, - common.LabelOperation: "persist_state_update", + common.LabelSensorName: sec.sensor.Name, + sn.LabelPhase: string(sec.sensor.Status.Phase), + sn.LabelControllerInstanceID: sec.controllerInstanceID, + common.LabelOperation: "persist_state_update", } eventType := common.StateChangeEventType - updatedSensor, err := sn.PersistUpdates(sec.sensorClient, sec.sensor, sec.controllerInstanceID, sec.log) + updatedSensor, err := sn.PersistUpdates(sec.sensorClient, sec.sensor, sec.log) if err != nil { sec.log.WithError(err).Error("failed to persist sensor update, escalating...") // escalate failure @@ -202,7 +202,7 @@ func (sec *sensorExecutionCtx) WatchEventsFromGateways() { case pc.NATS: sec.NatsEventProtocol() var err error - if sec.sensor, err = sn.PersistUpdates(sec.sensorClient, sec.sensor, sec.controllerInstanceID, sec.log); err != nil { + if sec.sensor, err = sn.PersistUpdates(sec.sensorClient, sec.sensor, sec.log); err != nil { sec.log.WithError(err).Error("failed to persist sensor update") labels := map[string]string{ common.LabelEventType: string(common.OperationFailureEventType), diff --git a/sensors/event-handler_test.go b/sensors/event-handler_test.go deleted file mode 100644 index 9d6b3db0e8..0000000000 --- a/sensors/event-handler_test.go +++ /dev/null @@ -1,475 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sensors - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "k8s.io/apimachinery/pkg/runtime" - "net/http" - "strings" - "testing" - "time" - - "github.com/argoproj/argo-events/common" - sensor2 "github.com/argoproj/argo-events/controllers/sensor" - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - sensorFake "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned/fake" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" - dynamicFake "k8s.io/client-go/dynamic/fake" - "k8s.io/client-go/kubernetes/fake" -) - -var sensorStr = ` -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: test-sensor - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events -spec: - template: - containers: - - name: "sensor" - image: "argoproj/sensor" - imagePullPolicy: Always - serviceAccountName: argo-events-sa - dependencies: - - name: "test-gateway:test" - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - name: test-workflow-trigger - group: argoproj.io - version: v1alpha1 - resource: workflows - source: - inline: | - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: hello-world- - spec: - entrypoint: whalesay - templates: - - name: whalesay - container: - args: - - "hello world" - command: - - cowsay - image: "docker/whalesay:latest" -` - -var podResourceList = metav1.APIResourceList{ - GroupVersion: metav1.GroupVersion{Group: "", Version: "v1"}.String(), - APIResources: []metav1.APIResource{ - {Kind: "Pod", Namespaced: true, Name: "pods", SingularName: "pod", Group: "", Version: "v1", Verbs: []string{"create", "get"}}, - }, -} - -func getSensor() (*v1alpha1.Sensor, error) { - var sensor v1alpha1.Sensor - err := yaml.Unmarshal([]byte(sensorStr), &sensor) - return &sensor, err -} - -type mockHttpWriter struct { - Status int - Payload []byte -} - -func (m *mockHttpWriter) Header() http.Header { - return http.Header{} -} - -func (m *mockHttpWriter) Write(p []byte) (int, error) { - m.Payload = p - return 0, nil -} - -func (m *mockHttpWriter) WriteHeader(statusCode int) { - m.Status = statusCode -} - -func getsensorExecutionCtx(sensor *v1alpha1.Sensor) *sensorExecutionCtx { - kubeClientset := fake.NewSimpleClientset() - fakeDynamicClient := dynamicFake.NewSimpleDynamicClient(&runtime.Scheme{}) - return &sensorExecutionCtx{ - kubeClient: kubeClientset, - dynamicClient: fakeDynamicClient, - log: common.NewArgoEventsLogger(), - sensorClient: sensorFake.NewSimpleClientset(), - sensor: sensor, - controllerInstanceID: "test-1", - queue: make(chan *updateNotification), - } -} - -func getCloudEvent() *apicommon.Event { - return &apicommon.Event{ - Context: apicommon.EventContext{ - CloudEventsVersion: common.CloudEventsVersion, - EventID: fmt.Sprintf("%x", "123"), - ContentType: "application/json", - EventTime: metav1.MicroTime{Time: time.Now().UTC()}, - EventType: "test", - EventTypeVersion: common.CloudEventsVersion, - Source: &apicommon.URI{ - Host: common.DefaultEventSourceName("test-gateway", "test"), - }, - }, - Payload: []byte(`{ - "x": "abc" - }`), - } -} - -func TestDependencyGlobMatch(t *testing.T) { - sensor, err := getSensor() - convey.Convey("Given a sensor spec, create a sensor", t, func() { - globDepName := "test-gateway:*" - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - sensor.Spec.Dependencies[0].Name = globDepName - sec := getsensorExecutionCtx(sensor) - - sec.sensor, err = sec.sensorClient.ArgoprojV1alpha1().Sensors(sensor.Namespace).Create(sensor) - convey.So(err, convey.ShouldBeNil) - - sec.sensor.Status.Nodes = make(map[string]v1alpha1.NodeStatus) - fmt.Println(sensor.NodeID(globDepName)) - - sensor2.InitializeNode(sec.sensor, globDepName, v1alpha1.NodeTypeEventDependency, sec.log, "node is init") - sensor2.MarkNodePhase(sec.sensor, globDepName, v1alpha1.NodeTypeEventDependency, v1alpha1.NodePhaseActive, nil, sec.log, "node is active") - - sensor2.InitializeNode(sec.sensor, "test-workflow-trigger", v1alpha1.NodeTypeTrigger, sec.log, "trigger is init") - - e := &apicommon.Event{ - Payload: []byte("hello"), - Context: apicommon.EventContext{ - Source: &apicommon.URI{ - Host: "test-gateway:test", - }, - }, - } - dataCh := make(chan *updateNotification) - go func() { - data := <-sec.queue - dataCh <- data - }() - ok := sec.sendEventToInternalQueue(e, &mockHttpWriter{}) - convey.So(ok, convey.ShouldEqual, true) - ew := <-dataCh - sec.processUpdateNotification(ew) - }) -} - -func TestEventHandler(t *testing.T) { - sensor, err := getSensor() - convey.Convey("Given a sensor spec, create a sensor", t, func() { - convey.So(err, convey.ShouldBeNil) - convey.So(sensor, convey.ShouldNotBeNil) - sec := getsensorExecutionCtx(sensor) - - sec.sensor, err = sec.sensorClient.ArgoprojV1alpha1().Sensors(sensor.Namespace).Create(sensor) - convey.So(err, convey.ShouldBeNil) - - sec.sensor.Status.Nodes = make(map[string]v1alpha1.NodeStatus) - fmt.Println(sensor.NodeID("test-gateway:test")) - - sensor2.InitializeNode(sec.sensor, "test-gateway:test", v1alpha1.NodeTypeEventDependency, sec.log, "node is init") - sensor2.MarkNodePhase(sec.sensor, "test-gateway:test", v1alpha1.NodeTypeEventDependency, v1alpha1.NodePhaseActive, nil, sec.log, "node is active") - - sensor2.InitializeNode(sec.sensor, "test-workflow-trigger", v1alpha1.NodeTypeTrigger, sec.log, "trigger is init") - - sec.processUpdateNotification(&updateNotification{ - event: getCloudEvent(), - notificationType: v1alpha1.EventNotification, - writer: &mockHttpWriter{}, - eventDependency: &v1alpha1.EventDependency{ - Name: "test-gateway:test", - }, - }) - - convey.Convey("Update sensor event dependencies", func() { - sensor = sec.sensor.DeepCopy() - sensor.Spec.Dependencies = append(sensor.Spec.Dependencies, v1alpha1.EventDependency{ - Name: "test-gateway:test2", - }) - sec.processUpdateNotification(&updateNotification{ - event: nil, - notificationType: v1alpha1.ResourceUpdateNotification, - writer: &mockHttpWriter{}, - eventDependency: &v1alpha1.EventDependency{ - Name: "test-gateway:test2", - }, - sensor: sensor, - }) - convey.So(len(sec.sensor.Status.Nodes), convey.ShouldEqual, 3) - }) - - }) -} - -func TestDeleteStaleStatusNodes(t *testing.T) { - convey.Convey("Given a sensor, delete the stale status nodes", t, func() { - sensor, err := getSensor() - convey.So(err, convey.ShouldBeNil) - sec := getsensorExecutionCtx(sensor) - nodeId1 := sensor.NodeID("test-gateway:test") - nodeId2 := sensor.NodeID("test-gateway:test2") - sec.sensor.Status.Nodes = map[string]v1alpha1.NodeStatus{ - nodeId1: v1alpha1.NodeStatus{ - Type: v1alpha1.NodeTypeEventDependency, - Name: "test-gateway:test", - Phase: v1alpha1.NodePhaseActive, - ID: "1234", - }, - nodeId2: v1alpha1.NodeStatus{ - Type: v1alpha1.NodeTypeEventDependency, - Name: "test-gateway:test2", - Phase: v1alpha1.NodePhaseActive, - ID: "2345", - }, - } - - _, ok := sec.sensor.Status.Nodes[nodeId1] - convey.So(ok, convey.ShouldEqual, true) - _, ok = sec.sensor.Status.Nodes[nodeId2] - convey.So(ok, convey.ShouldEqual, true) - - sec.deleteStaleStatusNodes() - convey.So(len(sec.sensor.Status.Nodes), convey.ShouldEqual, 1) - _, ok = sec.sensor.Status.Nodes[nodeId1] - convey.So(ok, convey.ShouldEqual, true) - _, ok = sec.sensor.Status.Nodes[nodeId2] - convey.So(ok, convey.ShouldEqual, false) - }) -} - -func TestValidateEvent(t *testing.T) { - convey.Convey("Given an event, validate it", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - dep, valid := sec.validateEvent(&apicommon.Event{ - Context: apicommon.EventContext{ - Source: &apicommon.URI{ - Host: "test-gateway:test", - }, - }, - }) - convey.So(valid, convey.ShouldEqual, true) - convey.So(dep, convey.ShouldNotBeNil) - }) -} - -func TestParseEvent(t *testing.T) { - convey.Convey("Given an event payload, parse event", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - e := &apicommon.Event{ - Payload: []byte("hello"), - Context: apicommon.EventContext{ - Source: &apicommon.URI{ - Host: "test-gateway:test", - }, - }, - } - payload, err := json.Marshal(e) - convey.So(err, convey.ShouldBeNil) - - event, err := sec.parseEvent(payload) - convey.So(err, convey.ShouldBeNil) - convey.So(string(event.Payload), convey.ShouldEqual, "hello") - }) -} - -func TestSendToInternalQueue(t *testing.T) { - convey.Convey("Given an event, send it on internal queue", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - e := &apicommon.Event{ - Payload: []byte("hello"), - Context: apicommon.EventContext{ - Source: &apicommon.URI{ - Host: "test-gateway:test", - }, - }, - } - go func() { - <-sec.queue - }() - ok := sec.sendEventToInternalQueue(e, &mockHttpWriter{}) - convey.So(ok, convey.ShouldEqual, true) - }) -} - -func TestHandleHttpEventHandler(t *testing.T) { - convey.Convey("Test http handler", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - e := &apicommon.Event{ - Payload: []byte("hello"), - Context: apicommon.EventContext{ - Source: &apicommon.URI{ - Host: "test-gateway:test", - }, - }, - } - go func() { - <-sec.queue - }() - payload, err := json.Marshal(e) - convey.So(err, convey.ShouldBeNil) - writer := &mockHttpWriter{} - sec.httpEventHandler(writer, &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(payload)), - }) - convey.So(writer.Status, convey.ShouldEqual, http.StatusOK) - }) -} - -func TestSuccessNatsConnection(t *testing.T) { - convey.Convey("Given a successful nats connection, generate K8s event", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - sec.successNatsConnection() - req1, err := labels.NewRequirement(common.LabelOperation, selection.Equals, []string{"nats_connection_setup"}) - convey.So(err, convey.ShouldBeNil) - req2, err := labels.NewRequirement(common.LabelEventType, selection.Equals, []string{string(common.OperationSuccessEventType)}) - convey.So(err, convey.ShouldBeNil) - req3, err := labels.NewRequirement(common.LabelSensorName, selection.Equals, []string{string(sec.sensor.Name)}) - convey.So(err, convey.ShouldBeNil) - - eventList, err := sec.kubeClient.CoreV1().Events(sec.sensor.Namespace).List(metav1.ListOptions{ - LabelSelector: labels.NewSelector().Add([]labels.Requirement{*req1, *req2, *req3}...).String(), - }) - convey.So(err, convey.ShouldBeNil) - convey.So(len(eventList.Items), convey.ShouldEqual, 1) - event := eventList.Items[0] - convey.So(event.Reason, convey.ShouldEqual, "connection setup successfully") - }) -} - -func TestEscalateNatsConnectionFailure(t *testing.T) { - convey.Convey("Given a failed nats connection, escalate through K8s event", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - sec.escalateNatsConnectionFailure() - req1, err := labels.NewRequirement(common.LabelOperation, selection.Equals, []string{"nats_connection_setup"}) - convey.So(err, convey.ShouldBeNil) - req2, err := labels.NewRequirement(common.LabelEventType, selection.Equals, []string{string(common.OperationFailureEventType)}) - convey.So(err, convey.ShouldBeNil) - req3, err := labels.NewRequirement(common.LabelSensorName, selection.Equals, []string{string(sec.sensor.Name)}) - convey.So(err, convey.ShouldBeNil) - - eventList, err := sec.kubeClient.CoreV1().Events(sec.sensor.Namespace).List(metav1.ListOptions{ - LabelSelector: labels.NewSelector().Add([]labels.Requirement{*req1, *req2, *req3}...).String(), - }) - convey.So(err, convey.ShouldBeNil) - convey.So(len(eventList.Items), convey.ShouldEqual, 1) - event := eventList.Items[0] - convey.So(event.Reason, convey.ShouldEqual, "connection setup failed") - }) -} - -func TestSuccessNatsSubscription(t *testing.T) { - convey.Convey("Given a successful nats subscription, generate K8s event", t, func() { - s, _ := getSensor() - eventSource := "fake" - sec := getsensorExecutionCtx(s) - sec.successNatsSubscription(eventSource) - req1, err := labels.NewRequirement(common.LabelOperation, selection.Equals, []string{"nats_subscription_success"}) - convey.So(err, convey.ShouldBeNil) - req2, err := labels.NewRequirement(common.LabelEventType, selection.Equals, []string{string(common.OperationSuccessEventType)}) - convey.So(err, convey.ShouldBeNil) - req3, err := labels.NewRequirement(common.LabelSensorName, selection.Equals, []string{string(sec.sensor.Name)}) - convey.So(err, convey.ShouldBeNil) - req4, err := labels.NewRequirement(common.LabelEventSource, selection.Equals, []string{strings.Replace(eventSource, ":", "_", -1)}) - convey.So(err, convey.ShouldBeNil) - - eventList, err := sec.kubeClient.CoreV1().Events(sec.sensor.Namespace).List(metav1.ListOptions{ - LabelSelector: labels.NewSelector().Add([]labels.Requirement{*req1, *req2, *req3, *req4}...).String(), - }) - convey.So(err, convey.ShouldBeNil) - convey.So(len(eventList.Items), convey.ShouldEqual, 1) - event := eventList.Items[0] - convey.So(event.Reason, convey.ShouldEqual, "nats subscription success") - }) -} - -func TestEscalateNatsSubscriptionFailure(t *testing.T) { - convey.Convey("Given a failed nats subscription, escalate K8s event", t, func() { - s, _ := getSensor() - eventSource := "fake" - sec := getsensorExecutionCtx(s) - sec.escalateNatsSubscriptionFailure(eventSource) - req1, err := labels.NewRequirement(common.LabelOperation, selection.Equals, []string{"nats_subscription_failure"}) - convey.So(err, convey.ShouldBeNil) - req2, err := labels.NewRequirement(common.LabelEventType, selection.Equals, []string{string(common.OperationFailureEventType)}) - convey.So(err, convey.ShouldBeNil) - req3, err := labels.NewRequirement(common.LabelSensorName, selection.Equals, []string{string(sec.sensor.Name)}) - convey.So(err, convey.ShouldBeNil) - req4, err := labels.NewRequirement(common.LabelEventSource, selection.Equals, []string{strings.Replace(eventSource, ":", "_", -1)}) - convey.So(err, convey.ShouldBeNil) - - eventList, err := sec.kubeClient.CoreV1().Events(sec.sensor.Namespace).List(metav1.ListOptions{ - LabelSelector: labels.NewSelector().Add([]labels.Requirement{*req1, *req2, *req3, *req4}...).String(), - }) - convey.So(err, convey.ShouldBeNil) - convey.So(len(eventList.Items), convey.ShouldEqual, 1) - event := eventList.Items[0] - convey.So(event.Reason, convey.ShouldEqual, "nats subscription failed") - }) -} - -func TestProcessNatsMessage(t *testing.T) { - convey.Convey("Given nats message, process it", t, func() { - s, _ := getSensor() - sec := getsensorExecutionCtx(s) - e := &apicommon.Event{ - Payload: []byte("hello"), - Context: apicommon.EventContext{ - Source: &apicommon.URI{ - Host: "test-gateway:test", - }, - }, - } - dataCh := make(chan []byte) - go func() { - data := <-sec.queue - dataCh <- data.event.Payload - }() - payload, err := json.Marshal(e) - convey.So(err, convey.ShouldBeNil) - sec.processNatsMessage(payload, "fake") - data := <-dataCh - convey.So(data, convey.ShouldNotBeNil) - convey.So(string(data), convey.ShouldEqual, "hello") - }) -} diff --git a/sensors/signal-filter_test.go b/sensors/signal-filter_test.go deleted file mode 100644 index aae0e2d64a..0000000000 --- a/sensors/signal-filter_test.go +++ /dev/null @@ -1,343 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package sensors - -import ( - "reflect" - "testing" - "time" - - "github.com/argoproj/argo-events/common" - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func Test_filterTime(t *testing.T) { - timeFilter := &v1alpha1.TimeFilter{ - Stop: "17:14:00", - Start: "10:11:00", - } - event := getCloudEvent() - - currentT := time.Now().UTC() - currentT = time.Date(currentT.Year(), currentT.Month(), currentT.Day(), 0, 0, 0, 0, time.UTC) - currentTStr := currentT.Format(common.StandardYYYYMMDDFormat) - parsedTime, err := time.Parse(common.StandardTimeFormat, currentTStr+" 16:36:34") - assert.Nil(t, err) - event.Context.EventTime = metav1.MicroTime{ - Time: parsedTime, - } - sensor, err := getSensor() - assert.Nil(t, err) - sOptCtx := getsensorExecutionCtx(sensor) - valid, err := sOptCtx.filterTime(timeFilter, &event.Context.EventTime) - assert.Nil(t, err) - assert.Equal(t, true, valid) - - // test invalid event - timeFilter.Start = "09:09:09" - timeFilter.Stop = "09:10:09" - valid, err = sOptCtx.filterTime(timeFilter, &event.Context.EventTime) - assert.Nil(t, err) - assert.Equal(t, false, valid) - - // test no stop - timeFilter.Start = "09:09:09" - timeFilter.Stop = "" - valid, err = sOptCtx.filterTime(timeFilter, &event.Context.EventTime) - assert.Nil(t, err) - assert.Equal(t, true, valid) - - // test no start - timeFilter.Start = "" - timeFilter.Stop = "17:09:09" - valid, err = sOptCtx.filterTime(timeFilter, &event.Context.EventTime) - assert.Nil(t, err) - assert.Equal(t, true, valid) -} - -func Test_filterContext(t *testing.T) { - event := getCloudEvent() - assert.NotNil(t, event) - sensor, err := getSensor() - assert.Nil(t, err) - sOptCtx := getsensorExecutionCtx(sensor) - assert.NotNil(t, sOptCtx) - testCtx := event.Context.DeepCopy() - valid := sOptCtx.filterContext(testCtx, &event.Context) - assert.Equal(t, true, valid) - testCtx.Source.Host = "dummy source" - valid = sOptCtx.filterContext(testCtx, &event.Context) - assert.Equal(t, false, valid) -} - -func Test_filterData(t *testing.T) { - type args struct { - data []v1alpha1.DataFilter - event *apicommon.Event - } - tests := []struct { - name string - args args - want bool - wantErr bool - }{ - { - name: "nil event", - args: args{data: nil, event: nil}, - want: true, - wantErr: false, - }, - { - name: "unsupported content type", - args: args{data: nil, event: &apicommon.Event{Payload: []byte("a")}}, - want: true, - wantErr: false, - }, - { - name: "empty data", - args: args{data: nil, event: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: "application/json", - }, - }}, - want: true, - wantErr: false, - }, - { - name: "nil filters, JSON data", - args: args{data: nil, event: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: "application/json", - }, - Payload: []byte("{\"k\": \"v\"}"), - }}, - want: true, - wantErr: false, - }, - { - name: "string filter, JSON data", - args: args{ - data: []v1alpha1.DataFilter{ - { - Path: "k", - Type: v1alpha1.JSONTypeString, - Value: []string{"v"}, - }, - }, - event: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: "application/json", - }, - Payload: []byte("{\"k\": \"v\"}"), - }, - }, - want: true, - wantErr: false, - }, - { - name: "number filter, JSON data", - args: args{data: []v1alpha1.DataFilter{ - { - Path: "k", - Type: v1alpha1.JSONTypeNumber, - Value: []string{"1.0"}, - }, - }, - event: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: "application/json", - }, - Payload: []byte("{\"k\": \"1.0\"}"), - }}, - want: true, - wantErr: false, - }, - { - name: "multiple filters, nested JSON data", - args: args{ - data: []v1alpha1.DataFilter{ - { - Path: "k", - Type: v1alpha1.JSONTypeString, - Value: []string{"v"}, - }, - { - Path: "k1.k", - Type: v1alpha1.JSONTypeNumber, - Value: []string{"3.14"}, - }, - { - Path: "k1.k2", - Type: v1alpha1.JSONTypeString, - Value: []string{"hello,world", "hello there"}, - }, - }, - event: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: "application/json", - }, - Payload: []byte("{\"k\": true, \"k1\": {\"k\": 3.14, \"k2\": \"hello, world\"}}"), - }}, - want: false, - wantErr: false, - }, - } - sensor, err := getSensor() - assert.Nil(t, err) - sOptCtx := getsensorExecutionCtx(sensor) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := sOptCtx.filterData(tt.args.data, tt.args.event) - if (err != nil) != tt.wantErr { - t.Errorf("filterData() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("filterData() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_mapIsSubset(t *testing.T) { - type args struct { - sub map[string]string - m map[string]string - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "nil sub, nil map", - args: args{sub: nil, m: nil}, - want: true, - }, - { - name: "empty sub, empty map", - args: args{sub: make(map[string]string), m: make(map[string]string)}, - want: true, - }, - { - name: "empty sub, non-empty map", - args: args{sub: make(map[string]string), m: map[string]string{"k": "v"}}, - want: true, - }, - { - name: "disjoint", - args: args{sub: map[string]string{"k1": "v1"}, m: map[string]string{"k": "v"}}, - want: false, - }, - { - name: "subset", - args: args{sub: map[string]string{"k1": "v1"}, m: map[string]string{"k": "v", "k1": "v1"}}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := mapIsSubset(tt.args.sub, tt.args.m); got != tt.want { - t.Errorf("mapIsSubset() = %v, want %v", got, tt.want) - } - }) - } -} - -// this test is meant to cover the missing cases for those not covered in eventDependency-filter_test.go and trigger-params_test.go -func Test_renderEventDataAsJSON(t *testing.T) { - type args struct { - e *apicommon.Event - } - tests := []struct { - name string - args args - want []byte - wantErr bool - }{ - { - name: "nil event", - args: args{e: nil}, - want: nil, - wantErr: true, - }, - { - name: "missing content type", - args: args{e: &apicommon.Event{}}, - want: nil, - wantErr: true, - }, - { - name: "valid yaml content", - args: args{e: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: MediaTypeYAML, - }, - Payload: []byte(`apiVersion: v1alpha1`), - }}, - want: []byte(`{"apiVersion":"v1alpha1"}`), - wantErr: false, - }, - { - name: "json content marked as yaml", - args: args{e: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: MediaTypeYAML, - }, - Payload: []byte(`{"apiVersion":5}`), - }}, - want: []byte(`{"apiVersion":5}`), - wantErr: false, - }, - { - name: "invalid json content", - args: args{e: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: MediaTypeJSON, - }, - Payload: []byte(`{5:"numberkey"}`), - }}, - want: nil, - wantErr: true, - }, - { - name: "invalid yaml content", - args: args{e: &apicommon.Event{ - Context: apicommon.EventContext{ - ContentType: MediaTypeYAML, - }, - Payload: []byte(`%\x786`), - }}, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := renderEventDataAsJSON(tt.args.e) - if (err != nil) != tt.wantErr { - t.Errorf("renderEventDataAsJSON() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("renderEventDataAsJSON() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/sensors/trigger-params_test.go b/sensors/trigger-params_test.go deleted file mode 100644 index 36e70a95fb..0000000000 --- a/sensors/trigger-params_test.go +++ /dev/null @@ -1,229 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sensors - -import ( - "reflect" - "testing" - - "fmt" - - apicommon "github.com/argoproj/argo-events/pkg/apis/common" - "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" -) - -func Test_applyParams(t *testing.T) { - defaultValue := "default" - events := map[string]apicommon.Event{ - "simpleJSON": { - Context: apicommon.EventContext{ - ContentType: MediaTypeJSON, - }, - Payload: []byte(`{"name":{"first":"matt","last":"magaldi"},"age":24}`), - }, - "nonJSON": { - Context: apicommon.EventContext{ - ContentType: MediaTypeJSON, - }, - Payload: []byte(`apiVersion: v1alpha1`), - }, - } - type args struct { - jsonObj []byte - params []v1alpha1.TriggerParameter - events map[string]apicommon.Event - } - tests := []struct { - name string - args args - want []byte - wantErr bool - }{ - { - name: "no event and missing default -> error", - args: args{ - jsonObj: []byte(""), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "missing", - }, - }, - }, - events: events, - }, - want: nil, - wantErr: true, - }, - { - name: "no event with default -> success", - args: args{ - jsonObj: []byte(""), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "missing", - Value: &defaultValue, - }, - Dest: "x", - }, - }, - events: events, - }, - want: []byte(`{"x":"default"}`), - wantErr: false, - }, - { - name: "no event with default, but missing dest -> error", - args: args{ - jsonObj: []byte(""), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "missing", - Value: &defaultValue, - }, - }, - }, - events: events, - }, - want: nil, - wantErr: true, - }, - { - name: "simpleJSON (new field) -> success", - args: args{ - jsonObj: []byte(``), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "simpleJSON", - Path: "name.last", - }, - Dest: "x", - }, - }, - events: events, - }, - want: []byte(`{"x":"magaldi"}`), - wantErr: false, - }, - { - name: "simpleJSON (updated field) -> success", - args: args{ - jsonObj: []byte(`{"x":"before"}`), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "simpleJSON", - Path: "name.last", - }, - Dest: "x", - }, - }, - events: events, - }, - want: []byte(`{"x":"magaldi"}`), - wantErr: false, - }, - { - name: "simpleJSON (prepended field) -> success", - args: args{ - jsonObj: []byte(`{"x":"before"}`), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "simpleJSON", - Path: "name.last", - }, - Dest: "x", - Operation: v1alpha1.TriggerParameterOpPrepend, - }, - }, - events: events, - }, - want: []byte(`{"x":"magaldibefore"}`), - wantErr: false, - }, - { - name: "simpleJSON (appended field) -> success", - args: args{ - jsonObj: []byte(`{"x":"before"}`), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "simpleJSON", - Path: "name.last", - }, - Dest: "x", - Operation: v1alpha1.TriggerParameterOpAppend, - }, - }, - events: events, - }, - want: []byte(`{"x":"beforemagaldi"}`), - wantErr: false, - }, - { - name: "non JSON, no default -> pass payload bytes without converting", - args: args{ - jsonObj: []byte(``), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "nonJSON", - }, - Dest: "x", - }, - }, - events: events, - }, - want: []byte(fmt.Sprintf(`{"x":"%s"}`, string(events["nonJSON"].Payload))), - wantErr: false, - }, - { - name: "non JSON, with path -> error", - args: args{ - jsonObj: []byte(``), - params: []v1alpha1.TriggerParameter{ - { - Src: &v1alpha1.TriggerParameterSource{ - Event: "nonJSON", - Path: "test", - }, - Dest: "x", - }, - }, - events: events, - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := applyParams(tt.args.jsonObj, tt.args.params, tt.args.events) - if (err != nil) != tt.wantErr { - t.Errorf("applyParams() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("applyParams() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/sensors/trigger.go b/sensors/trigger.go index 9c7904e569..88c797ff06 100644 --- a/sensors/trigger.go +++ b/sensors/trigger.go @@ -19,7 +19,6 @@ package sensors import ( "encoding/json" "fmt" - "k8s.io/apimachinery/pkg/runtime/schema" "github.com/Knetic/govaluate" @@ -46,11 +45,7 @@ func (sec *sensorExecutionCtx) canProcessTriggers() (bool, error) { group: for _, group := range sec.sensor.Spec.DependencyGroups { for _, dependency := range group.Dependencies { - nodeStatus := sn.GetNodeByName(sec.sensor, dependency) - if nodeStatus == nil { - return false, fmt.Errorf("failed to get a dependency: %+v", dependency) - } - if nodeStatus.Phase != v1alpha1.NodePhaseComplete { + if nodeStatus := sn.GetNodeByName(sec.sensor, dependency); nodeStatus.Phase != v1alpha1.NodePhaseComplete { groups[group.Name] = false continue group } @@ -253,7 +248,7 @@ func (sec *sensorExecutionCtx) applyTriggerPolicy(trigger *v1alpha1.Trigger, res Factor: trigger.Policy.Backoff.Factor, Jitter: trigger.Policy.Backoff.Jitter, }, func() (bool, error) { - obj, err := resourceInterface.Get(name, metav1.GetOptions{}) + obj, err := resourceInterface.Namespace(namespace).Get(name, metav1.GetOptions{}) if err != nil { sec.log.WithError(err).WithField("resource-name", obj.GetName()).Error("failed to get triggered resource") return false, nil @@ -332,7 +327,7 @@ func (sec *sensorExecutionCtx) createResourceObject(trigger *v1alpha1.Trigger, o Resource: trigger.Template.Resource, }) - liveObj, err := dynamicResInterface.Namespace(obj.GetNamespace()).Create(obj, metav1.CreateOptions{}) + liveObj, err := dynamicResInterface.Namespace(namespace).Create(obj, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("failed to create resource object. err: %+v", err) } diff --git a/sensors/trigger_test.go b/sensors/trigger_test.go deleted file mode 100644 index e262e638b5..0000000000 --- a/sensors/trigger_test.go +++ /dev/null @@ -1,457 +0,0 @@ -/* -Copyright 2018 BlackRock, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sensors - -// -//import ( -// "encoding/json" -// "testing" -// -// apicommon "github.com/argoproj/argo-events/pkg/apis/common" -// "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" -// "github.com/smartystreets/goconvey/convey" -// corev1 "k8s.io/api/core/v1" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -// "k8s.io/apimachinery/pkg/labels" -// "k8s.io/apimachinery/pkg/runtime" -// "k8s.io/apimachinery/pkg/runtime/schema" -// "k8s.io/apimachinery/pkg/types" -// "k8s.io/apimachinery/pkg/watch" -// "k8s.io/client-go/dynamic" -// dynamicfake "k8s.io/client-go/dynamic/fake" -// "k8s.io/client-go/kubernetes/fake" -// kTesting "k8s.io/client-go/testing" -// "k8s.io/client-go/util/flowcontrol" -//) -// -//var successLabels = map[string]string{ -// "success-label": "fake", -//} -// -//var failureLabels = map[string]string{ -// "failure-label": "fake", -//} -// -//var podTemplate = &corev1.Pod{ -// TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"}, -// Spec: corev1.PodSpec{ -// Containers: []corev1.Container{ -// { -// Name: "test1", -// Image: "docker/whalesay", -// }, -// }, -// }, -//} -// -//var triggerTemplate = v1alpha1.Trigger{ -// Template: &v1alpha1.TriggerTemplate{ -// GroupVersionResource: &metav1.GroupVersionResource{ -// Resource: "pods", -// Version: "v1", -// }, -// }, -//} -// -//func getUnstructured(res interface{}) (*unstructured.Unstructured, error) { -// obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(res) -// if err != nil { -// return nil, err -// } -// return &unstructured.Unstructured{Object: obj}, nil -//} -// -//func TestProcessTrigger(t *testing.T) { -// convey.Convey("Given a sensor", t, func() { -// trigger := *triggerTemplate.DeepCopy() -// trigger.Template.Name = "testTrigger" -// pod := podTemplate.DeepCopy() -// pod.Name = "testTrigger" -// uObj, err := getUnstructured(pod) -// convey.So(err, convey.ShouldBeNil) -// trigger.Template.Source = &v1alpha1.ArtifactLocation{ -// Resource: uObj, -// } -// testSensor, err := getSensor() -// convey.So(err, convey.ShouldBeNil) -// soc := getsensorExecutionCtx(testSensor) -// err = soc.executeTrigger(trigger) -// convey.So(err, convey.ShouldBeNil) -// }) -//} -// -//type FakeName struct { -// First string `json:"first"` -// Last string `json:"last"` -//} -// -//type fakeEvent struct { -// Name string `json:"name"` -// Namespace string `json:"namespace"` -// Group string `json:"group"` -// GenerateName string `json:"generateName"` -// Kind string `json:"kind"` -//} -// -//func TestTriggerParameterization(t *testing.T) { -// convey.Convey("Given an event, parameterize the trigger", t, func() { -// testSensor, err := getSensor() -// convey.So(err, convey.ShouldBeNil) -// soc := getsensorExecutionCtx(testSensor) -// triggerName := "test-workflow-trigger" -// dependency := "test-gateway:test" -// -// fe := &fakeEvent{ -// Namespace: "fake-namespace", -// Name: "fake", -// Group: "v1", -// GenerateName: "fake-", -// Kind: "Deployment", -// } -// eventBytes, err := json.Marshal(fe) -// convey.So(err, convey.ShouldBeNil) -// -// node := v1alpha1.NodeStatus{ -// Event: &apicommon.Event{ -// Payload: eventBytes, -// Context: apicommon.EventContext{ -// Source: &apicommon.URI{ -// Host: dependency, -// }, -// ContentType: "application/json", -// }, -// }, -// Name: dependency, -// Type: v1alpha1.NodeTypeEventDependency, -// ID: "1234", -// Phase: v1alpha1.NodePhaseActive, -// } -// -// trigger := triggerTemplate.DeepCopy() -// trigger.Template.Name = triggerName -// -// trigger.TemplateParameters = []v1alpha1.TriggerParameter{ -// { -// Src: &v1alpha1.TriggerParameterSource{ -// Event: dependency, -// Path: "name", -// }, -// Dest: "name", -// }, -// } -// -// trigger.ResourceParameters = []v1alpha1.TriggerParameter{ -// { -// Src: &v1alpha1.TriggerParameterSource{ -// Event: dependency, -// Path: "name", -// }, -// Dest: "metadata.generateName", -// }, -// } -// -// nodeId := soc.sensor.NodeID(dependency) -// wfNodeId := soc.sensor.NodeID(triggerName) -// -// wfnode := v1alpha1.NodeStatus{ -// Event: &apicommon.Event{ -// Payload: eventBytes, -// Context: apicommon.EventContext{ -// Source: &apicommon.URI{ -// Host: dependency, -// }, -// ContentType: "application/json", -// }, -// }, -// Name: triggerName, -// Type: v1alpha1.NodeTypeTrigger, -// ID: "1234", -// Phase: v1alpha1.NodePhaseNew, -// } -// -// soc.sensor.Status.Nodes = map[string]v1alpha1.NodeStatus{ -// nodeId: node, -// wfNodeId: wfnode, -// } -// -// err = soc.applyParamsTrigger(trigger) -// convey.So(err, convey.ShouldBeNil) -// convey.So(trigger.Template.Name, convey.ShouldEqual, fe.Name) -// -// rObj := podTemplate.DeepCopy() -// rObj.Name = "testTrigger" -// uObj, err := getUnstructured(rObj) -// convey.So(err, convey.ShouldBeNil) -// -// err = soc.applyParamsResource(trigger.ResourceParameters, uObj) -// convey.So(err, convey.ShouldBeNil) -// -// }) -//} -// -//func TestTriggerPolicy(t *testing.T) { -// convey.Convey("Given a trigger, apply policy", t, func() { -// testSensor, err := getSensor() -// convey.So(err, convey.ShouldBeNil) -// soc := getsensorExecutionCtx(testSensor) -// -// trigger1 := triggerTemplate.DeepCopy() -// trigger2 := triggerTemplate.DeepCopy() -// -// trigger1.Template.Name = "testTrigger1" -// trigger2.Template.Name = "testTrigger2" -// -// triggerPod1 := podTemplate.DeepCopy() -// triggerPod2 := podTemplate.DeepCopy() -// -// triggerPod1.Name = "testPod1" -// triggerPod2.Name = "testPod2" -// -// triggerPod1.Labels = successLabels -// triggerPod2.Labels = failureLabels -// -// uObj1, err := getUnstructured(triggerPod1) -// convey.So(err, convey.ShouldBeNil) -// -// uObj2, err := getUnstructured(triggerPod2) -// convey.So(err, convey.ShouldBeNil) -// -// backoff := v1alpha1.Backoff{ -// Duration: 1000000000, -// Factor: 2, -// Steps: 10, -// } -// -// trigger1.Template.Source = &v1alpha1.ArtifactLocation{ -// Resource: uObj1, -// } -// trigger1.Policy = &v1alpha1.TriggerPolicy{ -// Backoff: backoff, -// State: &v1alpha1.TriggerStateLabels{ -// Success: successLabels, -// }, -// } -// -// trigger2.Template.Source = &v1alpha1.ArtifactLocation{ -// Resource: uObj2, -// } -// trigger2.Policy = &v1alpha1.TriggerPolicy{ -// Backoff: backoff, -// State: &v1alpha1.TriggerStateLabels{ -// Failure: failureLabels, -// }, -// } -// -// convey.Convey("Execute the first trigger and make sure the trigger execution results in success", func() { -// err = soc.executeTrigger(*trigger1) -// convey.So(err, convey.ShouldBeNil) -// }) -// -// convey.Convey("Execute the second trigger and make sure the trigger execution results in failure", func() { -// err = soc.executeTrigger(*trigger2) -// convey.So(err, convey.ShouldNotBeNil) -// }) -// -// // modify backoff so that applyPolicy doesnt wait too much -// trigger1.Policy.Backoff = v1alpha1.Backoff{ -// Steps: 2, -// Duration: 1000000000, -// Factor: 1, -// } -// -// triggerPod1.Labels = nil -// uObj1, err = getUnstructured(triggerPod1) -// convey.So(err, convey.ShouldBeNil) -// trigger1.Template.Source.Resource = uObj1 -// -// convey.Convey("If trigger times out and error on timeout is set, trigger execution must fail", func() { -// trigger1.Policy.ErrorOnBackoffTimeout = true -// err = soc.executeTrigger(*trigger1) -// convey.So(err, convey.ShouldNotBeNil) -// }) -// -// convey.Convey("If trigger times out and error on timeout is not set, trigger execution must succeed", func() { -// trigger1.Policy.ErrorOnBackoffTimeout = false -// err = soc.executeTrigger(*trigger1) -// convey.So(err, convey.ShouldBeNil) -// }) -// }) -//} -// -//func TestCreateResourceObject(t *testing.T) { -// convey.Convey("Given a trigger", t, func() { -// testSensor, err := getSensor() -// convey.So(err, convey.ShouldBeNil) -// soc := getsensorExecutionCtx(testSensor) -// fakeclient := soc.dynamicClient.(*FakeClientPool).Fake -// dynamicClient := dynamicfake.FakeResourceClient{Resource: schema.GroupVersionResource{Version: "v1", Resource: "pods"}, Fake: &fakeclient} -// -// convey.Convey("Given a pod spec, create a pod trigger", func() { -// pod := podTemplate.DeepCopy() -// pod.Name = "testTrigger" -// pod.Namespace = "foo" -// uObj, err := getUnstructured(pod) -// convey.So(err, convey.ShouldBeNil) -// -// trigger := triggerTemplate.DeepCopy() -// trigger.Template.Name = "trigger" -// -// trigger.Template.Source = &v1alpha1.ArtifactLocation{ -// Resource: uObj, -// } -// -// convey.Println(trigger.Template.Source) -// -// err = soc.createResourceObject(trigger, uObj) -// convey.So(err, convey.ShouldBeNil) -// -// unstructuredPod, err := dynamicClient.Get(pod.Name, metav1.GetOptions{}) -// convey.So(err, convey.ShouldBeNil) -// convey.So(unstructuredPod.GetNamespace(), convey.ShouldEqual, "foo") -// }) -// -// convey.Convey("Given a pod without namespace,create a pod trigger", func() { -// pod := podTemplate.DeepCopy() -// pod.Name = "testTrigger" -// uObj, err := getUnstructured(pod) -// convey.So(err, convey.ShouldBeNil) -// -// trigger := triggerTemplate.DeepCopy() -// trigger.Template.Name = "trigger" -// -// trigger.Template.Source = &v1alpha1.ArtifactLocation{ -// Resource: uObj, -// } -// -// err = soc.createResourceObject(trigger, uObj) -// convey.So(err, convey.ShouldBeNil) -// -// unstructuredPod, err := dynamicClient.Get(pod.Name, metav1.GetOptions{}) -// convey.So(err, convey.ShouldBeNil) -// convey.So(unstructuredPod.GetNamespace(), convey.ShouldEqual, testSensor.Namespace) -// }) -// }) -//} -// -//func TestExtractEvents(t *testing.T) { -// convey.Convey("Given a sensor, extract events", t, func() { -// sensor, _ := getSensor() -// sec := getsensorExecutionCtx(sensor) -// id := sensor.NodeID("test-gateway:test") -// sensor.Status.Nodes = map[string]v1alpha1.NodeStatus{ -// id: { -// Type: v1alpha1.NodeTypeEventDependency, -// Event: &apicommon.Event{ -// Payload: []byte("hello"), -// Context: apicommon.EventContext{ -// Source: &apicommon.URI{ -// Host: "test-gateway:test", -// }, -// }, -// }, -// }, -// } -// extractedEvents := sec.extractEvents([]v1alpha1.TriggerParameter{ -// { -// Src: &v1alpha1.TriggerParameterSource{ -// Event: "test-gateway:test", -// }, -// Dest: "fake-dest", -// }, -// }) -// convey.So(len(extractedEvents), convey.ShouldEqual, 1) -// }) -//} -// -//func TestCanProcessTriggers(t *testing.T) { -// convey.Convey("Given a sensor, test if triggers can be processed", t, func() { -// sensor, err := getSensor() -// convey.So(err, convey.ShouldBeNil) -// -// sensor.Status.Nodes = map[string]v1alpha1.NodeStatus{ -// sensor.NodeID(sensor.Spec.Dependencies[0].Name): { -// Name: sensor.Spec.Dependencies[0].Name, -// Phase: v1alpha1.NodePhaseComplete, -// Type: v1alpha1.NodeTypeEventDependency, -// }, -// } -// -// for _, dep := range []v1alpha1.EventDependency{ -// { -// Name: "test-gateway:test2", -// }, -// { -// Name: "test-gateway:test3", -// }, -// } { -// sensor.Spec.Dependencies = append(sensor.Spec.Dependencies, dep) -// sensor.Status.Nodes[sensor.NodeID(dep.Name)] = v1alpha1.NodeStatus{ -// Name: dep.Name, -// Phase: v1alpha1.NodePhaseComplete, -// Type: v1alpha1.NodeTypeEventDependency, -// } -// } -// -// soc := getsensorExecutionCtx(sensor) -// ok, err := soc.canProcessTriggers() -// convey.So(err, convey.ShouldBeNil) -// convey.So(ok, convey.ShouldEqual, true) -// -// node := sensor.Status.Nodes[sensor.NodeID("test-gateway:test2")] -// node.Phase = v1alpha1.NodePhaseNew -// sensor.Status.Nodes[sensor.NodeID("test-gateway:test2")] = node -// -// ok, err = soc.canProcessTriggers() -// convey.So(err, convey.ShouldBeNil) -// convey.So(ok, convey.ShouldEqual, false) -// -// convey.Convey("Add dependency groups and evaluate the circuit", func() { -// for _, depGroup := range []v1alpha1.DependencyGroup{ -// { -// Name: "depg1", -// Dependencies: []string{sensor.Spec.Dependencies[1].Name, sensor.Spec.Dependencies[2].Name}, -// }, -// { -// Name: "depg2", -// Dependencies: []string{sensor.Spec.Dependencies[0].Name}, -// }, -// } { -// sensor.Spec.DependencyGroups = append(sensor.Spec.DependencyGroups, depGroup) -// sensor.Status.Nodes[sensor.NodeID(depGroup.Name)] = v1alpha1.NodeStatus{ -// Name: depGroup.Name, -// Phase: v1alpha1.NodePhaseNew, -// } -// } -// -// sensor.Spec.Circuit = "depg1 || depg2" -// -// ok, err = soc.canProcessTriggers() -// convey.So(err, convey.ShouldBeNil) -// convey.So(ok, convey.ShouldEqual, true) -// }) -// -// convey.Convey("If the previous round of triggers failed and error on previous round policy is set, then don't execute the triggers", func() { -// sensor.Spec.ErrorOnFailedRound = true -// sensor.Status.TriggerCycleStatus = v1alpha1.TriggerCycleFailure -// -// ok, err = soc.canProcessTriggers() -// convey.So(err, convey.ShouldNotBeNil) -// convey.So(ok, convey.ShouldEqual, false) -// }) -// }) -//} diff --git a/store/configmap_test.go b/store/configmap_test.go index f5d9cb09c0..a66b4c5bca 100644 --- a/store/configmap_test.go +++ b/store/configmap_test.go @@ -53,7 +53,7 @@ spec: convey.So(err, convey.ShouldBeNil) convey.So(cmReader, convey.ShouldNotBeNil) - convey.Convey("Create a workflow from configmap artifact", func() { + convey.Convey("Create a workflow from configmap minio", func() { resourceBody, err := cmReader.Read() convey.So(err, convey.ShouldBeNil) diff --git a/store/creds.go b/store/creds.go index 5aae8aadbc..143dff77ca 100644 --- a/store/creds.go +++ b/store/creds.go @@ -28,13 +28,13 @@ import ( "k8s.io/client-go/kubernetes" ) -// Credentials contains the information necessary to access the artifact +// Credentials contains the information necessary to access the minio type Credentials struct { accessKey string secretKey string } -// GetCredentials for this artifact +// GetCredentials for this minio func GetCredentials(kubeClient kubernetes.Interface, namespace string, art *v1alpha1.ArtifactLocation) (*Credentials, error) { if art.S3 != nil { accessKey, err := GetSecrets(kubeClient, namespace, art.S3.AccessKey.Name, art.S3.AccessKey.Key) diff --git a/store/creds_test.go b/store/creds_test.go index 160b5f7e48..01acbd60bc 100644 --- a/store/creds_test.go +++ b/store/creds_test.go @@ -41,13 +41,13 @@ func TestGetCredentials(t *testing.T) { _, err := fakeClient.CoreV1().Secrets("testing").Create(mySecretCredentials) assert.Nil(t, err) - // creds should be nil for unknown artifact type + // creds should be nil for unknown minio type unknownArtifact := &v1alpha1.ArtifactLocation{} creds, err := GetCredentials(fakeClient, "testing", unknownArtifact) assert.Nil(t, creds) assert.Nil(t, err) - // succeed for S3 artifact type + // succeed for S3 minio type s3Artifact := &v1alpha1.ArtifactLocation{ S3: &apicommon.S3Artifact{ AccessKey: &apiv1.SecretKeySelector{ diff --git a/store/git_test.go b/store/git_test.go index 9f3432dfd5..decb8c4fec 100644 --- a/store/git_test.go +++ b/store/git_test.go @@ -84,7 +84,7 @@ func TestGetGitAuth(t *testing.T) { } func TestGetBranchOrTag(t *testing.T) { - convey.Convey("Given a git artifact, get the branch or tag", t, func() { + convey.Convey("Given a git minio, get the branch or tag", t, func() { br := gar.getBranchOrTag() convey.So(br.Branch, convey.ShouldEqual, "refs/heads/master") gar.artifact.Branch = "br" @@ -95,7 +95,7 @@ func TestGetBranchOrTag(t *testing.T) { convey.So(tag.Branch, convey.ShouldNotEqual, "refs/heads/master") }) - convey.Convey("Given a git artifact with a specific ref, get the ref", t, func() { + convey.Convey("Given a git minio with a specific ref, get the ref", t, func() { gar.artifact.Ref = "refs/something/weird/or/specific" br := gar.getBranchOrTag() convey.So(br.Branch, convey.ShouldEqual, "refs/something/weird/or/specific") diff --git a/store/resource.go b/store/resource.go index e884771fe7..ea6d58a8d3 100644 --- a/store/resource.go +++ b/store/resource.go @@ -38,6 +38,6 @@ func NewResourceReader(resourceArtifact *unstructured.Unstructured) (ArtifactRea } func (reader *ResourceReader) Read() ([]byte, error) { - log.WithField("resource", reader.resourceArtifact.Object).Debug("reading artifact from resource template") + log.WithField("resource", reader.resourceArtifact.Object).Debug("reading minio from resource template") return yaml.Marshal(reader.resourceArtifact.Object) } diff --git a/store/store_test.go b/store/store_test.go index 530da5a2db..6f1c1bae49 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -18,12 +18,12 @@ package store import ( "io/ioutil" - "k8s.io/client-go/kubernetes/fake" "testing" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" ) type FakeWorkflowArtifactReader struct{} @@ -34,12 +34,12 @@ func (f *FakeWorkflowArtifactReader) Read() ([]byte, error) { func TestFetchArtifact(t *testing.T) { reader := &FakeWorkflowArtifactReader{} - gvk := &metav1.GroupVersionKind{ - Group: "argoproj.io", - Version: "v1alpha1", - Kind: "Workflow", + gvr := &metav1.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "workflows", } - obj, err := FetchArtifact(reader, gvk) + obj, err := FetchArtifact(reader, gvr) assert.Nil(t, err) assert.Equal(t, "argoproj.io/v1alpha1", obj.GetAPIVersion()) assert.Equal(t, "Workflow", obj.GetKind()) @@ -57,37 +57,27 @@ func TestGetArtifactReader(t *testing.T) { assert.NotNil(t, err) } -func TestDecodeAndUnstructure(t *testing.T) { - t.Run("sensor", decodeSensor) - t.Run("workflow", decodeWorkflow) - // Note that since #16 - Restrict ResourceObject creation via RBAC roles - // decoding&converting to unstructure objects should pass fine for any valid objects - // the store no longer should control restrictions around object creation - t.Run("unsupported", decodeUnsupported) - t.Run("unknown", decodeUnknown) -} - -func decodeSensor(t *testing.T) { +func TestDecodeSensor(t *testing.T) { b, err := ioutil.ReadFile("../examples/sensors/multi-trigger-sensor.yaml") assert.Nil(t, err) - gvk := &metav1.GroupVersionKind{ - Group: v1alpha1.SchemaGroupVersionKind.Group, - Version: v1alpha1.SchemaGroupVersionKind.Version, - Kind: v1alpha1.SchemaGroupVersionKind.Kind, + gvr := &metav1.GroupVersionResource{ + Group: v1alpha1.SchemaGroupVersionKind.Group, + Version: v1alpha1.SchemaGroupVersionKind.Version, + Resource: v1alpha1.Resource("sensors").Resource, } - _, err = decodeAndUnstructure(b, gvk) + _, err = decodeAndUnstructure(b, gvr) assert.Nil(t, err) } -func decodeWorkflow(t *testing.T) { - gvk := &metav1.GroupVersionKind{ - Group: "argoproj.io", - Version: "v1alpha1", - Kind: "Workflow", +func TestDecodeWorkflow(t *testing.T) { + gvr := &metav1.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "workflows", } - _, err := decodeAndUnstructure([]byte(workflowv1alpha1), gvk) + _, err := decodeAndUnstructure([]byte(workflowv1alpha1), gvr) assert.Nil(t, err) } @@ -106,13 +96,13 @@ spec: args: ["hello world"] ` -func decodeDeploymentv1(t *testing.T) { - gvk := &metav1.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", +func TestDecodeDeploymentv1(t *testing.T) { + gvr := &metav1.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", } - _, err := decodeAndUnstructure([]byte(deploymentv1), gvk) + _, err := decodeAndUnstructure([]byte(deploymentv1), gvr) assert.Nil(t, err) } @@ -157,13 +147,13 @@ var deploymentv1 = ` } ` -func decodeJobv1(t *testing.T) { - gvk := &metav1.GroupVersionKind{ - Group: "batch", - Version: "v1", - Kind: "Job", +func TestDecodeJobv1(t *testing.T) { + gvr := &metav1.GroupVersionResource{ + Group: "batch", + Version: "v1", + Resource: "jobs", } - _, err := decodeAndUnstructure([]byte(jobv1), gvk) + _, err := decodeAndUnstructure([]byte(jobv1), gvr) assert.Nil(t, err) } @@ -187,13 +177,13 @@ spec: restartPolicy: Never ` -func decodeUnsupported(t *testing.T) { - gvk := &metav1.GroupVersionKind{ - Group: "batch", - Version: "v1", - Kind: "Job", +func TestDecodeUnsupported(t *testing.T) { + gvr := &metav1.GroupVersionResource{ + Group: "batch", + Version: "v1", + Resource: "jobs", } - _, err := decodeAndUnstructure([]byte(unsupportedType), gvk) + _, err := decodeAndUnstructure([]byte(unsupportedType), gvr) assert.Nil(t, err) } @@ -224,12 +214,12 @@ spec: done ` -func decodeUnknown(t *testing.T) { - gvk := &metav1.GroupVersionKind{ - Group: "unknown", - Version: "123", - Kind: "What??", +func TestDecodeUnknown(t *testing.T) { + gvr := &metav1.GroupVersionResource{ + Group: "unknown", + Version: "123", + Resource: "What??", } - _, err := decodeAndUnstructure([]byte(unsupportedType), gvk) + _, err := decodeAndUnstructure([]byte(unsupportedType), gvr) assert.Nil(t, err, "expected nil error but got", err) } diff --git a/test/e2e/common/client.go b/test/e2e/common/client.go deleted file mode 100644 index 7675674a03..0000000000 --- a/test/e2e/common/client.go +++ /dev/null @@ -1,129 +0,0 @@ -package common - -import ( - "bytes" - "fmt" - "math/rand" - "net/http" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - gwv1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - sv1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - gwclient "github.com/argoproj/argo-events/pkg/client/gateway/clientset/versioned" - sensorclient "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" - "github.com/pkg/errors" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/portforward" - "k8s.io/client-go/transport/spdy" -) - -func init() { - // Add custom schemes - if err := sv1.AddToScheme(scheme.Scheme); err != nil { - panic(err) - } - if err := gwv1.AddToScheme(scheme.Scheme); err != nil { - panic(err) - } -} - -type E2EClient struct { - Config *restclient.Config - KubeClient kubernetes.Interface - GwClient gwclient.Interface - SnClient sensorclient.Interface - E2EID string - ClientID string -} - -func NewE2EClient() (*E2EClient, error) { - var kubeconfig string - if os.Getenv("KUBECONFIG") != "" { - kubeconfig = os.Getenv("KUBECONFIG") - } else { - kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube/config") - } - config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) - if err != nil { - return nil, err - } - - kubeClient, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, err - } - - gwClient, err := gwclient.NewForConfig(config) - if err != nil { - return nil, err - } - - sensorClient, err := sensorclient.NewForConfig(config) - if err != nil { - return nil, err - } - - myrand := rand.New(rand.NewSource(time.Now().UnixNano())) - clientID := strconv.FormatUint(myrand.Uint64(), 16) - - return &E2EClient{ - Config: config, - KubeClient: kubeClient, - GwClient: gwClient, - SnClient: sensorClient, - ClientID: clientID, - }, nil -} - -func (clpl *E2EClient) ForwardServicePort(tmpNamespace, podName string, localPort, targetPort int) (chan struct{}, error) { - // Implementation ref: https://github.com/kubernetes/client-go/issues/51#issuecomment-436200428 - roundTripper, upgrader, err := spdy.RoundTripperFor(clpl.Config) - if err != nil { - return nil, err - } - - path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", tmpNamespace, podName) - hostIP := strings.TrimLeft(clpl.Config.Host, "https://") - serverURL := url.URL{Scheme: "https", Path: path, Host: hostIP} - - dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, &serverURL) - - stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) - - portDesc := fmt.Sprintf("%d:%d", localPort, targetPort) - out, errOut := new(bytes.Buffer), new(bytes.Buffer) - forwarder, err := portforward.New(dialer, []string{portDesc}, stopChan, readyChan, out, errOut) - if err != nil { - return nil, err - } - - go func() { - err = forwarder.ForwardPorts() - if err != nil { - fmt.Printf("%+v\n", err) - } - }() - - err = nil -L: - for { - select { - case <-time.After(10 * time.Second): - err = errors.New("timed out port forwarding") - break L - case <-readyChan: - break L - default: - } - } - - return stopChan, err -} diff --git a/test/e2e/core/main_test.go b/test/e2e/core/main_test.go deleted file mode 100644 index 737300c506..0000000000 --- a/test/e2e/core/main_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package core - -import ( - "fmt" - "io/ioutil" - "net" - "net/http" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - gwalpha1 "github.com/argoproj/argo-events/pkg/apis/gateway/v1alpha1" - snv1alpha1 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" - e2ecommon "github.com/argoproj/argo-events/test/e2e/common" - "github.com/ghodss/yaml" - "github.com/smartystreets/goconvey/convey" - corev1 "k8s.io/api/core/v1" - apierr "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const NAMESPACE = "argo-events" - -func TestGeneralUseCase(t *testing.T) { - client, err := e2ecommon.NewE2EClient() - if err != nil { - t.Fatal(err) - } - - _, filename, _, _ := runtime.Caller(0) - dir, err := filepath.Abs(filepath.Dir(filename)) - if err != nil { - t.Fatal(err) - } - manifestsDir := filepath.Join(dir, "manifests", "general-use-case") - - convey.Convey("Test the general use case", t, func() { - - convey.Convey("Create event source", func() { - esBytes, err := ioutil.ReadFile(filepath.Join(manifestsDir, "webhook-gateway-event-source.yaml")) - if err != nil { - convey.ShouldPanic(err) - } - var cm *corev1.ConfigMap - if err := yaml.Unmarshal(esBytes, &cm); err != nil { - convey.ShouldPanic(err) - } - if _, err = client.KubeClient.CoreV1().ConfigMaps(NAMESPACE).Create(cm); err != nil { - convey.ShouldPanic(err) - } - }) - - convey.Convey("Create a gateway.", func() { - gwBytes, err := ioutil.ReadFile(filepath.Join(manifestsDir, "webhook-gateway.yaml")) - if err != nil { - convey.ShouldPanic(err) - } - var gw *gwalpha1.Gateway - if err := yaml.Unmarshal(gwBytes, &gw); err != nil { - convey.ShouldPanic(err) - } - if _, err = client.GwClient.ArgoprojV1alpha1().Gateways(NAMESPACE).Create(gw); err != nil { - convey.ShouldPanic(err) - } - }) - - convey.Convey("Create a sensor.", func() { - swBytes, err := ioutil.ReadFile(filepath.Join(manifestsDir, "webhook-sensor.yaml")) - if err != nil { - convey.ShouldPanic(err) - } - var sn *snv1alpha1.Sensor - if err := yaml.Unmarshal(swBytes, &sn); err != nil { - convey.ShouldPanic(err) - } - if _, err = client.SnClient.ArgoprojV1alpha1().Sensors(NAMESPACE).Create(sn); err != nil { - convey.ShouldPanic(err) - } - }) - - convey.Convey("Wait for corresponding resources.", func() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - var gwpod, spod *corev1.Pod - var gwsvc *corev1.Service - for { - if gwpod == nil { - pod, err := client.KubeClient.CoreV1().Pods(NAMESPACE).Get("webhook-gateway", metav1.GetOptions{}) - if err != nil && !apierr.IsNotFound(err) { - t.Fatal(err) - } - _, _ = yaml.Marshal(pod) - if pod != nil && pod.Status.Phase == corev1.PodRunning { - gwpod = pod - } - } - - if gwsvc == nil { - svc, err := client.KubeClient.CoreV1().Services(NAMESPACE).Get("webhook-gateway-svc", metav1.GetOptions{}) - if err != nil && !apierr.IsNotFound(err) { - t.Fatal(err) - } - gwsvc = svc - } - if spod == nil { - pod, err := client.KubeClient.CoreV1().Pods(NAMESPACE).Get("webhook-sensor", metav1.GetOptions{}) - if err != nil && !apierr.IsNotFound(err) { - t.Fatal(err) - } - if pod != nil && pod.Status.Phase == corev1.PodRunning { - spod = pod - } - } - if gwpod != nil && gwsvc != nil && spod != nil { - break - } - } - }) - - convey.Convey("Make a request to the gateway.", func() { - // Avoid too early access - time.Sleep(5 * time.Second) - - // Use available port - l, _ := net.Listen("tcp", ":0") - port := l.Addr().(*net.TCPAddr).Port - l.Close() - - // Use port forwarding to access pods in minikube - stopChan, err := client.ForwardServicePort(NAMESPACE, "webhook-gateway", port, 12000) - if err != nil { - t.Fatal(err) - } - defer close(stopChan) - - url := fmt.Sprintf("http://localhost:%d/foo", port) - req, err := http.NewRequest("POST", url, strings.NewReader("e2e")) - if err != nil { - t.Fatal(err) - } - - resp, err := new(http.Client).Do(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - - if t.Failed() { - t.FailNow() - } - }) - - convey.Convey("Check if the sensor trigggered a pod.", func() { - pod, err := client.KubeClient.CoreV1().Pods(NAMESPACE).Get("webhook-sensor-triggered-pod", metav1.GetOptions{}) - if err != nil && !apierr.IsNotFound(err) { - t.Error(err) - } - if pod != nil && pod.Status.Phase == corev1.PodSucceeded { - convey.So(pod.Spec.Containers[0].Args[0], convey.ShouldEqual, "e2e") - } - }) - }) -} diff --git a/test/e2e/core/manifests/general-use-case/webhook-gateway-event-source.yaml b/test/e2e/core/manifests/general-use-case/webhook-gateway-event-source.yaml deleted file mode 100644 index d274e271ae..0000000000 --- a/test/e2e/core/manifests/general-use-case/webhook-gateway-event-source.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: webhook-event-source - labels: - # do not remove - argo-events-event-source-version: v0.11 -data: - foo: |- - # port to run HTTP server on - port: "12000" - # endpoint to listen to - endpoint: "/index" - # HTTP request method to allow. In this case, only POST requests are accepted - method: "POST" diff --git a/test/e2e/core/manifests/general-use-case/webhook-gateway.yaml b/test/e2e/core/manifests/general-use-case/webhook-gateway.yaml deleted file mode 100644 index 0aac8fe809..0000000000 --- a/test/e2e/core/manifests/general-use-case/webhook-gateway.yaml +++ /dev/null @@ -1,56 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Gateway -metadata: - name: webhook-gateway - labels: - # gateway controller with instanceId "argo-events" will process this gateway - gateways.argoproj.io/gateway-controller-instanceid: argo-events - # gateway controller will use this label to match with it's own version - # do not remove - argo-events-gateway-version: v0.11 -spec: - type: "webhook" - eventSource: "webhook-event-source" - processorPort: "9330" - eventProtocol: - type: "HTTP" - http: - port: "9300" - template: - metadata: - name: "webhook-gateway" - labels: - gateway-name: "webhook-gateway" - spec: - containers: - - name: "gateway-client" - image: "argoproj/gateway-client:v0.11" - imagePullPolicy: "Always" - command: ["/bin/gateway-client"] - - name: "webhook-events" - image: "argoproj/webhook-gateway:v0.11" - imagePullPolicy: "Always" - command: ["/bin/webhook-gateway"] - # To make webhook secure, mount the secret that contains certificate and private key in the container - # and refer that mountPath in the event source. - # volumeMounts: - # - mountPath: "/bin/webhook-secure" - # name: secure - # volumes: - # - name: secure - # secret: - # secretName: webhook-secure - serviceAccountName: "argo-events-sa" - service: - metadata: - name: webhook-gateway-svc - spec: - selector: - gateway-name: "webhook-gateway" - ports: - - port: 12000 - targetPort: 12000 - type: ClusterIP - watchers: - sensors: - - name: "webhook-sensor" diff --git a/test/e2e/core/manifests/general-use-case/webhook-sensor.yaml b/test/e2e/core/manifests/general-use-case/webhook-sensor.yaml deleted file mode 100644 index 2c494b56bb..0000000000 --- a/test/e2e/core/manifests/general-use-case/webhook-sensor.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: webhook-sensor - labels: - sensors.argoproj.io/sensor-controller-instanceid: argo-events - # sensor controller will use this label to match with it's own version - # do not remove - argo-events-sensor-version: v0.11 -spec: - template: - spec: - containers: - - name: "sensor" - image: "argoproj/sensor:v0.11" - imagePullPolicy: "IfNotPresent" - serviceAccountName: argo-events-sa - dependencies: - - name: "webhook-gateway:foo" - eventProtocol: - type: "HTTP" - http: - port: "9300" - triggers: - - template: - name: webhook-pod-trigger - version: v1 - kind: Pod - source: - inline: | - apiVersion: v1 - kind: Pod - metadata: - name: webhook-sensor-triggered-pod - spec: - containers: - - name: whalesay - image: docker/whalesay:latest - command: [cowsay] - args: ["TO_BE_PASSED"] - restartPolicy: "Never" - resourceParameters: - - src: - event: "webhook-gateway:foo" - dest: spec.containers.0.args.0 diff --git a/version.go b/version.go index 6eb89f17d4..5047bafc7c 100644 --- a/version.go +++ b/version.go @@ -24,7 +24,7 @@ import ( // Version information set by link flags during build. We fall back to these sane // default values when we build outside the Makefile context (e.g. go build or go test). var ( - version = "v0.11" // value from VERSION file + version = "v0.12-rc" // value from VERSION file buildDate = "1970-01-01T00:00:00Z" // output from `date -u +'%Y-%m-%dT%H:%M:%SZ'` gitCommit = "" // output from `git rev-parse HEAD` gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state)