diff --git a/Gopkg.lock b/Gopkg.lock index 81597fe9ee3..a3e5d5fbee2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -676,6 +676,14 @@ pruneopts = "NUT" revision = "3113b8401b8a98917cde58f8bbd42a1b1c03b1fd" +[[projects]] + digest = "1:53c3320ee307f01fd24a88e396a8d2239cd8346d1a085320209319f2d33f59cc" + name = "github.com/robfig/cron" + packages = ["."] + pruneopts = "NUT" + revision = "b41be1df696709bb6395fe435af20370037c0b4c" + version = "v1.1" + [[projects]] digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04" name = "github.com/sergi/go-diff" @@ -780,7 +788,8 @@ revision = "1e491301e022f8f977054da4c2d852decd59571f" [[projects]] - digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44" + branch = "master" + digest = "1:3121d742fbe48670a16d98b6da4693501fc33cd76d69ed6f35850c564f255c65" name = "golang.org/x/oauth2" packages = [ ".", @@ -790,7 +799,7 @@ "jwt", ] pruneopts = "NUT" - revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269" + revision = "9f3314589c9a9136388751d9adae6b0ed400978a" [[projects]] branch = "master" @@ -1358,7 +1367,10 @@ "github.com/Shopify/sarama", "github.com/bsm/sarama-cluster", "github.com/cloudevents/sdk-go", + "github.com/cloudevents/sdk-go/pkg/cloudevents", + "github.com/cloudevents/sdk-go/pkg/cloudevents/client", "github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http", + "github.com/cloudevents/sdk-go/pkg/cloudevents/types", "github.com/fsnotify/fsnotify", "github.com/google/go-cmp/cmp", "github.com/google/go-cmp/cmp/cmpopts", @@ -1371,6 +1383,7 @@ "github.com/knative/pkg/client/listers/istio/v1alpha3", "github.com/knative/pkg/configmap", "github.com/knative/pkg/controller", + "github.com/knative/pkg/kmeta", "github.com/knative/pkg/kmp", "github.com/knative/pkg/logging", "github.com/knative/pkg/logging/logkey", @@ -1388,6 +1401,7 @@ "github.com/nats-io/go-nats-streaming", "github.com/nats-io/nats-streaming-server/server", "github.com/prometheus/client_golang/prometheus/promhttp", + "github.com/robfig/cron", "go.opencensus.io/exporter/prometheus", "go.opencensus.io/stats", "go.opencensus.io/stats/view", @@ -1397,6 +1411,7 @@ "go.uber.org/zap", "go.uber.org/zap/zapcore", "go.uber.org/zap/zaptest/observer", + "golang.org/x/net/context", "golang.org/x/oauth2/google", "google.golang.org/api/option", "gopkg.in/yaml.v2", @@ -1426,6 +1441,7 @@ "k8s.io/client-go/dynamic", "k8s.io/client-go/dynamic/fake", "k8s.io/client-go/informers", + "k8s.io/client-go/informers/apps/v1", "k8s.io/client-go/informers/core/v1", "k8s.io/client-go/kubernetes", "k8s.io/client-go/kubernetes/fake", @@ -1437,6 +1453,7 @@ "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/record", "k8s.io/client-go/util/flowcontrol", "k8s.io/client-go/util/workqueue", diff --git a/cmd/controller/main.go b/cmd/controller/main.go index f1993c336a0..34aee80848a 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -104,12 +104,15 @@ func startPkgController(stopCh <-chan struct{}, cfg *rest.Config, logger *zap.Su kubeInformerFactory := kubeinformers.NewSharedInformerFactory(opt.KubeClientSet, opt.ResyncPeriod) eventingInformerFactory := informers.NewSharedInformerFactory(opt.EventingClientSet, opt.ResyncPeriod) + // Eventing triggerInformer := eventingInformerFactory.Eventing().V1alpha1().Triggers() channelInformer := eventingInformerFactory.Eventing().V1alpha1().Channels() subscriptionInformer := eventingInformerFactory.Eventing().V1alpha1().Subscriptions() brokerInformer := eventingInformerFactory.Eventing().V1alpha1().Brokers() - coreServiceInformer := kubeInformerFactory.Core().V1().Services() - coreNamespaceInformer := kubeInformerFactory.Core().V1().Namespaces() + + // Kube + serviceInformer := kubeInformerFactory.Core().V1().Services() + namespaceInformer := kubeInformerFactory.Core().V1().Namespaces() configMapInformer := kubeInformerFactory.Core().V1().ConfigMaps() // Build all of our controllers, with the clients constructed above. @@ -122,7 +125,7 @@ func startPkgController(stopCh <-chan struct{}, cfg *rest.Config, logger *zap.Su ), namespace.NewController( opt, - coreNamespaceInformer, + namespaceInformer, ), channel.NewController( opt, @@ -134,7 +137,7 @@ func startPkgController(stopCh <-chan struct{}, cfg *rest.Config, logger *zap.Su channelInformer, subscriptionInformer, brokerInformer, - coreServiceInformer, + serviceInformer, ), } if len(controllers) != numControllers { @@ -153,13 +156,15 @@ func startPkgController(stopCh <-chan struct{}, cfg *rest.Config, logger *zap.Su logger.Info("Starting informers.") if err := kncontroller.StartInformers( stopCh, + // Eventing + brokerInformer.Informer(), + channelInformer.Informer(), subscriptionInformer.Informer(), - configMapInformer.Informer(), - coreNamespaceInformer.Informer(), triggerInformer.Informer(), - channelInformer.Informer(), - brokerInformer.Informer(), - coreServiceInformer.Informer(), + // Kube + configMapInformer.Informer(), + serviceInformer.Informer(), + namespaceInformer.Informer(), ); err != nil { logger.Fatalf("Failed to start informers: %v", err) } diff --git a/cmd/cronjob_receive_adapter/main.go b/cmd/cronjob_receive_adapter/main.go new file mode 100644 index 00000000000..965ad2af176 --- /dev/null +++ b/cmd/cronjob_receive_adapter/main.go @@ -0,0 +1,74 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "log" + "os" + + "github.com/knative/eventing/pkg/adapter/cronjobevents" + "github.com/knative/pkg/signals" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/net/context" +) + +const ( + // Environment variable container schedule. + envSchedule = "SCHEDULE" + + // Environment variable containing data. + envData = "DATA" + + // Sink for messages. + envSinkURI = "SINK_URI" +) + +func getRequiredEnv(envKey string) string { + val, defined := os.LookupEnv(envKey) + if !defined { + log.Fatalf("required environment variable not defined %q", envKey) + } + return val +} + +func main() { + flag.Parse() + + ctx := context.Background() + logCfg := zap.NewProductionConfig() + logCfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + logger, err := logCfg.Build() + if err != nil { + log.Fatalf("Unable to create logger: %v", err) + } + + adapter := &cronjobevents.Adapter{ + Schedule: getRequiredEnv(envSchedule), + Data: getRequiredEnv(envData), + SinkURI: getRequiredEnv(envSinkURI), + } + + logger.Info("Starting Receive Adapter", zap.Reflect("adapter", adapter)) + + stopCh := signals.SetupSignalHandler() + + if err := adapter.Start(ctx, stopCh); err != nil { + logger.Fatal("Failed to start adapter", zap.Error(err)) + } +} diff --git a/cmd/sources-controller/kodata/HEAD b/cmd/sources-controller/kodata/HEAD new file mode 120000 index 00000000000..8f63681d362 --- /dev/null +++ b/cmd/sources-controller/kodata/HEAD @@ -0,0 +1 @@ +../../../.git/HEAD \ No newline at end of file diff --git a/cmd/sources-controller/kodata/LICENSE b/cmd/sources-controller/kodata/LICENSE new file mode 120000 index 00000000000..5853aaea53b --- /dev/null +++ b/cmd/sources-controller/kodata/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/cmd/sources-controller/kodata/VENDOR-LICENSE b/cmd/sources-controller/kodata/VENDOR-LICENSE new file mode 120000 index 00000000000..3cc89764519 --- /dev/null +++ b/cmd/sources-controller/kodata/VENDOR-LICENSE @@ -0,0 +1 @@ +../../../third_party/VENDOR-LICENSE \ No newline at end of file diff --git a/cmd/sources-controller/main.go b/cmd/sources-controller/main.go new file mode 100644 index 00000000000..6abe075e348 --- /dev/null +++ b/cmd/sources-controller/main.go @@ -0,0 +1,163 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "k8s.io/client-go/tools/clientcmd" + "log" + + // Uncomment the following line to load the gcp plugin (only required to authenticate against GKE clusters). + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + + kubeinformers "k8s.io/client-go/informers" + "k8s.io/client-go/rest" + + informers "github.com/knative/eventing/pkg/client/informers/externalversions" + "github.com/knative/eventing/pkg/logconfig" + "github.com/knative/eventing/pkg/logging" + "github.com/knative/eventing/pkg/reconciler" + "github.com/knative/eventing/pkg/reconciler/cronjobsource" + "github.com/knative/pkg/configmap" + kncontroller "github.com/knative/pkg/controller" + "github.com/knative/pkg/logging/logkey" + "github.com/knative/pkg/signals" + "go.uber.org/zap" +) + +var ( + hardcodedLoggingConfig = flag.Bool("hardCodedLoggingConfig", false, "If true, use the hard coded logging config. It is intended to be used only when debugging outside a Kubernetes cluster.") + masterURL = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") +) + +func main() { + flag.Parse() + + logger, atomicLevel := setupLogger() + defer logger.Sync() + logger = logger.With(zap.String(logkey.ControllerType, logconfig.SourcesController)) + + // set up signals so we handle the first shutdown signal gracefully + stopCh := signals.SetupSignalHandler() + + cfg, err := clientcmd.BuildConfigFromFlags(*masterURL, *kubeconfig) + if err != nil { + logger.Fatalw("Error building kubeconfig", zap.Error(err)) + } + + logger = logger.With(zap.String("controller/impl", "pkg")) + logger.Info("Starting the controller") + + const numControllers = 1 + cfg.QPS = numControllers * rest.DefaultQPS + cfg.Burst = numControllers * rest.DefaultBurst + opt := reconciler.NewOptionsOrDie(cfg, logger, stopCh) + + kubeInformerFactory := kubeinformers.NewSharedInformerFactory(opt.KubeClientSet, opt.ResyncPeriod) + eventingInformerFactory := informers.NewSharedInformerFactory(opt.EventingClientSet, opt.ResyncPeriod) + + // Eventing + cronjobsourceInformer := eventingInformerFactory.Sources().V1alpha1().CronJobSources() + + // Kube + deploymentInformer := kubeInformerFactory.Apps().V1().Deployments() + + // Build all of our controllers, with the clients constructed above. + // Add new controllers to this array. + // You also need to modify numControllers above to match this. + controllers := []*kncontroller.Impl{ + cronjobsource.NewController( + opt, + cronjobsourceInformer, + deploymentInformer, + ), + } + if len(controllers) != numControllers { + logger.Fatalf("Number of controllers and QPS settings mismatch: %d != %d", len(controllers), numControllers) + } + + // Watch the logging config map and dynamically update logging levels. + opt.ConfigMapWatcher.Watch(logconfig.ConfigMapName(), logging.UpdateLevelFromConfigMap(logger, atomicLevel, logconfig.SourcesController)) + // TODO: Watch the observability config map and dynamically update metrics exporter. + //opt.ConfigMapWatcher.Watch(metrics.ObservabilityConfigName, metrics.UpdateExporterFromConfigMap(component, logger)) + if err := opt.ConfigMapWatcher.Start(stopCh); err != nil { + logger.Fatalw("failed to start configuration manager", zap.Error(err)) + } + + // Start all of the informers and wait for them to sync. + logger.Info("Starting informers.") + if err := kncontroller.StartInformers( + stopCh, + // Eventing + cronjobsourceInformer.Informer(), + // Kube + deploymentInformer.Informer(), + ); err != nil { + logger.Fatalf("Failed to start informers: %v", err) + } + + // Start all of the controllers. + logger.Info("Starting controllers.") + go kncontroller.StartAll(stopCh, controllers...) + + <-stopCh +} + +func setupLogger() (*zap.SugaredLogger, zap.AtomicLevel) { + // Set up our logger. + loggingConfigMap := getLoggingConfigOrDie() + loggingConfig, err := logging.NewConfigFromMap(loggingConfigMap) + if err != nil { + log.Fatalf("Error parsing logging configuration: %v", err) + } + return logging.NewLoggerFromConfig(loggingConfig, logconfig.Controller) +} + +func getLoggingConfigOrDie() map[string]string { + if hardcodedLoggingConfig != nil && *hardcodedLoggingConfig { + return map[string]string{ + "loglevel.controller": "info", + "zap-logger-config": ` + { + "level": "info", + "development": false, + "outputPaths": ["stdout"], + "errorOutputPaths": ["stderr"], + "encoding": "json", + "encoderConfig": { + "timeKey": "ts", + "levelKey": "level", + "nameKey": "logger", + "callerKey": "caller", + "messageKey": "msg", + "stacktraceKey": "stacktrace", + "lineEnding": "", + "levelEncoder": "", + "timeEncoder": "iso8601", + "durationEncoder": "", + "callerEncoder": "" + }`, + } + } else { + cm, err := configmap.Load("/etc/config-logging") + if err != nil { + log.Fatalf("Error loading logging configuration: %v", err) + } + return cm + } +} diff --git a/config/200-controller-clusterrole.yaml b/config/200-controller-clusterrole.yaml index 7b0d29bca4c..44a8d3f1408 100644 --- a/config/200-controller-clusterrole.yaml +++ b/config/200-controller-clusterrole.yaml @@ -26,7 +26,7 @@ rules: - "services" - "events" - "serviceaccounts" - verbs: + verbs: &everything - "get" - "list" - "create" @@ -40,42 +40,21 @@ rules: - "networking.istio.io" resources: - "virtualservices" - verbs: - - "get" - - "list" - - "create" - - "update" - - "delete" - - "patch" - - "watch" + verbs: *everything # Brokers and the namespace annotation controllers manipulate Deployments. - apiGroups: - "apps" resources: - "deployments" - verbs: - - "get" - - "list" - - "create" - - "update" - - "delete" - - "patch" - - "watch" + verbs: *everything # The namespace annotation controller needs to manipulate RoleBindings. - apiGroups: - "rbac.authorization.k8s.io" resources: - "rolebindings" - verbs: - - "get" - - "list" - - "create" - - "update" - - "delete" - - "patch" - - "watch" + verbs: *everything # Our own resources and statuses we care about. - apiGroups: @@ -91,11 +70,13 @@ rules: - "subscriptions/status" - "triggers" - "triggers/status" - verbs: - - "get" - - "list" - - "create" - - "update" - - "delete" - - "patch" - - "watch" + verbs: *everything + + # Source resources and statuses we care about. + - apiGroups: + - "sources.eventing.knative.dev" + resources: + - "cronjobsources" + - "cronjobsources/status" + - "cronjobsources/finalizers" + verbs: *everything diff --git a/config/300-cronjobsource.yaml b/config/300-cronjobsource.yaml new file mode 100644 index 00000000000..50974d4718e --- /dev/null +++ b/config/300-cronjobsource.yaml @@ -0,0 +1,84 @@ +# Copyright 2019 The Knative 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. + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + labels: + eventing.knative.dev/source: "true" + knative.dev/crd-install: "true" + name: cronjobsources.sources.eventing.knative.dev +spec: + group: sources.eventing.knative.dev + names: + categories: + - all + - knative + - eventing + - sources + kind: CronJobSource + plural: cronjobsources + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + data: + type: string + schedule: + type: string + serviceAccountName: + type: string + sink: + type: object + required: + - schedule + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + # we use a string in the stored object but a wrapper object + # at runtime. + type: string + message: + type: string + reason: + type: string + severity: + type: string + status: + type: string + type: + type: string + required: + - type + - status + type: object + type: array + sinkUri: + type: string + type: object + version: v1alpha1 diff --git a/config/400-source-controller-service.yaml b/config/400-source-controller-service.yaml new file mode 100644 index 00000000000..3b37422a88b --- /dev/null +++ b/config/400-source-controller-service.yaml @@ -0,0 +1,30 @@ +# Copyright 2019 The Knative 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 +# +# https://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. + +apiVersion: v1 +kind: Service +metadata: + labels: + app: sources-controller + serving.knative.dev/release: devel + name: sources-controller + namespace: knative-eventing +spec: + ports: + - name: metrics + port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: sources-controller \ No newline at end of file diff --git a/config/500-controller.yaml b/config/500-controller.yaml index f1466e1a55a..2cb7d815c06 100644 --- a/config/500-controller.yaml +++ b/config/500-controller.yaml @@ -11,6 +11,7 @@ # 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. + apiVersion: apps/v1 kind: Deployment metadata: @@ -49,6 +50,8 @@ spec: value: github.com/knative/eventing/cmd/broker/filter - name: BROKER_FILTER_SERVICE_ACCOUNT value: eventing-broker-filter + - name: CRONJOB_RA_IMAGE + value: github.com/knative/eventing/cmd/cronjob_receive_adapter ports: - containerPort: 9090 name: metrics diff --git a/config/500-sources-controller.yaml b/config/500-sources-controller.yaml new file mode 100644 index 00000000000..6f77f264418 --- /dev/null +++ b/config/500-sources-controller.yaml @@ -0,0 +1,68 @@ +# Copyright 2019 The Knative 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sources-controller + namespace: knative-eventing + labels: + eventing.knative.dev/release: devel +spec: + replicas: 1 + selector: + matchLabels: + app: sources-controller + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + labels: + app: sources-controller + eventing.knative.dev/release: devel + spec: + serviceAccountName: eventing-controller + containers: + - name: controller + # This is the Go import path for the binary that is containerized + # and substituted here. + image: github.com/knative/eventing/cmd/sources-controller + resources: + requests: + cpu: 100m + memory: 100Mi + limits: + cpu: 1000m + memory: 1000Mi + ports: + - name: metrics + containerPort: 9090 + volumeMounts: + - name: config-logging + mountPath: /etc/config-logging + env: + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONFIG_LOGGING_NAME + value: config-logging + - name: CRONJOB_RA_IMAGE + # This is the Go import path for cron job receive adapter binary + # that is containerized and substituted here. + value: github.com/knative/eventing/cmd/cronjob_receive_adapter + volumes: + - name: config-logging + configMap: + name: config-logging diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index b651c1fc19a..dea5391fec7 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -28,7 +28,7 @@ CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${REPO_ROOT_DIR}; ls -d -1 ./vendor/k8s.io/code- # instead of the $GOPATH directly. For normal projects this can be dropped. ${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \ github.com/knative/eventing/pkg/client github.com/knative/eventing/pkg/apis \ - "eventing:v1alpha1" \ + "eventing:v1alpha1 sources:v1alpha1" \ --go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt # Only deepcopy the Duck types, as they are not real resources. diff --git a/pkg/adapter/cronjobevents/adapter.go b/pkg/adapter/cronjobevents/adapter.go new file mode 100644 index 00000000000..ee3146e3472 --- /dev/null +++ b/pkg/adapter/cronjobevents/adapter.go @@ -0,0 +1,114 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cronjobevents + +import ( + "context" + "encoding/json" + + "github.com/cloudevents/sdk-go/pkg/cloudevents" + "github.com/cloudevents/sdk-go/pkg/cloudevents/client" + "github.com/cloudevents/sdk-go/pkg/cloudevents/types" + "github.com/knative/eventing/pkg/kncloudevents" + "github.com/knative/pkg/logging" + "github.com/robfig/cron" + "go.uber.org/zap" +) + +const ( + eventType = "dev.knative.cronjob.event" +) + +// TODO: this should be a k8s cron. + +// Adapter implements the Cron Job adapter to trigger a Sink. +type Adapter struct { + // Schedule is a cron format string such as 0 * * * * or @hourly + Schedule string + + // Data is the data to be posted to the target. + Data string + + // SinkURI is the URI messages will be forwarded on to. + SinkURI string + + // client sends cloudevents. + client client.Client +} + +// Initialize cloudevent client +func (a *Adapter) initClient() error { + if a.client == nil { + var err error + if a.client, err = kncloudevents.NewDefaultClient(a.SinkURI); err != nil { + return err + } + } + return nil +} + +func (a *Adapter) Start(ctx context.Context, stopCh <-chan struct{}) error { + logger := logging.FromContext(ctx) + + sched, err := cron.ParseStandard(a.Schedule) + if err != nil { + logger.Error("Unparseable schedule: ", a.Schedule, zap.Error(err)) + return err + } + + if err = a.initClient(); err != nil { + logger.Error("Failed to create cloudevent client", zap.Error(err)) + return err + } + + c := cron.New() + c.Schedule(sched, cron.FuncJob(a.cronTick)) + c.Start() + <-stopCh + c.Stop() + logger.Info("Shutting down.") + return nil +} + +func (a *Adapter) cronTick() { + logger := logging.FromContext(context.TODO()) + + event := cloudevents.Event{ + Context: cloudevents.EventContextV02{ + Type: eventType, + Source: *types.ParseURLRef("/CronJob"), + }.AsV02(), + Data: message(a.Data), + } + if _, err := a.client.Send(context.TODO(), event); err != nil { + logger.Error("failed to send cloudevent", err) + } +} + +type Message struct { + Body string `json:"body"` +} + +func message(body string) interface{} { + // try to marshal the body into an interface. + var objmap map[string]*json.RawMessage + if err := json.Unmarshal([]byte(body), &objmap); err != nil { + //default to a wrapped message. + return Message{Body: body} + } + return objmap +} diff --git a/pkg/adapter/cronjobevents/adapter_test.go b/pkg/adapter/cronjobevents/adapter_test.go new file mode 100644 index 00000000000..ae9ca48dd42 --- /dev/null +++ b/pkg/adapter/cronjobevents/adapter_test.go @@ -0,0 +1,208 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cronjobevents + +import ( + "context" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStart_ServeHTTP(t *testing.T) { + testCases := map[string]struct { + schedule string + sink func(http.ResponseWriter, *http.Request) + reqBody string + error bool + }{ + "happy": { + schedule: "* * * * *", // every minute + sink: sinkAccepted, + reqBody: `{"body":"data"}`, + }, + "rejected": { + schedule: "* * * * *", // every minute + sink: sinkRejected, + reqBody: `{"body":"data"}`, + error: true, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + h := &fakeHandler{ + handler: tc.sink, + } + sinkServer := httptest.NewServer(h) + defer sinkServer.Close() + + a := &Adapter{ + Schedule: tc.schedule, + Data: "data", + SinkURI: sinkServer.URL, + } + + if err := a.initClient(); err != nil { + t.Errorf("failed to create cloudevent client, %v", err) + } + + stop := make(chan struct{}) + go func() { + if err := a.Start(context.TODO(), stop); err != nil { + if tc.error { + // skip + } else { + t.Errorf("failed to start, %v", err) + } + } + }() + + a.cronTick() // force a tick. + + if tc.reqBody != string(h.body) { + t.Errorf("expected request body %q, but got %q", tc.reqBody, h.body) + } + log.Print("test done") + }) + } +} + +func TestStartBadCron(t *testing.T) { + schedule := "bad" + + a := &Adapter{ + Schedule: schedule, + } + + stop := make(chan struct{}) + if err := a.Start(context.TODO(), stop); err == nil { + + t.Errorf("failed to fail, %v", err) + + } +} + +func TestPostMessage_ServeHTTP(t *testing.T) { + testCases := map[string]struct { + sink func(http.ResponseWriter, *http.Request) + reqBody string + error bool + }{ + "happy": { + sink: sinkAccepted, + reqBody: `{"body":"data"}`, + }, + "rejected": { + sink: sinkRejected, + reqBody: `{"body":"data"}`, + error: true, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + h := &fakeHandler{ + handler: tc.sink, + } + sinkServer := httptest.NewServer(h) + defer sinkServer.Close() + + a := &Adapter{ + Data: "data", + SinkURI: sinkServer.URL, + } + + if err := a.initClient(); err != nil { + t.Errorf("failed to create cloudevent client, %v", err) + } + + a.cronTick() + + if tc.reqBody != string(h.body) { + t.Errorf("expected request body %q, but got %q", tc.reqBody, h.body) + } + }) + } +} + +func TestMessage(t *testing.T) { + testCases := map[string]struct { + body string + want string + }{ + "json simple": { + body: `{"message": "Hello world!"}`, + want: `{"message":"Hello world!"}`, + }, + "json complex": { + body: `{"message": "Hello world!","extra":{"a":"sub", "b":[1,2,3]}}`, + want: `{"extra":{"a":"sub","b":[1,2,3]},"message":"Hello world!"}`, + }, + "string": { + body: "Hello, World!", + want: `{"body":"Hello, World!"}`, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + m := message(tc.body) + + j, err := json.Marshal(m) + if err != nil { + t.Errorf("failed to marshel message: %v", err) + } + + got := string(j) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("%s: (-want, +got) = %v", n, diff) + } + }) + } +} + +type fakeHandler struct { + body []byte + ran int + handler func(http.ResponseWriter, *http.Request) +} + +func (h *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "can not read body", http.StatusBadRequest) + return + } + h.body = body + + defer r.Body.Close() + h.handler(w, r) + + h.ran++ +} + +func sinkAccepted(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusOK) +} + +func sinkRejected(writer http.ResponseWriter, _ *http.Request) { + writer.WriteHeader(http.StatusRequestTimeout) +} diff --git a/pkg/apis/duck/v1alpha1/doc.go b/pkg/apis/duck/v1alpha1/doc.go index 3e5d1e3bc5a..5019094b1e5 100644 --- a/pkg/apis/duck/v1alpha1/doc.go +++ b/pkg/apis/duck/v1alpha1/doc.go @@ -1,9 +1,12 @@ /* -Copyright 2018 The Knative Authors +Copyright 2019 The Knative 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. diff --git a/pkg/apis/duck/v1alpha1/subscribable_types.go b/pkg/apis/duck/v1alpha1/subscribable_types.go index 740a035ead3..407a7912407 100644 --- a/pkg/apis/duck/v1alpha1/subscribable_types.go +++ b/pkg/apis/duck/v1alpha1/subscribable_types.go @@ -1,18 +1,18 @@ /* - * Copyright 2018 The Knative 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. - */ +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package v1alpha1 diff --git a/pkg/apis/duck/v1alpha1/subscribable_types_test.go b/pkg/apis/duck/v1alpha1/subscribable_types_test.go index ccc6512ab36..5052347a97f 100644 --- a/pkg/apis/duck/v1alpha1/subscribable_types_test.go +++ b/pkg/apis/duck/v1alpha1/subscribable_types_test.go @@ -1,18 +1,18 @@ /* - * Copyright 2018 The Knative 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. - */ +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package v1alpha1 diff --git a/pkg/apis/eventing/v1alpha1/doc.go b/pkg/apis/eventing/v1alpha1/doc.go index e080fe737d2..1b017951a8a 100644 --- a/pkg/apis/eventing/v1alpha1/doc.go +++ b/pkg/apis/eventing/v1alpha1/doc.go @@ -1,9 +1,12 @@ /* -Copyright 2018 The Knative Authors +Copyright 2019 The Knative 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. diff --git a/pkg/apis/sources/register.go b/pkg/apis/sources/register.go new file mode 100644 index 00000000000..4d2a4477695 --- /dev/null +++ b/pkg/apis/sources/register.go @@ -0,0 +1,21 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sources + +const ( + GroupName = "sources.eventing.knative.dev" +) diff --git a/pkg/apis/sources/v1alpha1/cron_job_lifecycle.go b/pkg/apis/sources/v1alpha1/cron_job_lifecycle.go new file mode 100644 index 00000000000..c51d6d4feec --- /dev/null +++ b/pkg/apis/sources/v1alpha1/cron_job_lifecycle.go @@ -0,0 +1,96 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" +) + +const ( + // CronJobConditionReady has status True when the CronJobSource is ready to send events. + CronJobConditionReady = duckv1alpha1.ConditionReady + + // CronJobConditionValidSchedule has status True when the CronJobSource has been configured with a valid schedule. + CronJobConditionValidSchedule duckv1alpha1.ConditionType = "ValidSchedule" + + // CronJobConditionSinkProvided has status True when the CronJobSource has been configured with a sink target. + CronJobConditionSinkProvided duckv1alpha1.ConditionType = "SinkProvided" + + // CronJobConditionDeployed has status True when the CronJobSource has had it's receive adapter deployment created. + CronJobConditionDeployed duckv1alpha1.ConditionType = "Deployed" +) + +var cronJobSourceCondSet = duckv1alpha1.NewLivingConditionSet( + CronJobConditionValidSchedule, + CronJobConditionSinkProvided, + CronJobConditionDeployed) + +// GetCondition returns the condition currently associated with the given type, or nil. +func (s *CronJobSourceStatus) GetCondition(t duckv1alpha1.ConditionType) *duckv1alpha1.Condition { + return cronJobSourceCondSet.Manage(s).GetCondition(t) +} + +// IsReady returns true if the resource is ready overall. +func (s *CronJobSourceStatus) IsReady() bool { + return cronJobSourceCondSet.Manage(s).IsHappy() +} + +// InitializeConditions sets relevant unset conditions to Unknown state. +func (s *CronJobSourceStatus) InitializeConditions() { + cronJobSourceCondSet.Manage(s).InitializeConditions() +} + +// TODO: this is a bad method name, change it. +// MarkSchedule sets the condition that the source has a valid schedule configured. +func (s *CronJobSourceStatus) MarkSchedule() { + cronJobSourceCondSet.Manage(s).MarkTrue(CronJobConditionValidSchedule) +} + +// MarkInvalidSchedule sets the condition that the source does not have a valid schedule configured. +func (s *CronJobSourceStatus) MarkInvalidSchedule(reason, messageFormat string, messageA ...interface{}) { + cronJobSourceCondSet.Manage(s).MarkFalse(CronJobConditionValidSchedule, reason, messageFormat, messageA...) +} + +// MarkSink sets the condition that the source has a sink configured. +func (s *CronJobSourceStatus) MarkSink(uri string) { + s.SinkURI = uri + if len(uri) > 0 { + cronJobSourceCondSet.Manage(s).MarkTrue(CronJobConditionSinkProvided) + } else { + cronJobSourceCondSet.Manage(s).MarkUnknown(CronJobConditionSinkProvided, "SinkEmpty", "Sink has resolved to empty.%s", "") + } +} + +// MarkNoSink sets the condition that the source does not have a sink configured. +func (s *CronJobSourceStatus) MarkNoSink(reason, messageFormat string, messageA ...interface{}) { + cronJobSourceCondSet.Manage(s).MarkFalse(CronJobConditionSinkProvided, reason, messageFormat, messageA...) +} + +// MarkDeployed sets the condition that the source has been deployed. +func (s *CronJobSourceStatus) MarkDeployed() { + cronJobSourceCondSet.Manage(s).MarkTrue(CronJobConditionDeployed) +} + +// MarkDeploying sets the condition that the source is deploying. +func (s *CronJobSourceStatus) MarkDeploying(reason, messageFormat string, messageA ...interface{}) { + cronJobSourceCondSet.Manage(s).MarkUnknown(CronJobConditionDeployed, reason, messageFormat, messageA...) +} + +// MarkNotDeployed sets the condition that the source has not been deployed. +func (s *CronJobSourceStatus) MarkNotDeployed(reason, messageFormat string, messageA ...interface{}) { + cronJobSourceCondSet.Manage(s).MarkFalse(CronJobConditionDeployed, reason, messageFormat, messageA...) +} diff --git a/pkg/apis/sources/v1alpha1/cron_job_lifecycle_test.go b/pkg/apis/sources/v1alpha1/cron_job_lifecycle_test.go new file mode 100644 index 00000000000..9dd7aaefb88 --- /dev/null +++ b/pkg/apis/sources/v1alpha1/cron_job_lifecycle_test.go @@ -0,0 +1,376 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1_test + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" +) + +func TestCronJobSourceStatusIsReady(t *testing.T) { + tests := []struct { + name string + s *v1alpha1.CronJobSourceStatus + want bool + }{{ + name: "uninitialized", + s: &v1alpha1.CronJobSourceStatus{}, + want: false, + }, { + name: "initialized", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + return s + }(), + want: false, + }, { + name: "mark deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkDeployed() + return s + }(), + want: false, + }, { + name: "mark sink", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSink("uri://example") + return s + }(), + want: false, + }, { + name: "mark schedule", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + return s + }(), + want: false, + }, { + name: "mark sink and deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSink("uri://example") + s.MarkDeployed() + return s + }(), + want: false, + }, { + name: "mark schedule and sink", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + return s + }(), + want: false, + }, { + name: "mark schedule, sink and deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + return s + }(), + want: true, + }, { + name: "mark schedule, sink and deployed then not deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + s.MarkNotDeployed("Testing", "") + return s + }(), + want: false, + }, { + name: "mark schedule, sink and not deployed then deploying then deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkNotDeployed("MarkNotDeployed", "") + s.MarkDeploying("MarkDeploying", "") + s.MarkDeployed() + return s + }(), + want: true, + }, { + name: "mark schedule validated, sink empty and deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("") + s.MarkDeployed() + return s + }(), + want: false, + }, { + name: "mark schedule validated, sink empty and deployed then sink", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("") + s.MarkDeployed() + s.MarkSink("uri://example") + return s + }(), + want: true, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.s.IsReady() + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("%s: unexpected condition (-want, +got) = %v", test.name, diff) + } + }) + } +} + +func TestCronJobSourceStatusGetCondition(t *testing.T) { + tests := []struct { + name string + s *v1alpha1.CronJobSourceStatus + condQuery duckv1alpha1.ConditionType + want *duckv1alpha1.Condition + }{{ + name: "uninitialized", + s: &v1alpha1.CronJobSourceStatus{}, + condQuery: v1alpha1.CronJobConditionReady, + want: nil, + }, { + name: "initialized", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionUnknown, + }, + }, { + name: "mark deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkDeployed() + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionUnknown, + }, + }, { + name: "mark sink", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSink("uri://example") + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionUnknown, + }, + }, { + name: "mark schedule", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionUnknown, + }, + }, { + name: "mark schedule, sink and deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionTrue, + }, + }, { + name: "mark schedule, sink and deployed then no sink", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + s.MarkNoSink("Testing", "hi%s", "") + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionFalse, + Reason: "Testing", + Message: "hi", + }, + }, { + name: "mark schedule, sink and deployed then invalid schedule", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + s.MarkInvalidSchedule("Testing", "hi%s", "") + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionFalse, + Reason: "Testing", + Message: "hi", + }, + }, { + name: "mark schedule, sink and deployed then deploying", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + s.MarkDeploying("Testing", "hi%s", "") + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionUnknown, + Reason: "Testing", + Message: "hi", + }, + }, { + name: "mark schedule, sink and deployed then not deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkDeployed() + s.MarkNotDeployed("Testing", "hi%s", "") + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionFalse, + Reason: "Testing", + Message: "hi", + }, + }, { + name: "mark schedule, sink and not deployed then deploying then deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("uri://example") + s.MarkNotDeployed("MarkNotDeployed", "%s", "") + s.MarkDeploying("MarkDeploying", "%s", "") + s.MarkDeployed() + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionTrue, + }, + }, { + name: "mark schedule, sink empty and deployed", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("") + s.MarkDeployed() + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionUnknown, + Reason: "SinkEmpty", + Message: "Sink has resolved to empty.", + }, + }, { + name: "mark schedule, sink empty and deployed then sink", + s: func() *v1alpha1.CronJobSourceStatus { + s := &v1alpha1.CronJobSourceStatus{} + s.InitializeConditions() + s.MarkSchedule() + s.MarkSink("") + s.MarkDeployed() + s.MarkSink("uri://example") + return s + }(), + condQuery: v1alpha1.CronJobConditionReady, + want: &duckv1alpha1.Condition{ + Type: v1alpha1.CronJobConditionReady, + Status: corev1.ConditionTrue, + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.s.GetCondition(test.condQuery) + ignoreTime := cmpopts.IgnoreFields(duckv1alpha1.Condition{}, + "LastTransitionTime", "Severity") + if diff := cmp.Diff(test.want, got, ignoreTime); diff != "" { + t.Errorf("unexpected condition (-want, +got) = %v", diff) + } + }) + } +} diff --git a/pkg/apis/sources/v1alpha1/cron_job_types.go b/pkg/apis/sources/v1alpha1/cron_job_types.go new file mode 100644 index 00000000000..439184aba9d --- /dev/null +++ b/pkg/apis/sources/v1alpha1/cron_job_types.go @@ -0,0 +1,96 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "github.com/knative/pkg/apis/duck" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + "github.com/knative/pkg/kmeta" + 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" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:defaulter-gen=true + +// CronJobSource is the Schema for the cronjobsources API. +type CronJobSource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CronJobSourceSpec `json:"spec,omitempty"` + Status CronJobSourceStatus `json:"status,omitempty"` +} + +// TODO: Check that CronJobSource can be validated and can be defaulted. + +// Check that it is a runtime object. +var _ runtime.Object = (*CronJobSource)(nil) + +// Check that we can create OwnerReferences to a Configuration. +var _ kmeta.OwnerRefable = (*CronJobSource)(nil) + +// Check that CronJobSource implements the Conditions duck type. +var _ = duck.VerifyType(&CronJobSource{}, &duckv1alpha1.Conditions{}) + +// CronJobSourceSpec defines the desired state of the CronJobSource. +type CronJobSourceSpec struct { + + // Schedule is the cronjob schedule. + // +required + Schedule string `json:"schedule"` + + // Data is the data posted to the target function. + Data string `json:"data,omitempty"` + + // Sink is a reference to an object that will resolve to a domain name to use as the sink. + // +optional + Sink *corev1.ObjectReference `json:"sink,omitempty"` + + // ServiceAccoutName is the name of the ServiceAccount that will be used to run the Receive + // Adapter Deployment. + ServiceAccountName string `json:"serviceAccountName,omitempty"` +} + +// GetGroupVersionKind returns the GroupVersionKind. +func (s *CronJobSource) GetGroupVersionKind() schema.GroupVersionKind { + return SchemeGroupVersion.WithKind("CronJobSource") +} + +// CronJobSourceStatus defines the observed state of CronJobSource. +type CronJobSourceStatus struct { + // inherits duck/v1alpha1 Status, which currently provides: + // * ObservedGeneration - the 'Generation' of the Service that was last processed by the controller. + // * Conditions - the latest available observations of a resource's current state. + duckv1alpha1.Status `json:",inline"` + + // SinkURI is the current active sink URI that has been configured for the CronJobSource. + // +optional + SinkURI string `json:"sinkUri,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CronJobSourceList contains a list of CronJobSources. +type CronJobSourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CronJobSource `json:"items"` +} diff --git a/pkg/apis/sources/v1alpha1/doc.go b/pkg/apis/sources/v1alpha1/doc.go new file mode 100644 index 00000000000..7d7f6738fbc --- /dev/null +++ b/pkg/apis/sources/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the sources v1alpha1 API group +// +k8s:deepcopy-gen=package +// +groupName=sources.eventing.knative.dev +package v1alpha1 diff --git a/pkg/apis/sources/v1alpha1/register.go b/pkg/apis/sources/v1alpha1/register.go new file mode 100644 index 00000000000..a170bc79c54 --- /dev/null +++ b/pkg/apis/sources/v1alpha1/register.go @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "github.com/knative/eventing/pkg/apis/sources" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: sources.GroupName, Version: "v1alpha1"} + +// 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 an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &CronJobSource{}, + &CronJobSourceList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/sources/v1alpha1/register_test.go b/pkg/apis/sources/v1alpha1/register_test.go new file mode 100644 index 00000000000..2c4e17d8b5e --- /dev/null +++ b/pkg/apis/sources/v1alpha1/register_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/google/go-cmp/cmp" +) + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func TestResource(t *testing.T) { + want := schema.GroupResource{ + Group: "sources.eventing.knative.dev", + Resource: "foo", + } + + got := Resource("foo") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected resource (-want, +got) = %v", diff) + } +} + +// Kind takes an unqualified resource and returns a Group qualified GroupKind +func TestKind(t *testing.T) { + want := schema.GroupKind{ + Group: "sources.eventing.knative.dev", + Kind: "kind", + } + + got := Kind("kind") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected resource (-want, +got) = %v", diff) + } +} + +// TestKnownTypes makes sure that expected types get added. +func TestKnownTypes(t *testing.T) { + scheme := runtime.NewScheme() + addKnownTypes(scheme) + types := scheme.KnownTypes(SchemeGroupVersion) + + for _, name := range []string{ + "CronJobSource", + "CronJobSourceList", + } { + if _, ok := types[name]; !ok { + t.Errorf("Did not find %q as registered type", name) + } + } + +} diff --git a/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..686c603a5e3 --- /dev/null +++ b/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,125 @@ +// +build !ignore_autogenerated + +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/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 *CronJobSource) DeepCopyInto(out *CronJobSource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSource. +func (in *CronJobSource) DeepCopy() *CronJobSource { + if in == nil { + return nil + } + out := new(CronJobSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CronJobSource) 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 *CronJobSourceList) DeepCopyInto(out *CronJobSourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CronJobSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSourceList. +func (in *CronJobSourceList) DeepCopy() *CronJobSourceList { + if in == nil { + return nil + } + out := new(CronJobSourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CronJobSourceList) 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 *CronJobSourceSpec) DeepCopyInto(out *CronJobSourceSpec) { + *out = *in + if in.Sink != nil { + in, out := &in.Sink, &out.Sink + *out = new(v1.ObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSourceSpec. +func (in *CronJobSourceSpec) DeepCopy() *CronJobSourceSpec { + if in == nil { + return nil + } + out := new(CronJobSourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CronJobSourceStatus) DeepCopyInto(out *CronJobSourceStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSourceStatus. +func (in *CronJobSourceStatus) DeepCopy() *CronJobSourceStatus { + if in == nil { + return nil + } + out := new(CronJobSourceStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index e93eb9138e5..526251ec438 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -20,6 +20,7 @@ package versioned import ( eventingv1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/eventing/v1alpha1" + sourcesv1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/sources/v1alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" @@ -30,6 +31,9 @@ type Interface interface { EventingV1alpha1() eventingv1alpha1.EventingV1alpha1Interface // Deprecated: please explicitly pick a version if possible. Eventing() eventingv1alpha1.EventingV1alpha1Interface + SourcesV1alpha1() sourcesv1alpha1.SourcesV1alpha1Interface + // Deprecated: please explicitly pick a version if possible. + Sources() sourcesv1alpha1.SourcesV1alpha1Interface } // Clientset contains the clients for groups. Each group has exactly one @@ -37,6 +41,7 @@ type Interface interface { type Clientset struct { *discovery.DiscoveryClient eventingV1alpha1 *eventingv1alpha1.EventingV1alpha1Client + sourcesV1alpha1 *sourcesv1alpha1.SourcesV1alpha1Client } // EventingV1alpha1 retrieves the EventingV1alpha1Client @@ -50,6 +55,17 @@ func (c *Clientset) Eventing() eventingv1alpha1.EventingV1alpha1Interface { return c.eventingV1alpha1 } +// SourcesV1alpha1 retrieves the SourcesV1alpha1Client +func (c *Clientset) SourcesV1alpha1() sourcesv1alpha1.SourcesV1alpha1Interface { + return c.sourcesV1alpha1 +} + +// Deprecated: Sources retrieves the default version of SourcesClient. +// Please explicitly pick a version. +func (c *Clientset) Sources() sourcesv1alpha1.SourcesV1alpha1Interface { + return c.sourcesV1alpha1 +} + // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { @@ -70,6 +86,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { if err != nil { return nil, err } + cs.sourcesV1alpha1, err = sourcesv1alpha1.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) if err != nil { @@ -83,6 +103,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset cs.eventingV1alpha1 = eventingv1alpha1.NewForConfigOrDie(c) + cs.sourcesV1alpha1 = sourcesv1alpha1.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) return &cs @@ -92,6 +113,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { func New(c rest.Interface) *Clientset { var cs Clientset cs.eventingV1alpha1 = eventingv1alpha1.New(c) + cs.sourcesV1alpha1 = sourcesv1alpha1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index 5fc64b9c1c7..fbb1c267d76 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -22,6 +22,8 @@ import ( clientset "github.com/knative/eventing/pkg/client/clientset/versioned" eventingv1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/eventing/v1alpha1" fakeeventingv1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake" + sourcesv1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/sources/v1alpha1" + fakesourcesv1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" @@ -80,3 +82,13 @@ func (c *Clientset) EventingV1alpha1() eventingv1alpha1.EventingV1alpha1Interfac func (c *Clientset) Eventing() eventingv1alpha1.EventingV1alpha1Interface { return &fakeeventingv1alpha1.FakeEventingV1alpha1{Fake: &c.Fake} } + +// SourcesV1alpha1 retrieves the SourcesV1alpha1Client +func (c *Clientset) SourcesV1alpha1() sourcesv1alpha1.SourcesV1alpha1Interface { + return &fakesourcesv1alpha1.FakeSourcesV1alpha1{Fake: &c.Fake} +} + +// Sources retrieves the SourcesV1alpha1Client +func (c *Clientset) Sources() sourcesv1alpha1.SourcesV1alpha1Interface { + return &fakesourcesv1alpha1.FakeSourcesV1alpha1{Fake: &c.Fake} +} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index e29260ec25f..28e19da277e 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -20,6 +20,7 @@ package fake import ( eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -32,6 +33,7 @@ var codecs = serializer.NewCodecFactory(scheme) var parameterCodec = runtime.NewParameterCodec(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ eventingv1alpha1.AddToScheme, + sourcesv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 5df4708af46..b78dadc4c61 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -20,6 +20,7 @@ package scheme import ( eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -32,6 +33,7 @@ var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ eventingv1alpha1.AddToScheme, + sourcesv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/cronjobsource.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/cronjobsource.go new file mode 100644 index 00000000000..bb6a1257c6f --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/cronjobsource.go @@ -0,0 +1,174 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + scheme "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// CronJobSourcesGetter has a method to return a CronJobSourceInterface. +// A group's client should implement this interface. +type CronJobSourcesGetter interface { + CronJobSources(namespace string) CronJobSourceInterface +} + +// CronJobSourceInterface has methods to work with CronJobSource resources. +type CronJobSourceInterface interface { + Create(*v1alpha1.CronJobSource) (*v1alpha1.CronJobSource, error) + Update(*v1alpha1.CronJobSource) (*v1alpha1.CronJobSource, error) + UpdateStatus(*v1alpha1.CronJobSource) (*v1alpha1.CronJobSource, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.CronJobSource, error) + List(opts v1.ListOptions) (*v1alpha1.CronJobSourceList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.CronJobSource, err error) + CronJobSourceExpansion +} + +// cronJobSources implements CronJobSourceInterface +type cronJobSources struct { + client rest.Interface + ns string +} + +// newCronJobSources returns a CronJobSources +func newCronJobSources(c *SourcesV1alpha1Client, namespace string) *cronJobSources { + return &cronJobSources{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the cronJobSource, and returns the corresponding cronJobSource object, and an error if there is any. +func (c *cronJobSources) Get(name string, options v1.GetOptions) (result *v1alpha1.CronJobSource, err error) { + result = &v1alpha1.CronJobSource{} + err = c.client.Get(). + Namespace(c.ns). + Resource("cronjobsources"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CronJobSources that match those selectors. +func (c *cronJobSources) List(opts v1.ListOptions) (result *v1alpha1.CronJobSourceList, err error) { + result = &v1alpha1.CronJobSourceList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("cronjobsources"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested cronJobSources. +func (c *cronJobSources) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("cronjobsources"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a cronJobSource and creates it. Returns the server's representation of the cronJobSource, and an error, if there is any. +func (c *cronJobSources) Create(cronJobSource *v1alpha1.CronJobSource) (result *v1alpha1.CronJobSource, err error) { + result = &v1alpha1.CronJobSource{} + err = c.client.Post(). + Namespace(c.ns). + Resource("cronjobsources"). + Body(cronJobSource). + Do(). + Into(result) + return +} + +// Update takes the representation of a cronJobSource and updates it. Returns the server's representation of the cronJobSource, and an error, if there is any. +func (c *cronJobSources) Update(cronJobSource *v1alpha1.CronJobSource) (result *v1alpha1.CronJobSource, err error) { + result = &v1alpha1.CronJobSource{} + err = c.client.Put(). + Namespace(c.ns). + Resource("cronjobsources"). + Name(cronJobSource.Name). + Body(cronJobSource). + 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 *cronJobSources) UpdateStatus(cronJobSource *v1alpha1.CronJobSource) (result *v1alpha1.CronJobSource, err error) { + result = &v1alpha1.CronJobSource{} + err = c.client.Put(). + Namespace(c.ns). + Resource("cronjobsources"). + Name(cronJobSource.Name). + SubResource("status"). + Body(cronJobSource). + Do(). + Into(result) + return +} + +// Delete takes name of the cronJobSource and deletes it. Returns an error if one occurs. +func (c *cronJobSources) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("cronjobsources"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *cronJobSources) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("cronjobsources"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched cronJobSource. +func (c *cronJobSources) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.CronJobSource, err error) { + result = &v1alpha1.CronJobSource{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("cronjobsources"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/doc.go new file mode 100644 index 00000000000..a1c6bb9fe8f --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/doc.go new file mode 100644 index 00000000000..a00e5d7b21a --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_cronjobsource.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_cronjobsource.go new file mode 100644 index 00000000000..a7b30f81401 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_cronjobsource.go @@ -0,0 +1,140 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCronJobSources implements CronJobSourceInterface +type FakeCronJobSources struct { + Fake *FakeSourcesV1alpha1 + ns string +} + +var cronjobsourcesResource = schema.GroupVersionResource{Group: "sources.eventing.knative.dev", Version: "v1alpha1", Resource: "cronjobsources"} + +var cronjobsourcesKind = schema.GroupVersionKind{Group: "sources.eventing.knative.dev", Version: "v1alpha1", Kind: "CronJobSource"} + +// Get takes name of the cronJobSource, and returns the corresponding cronJobSource object, and an error if there is any. +func (c *FakeCronJobSources) Get(name string, options v1.GetOptions) (result *v1alpha1.CronJobSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(cronjobsourcesResource, c.ns, name), &v1alpha1.CronJobSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CronJobSource), err +} + +// List takes label and field selectors, and returns the list of CronJobSources that match those selectors. +func (c *FakeCronJobSources) List(opts v1.ListOptions) (result *v1alpha1.CronJobSourceList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(cronjobsourcesResource, cronjobsourcesKind, c.ns, opts), &v1alpha1.CronJobSourceList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.CronJobSourceList{ListMeta: obj.(*v1alpha1.CronJobSourceList).ListMeta} + for _, item := range obj.(*v1alpha1.CronJobSourceList).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 cronJobSources. +func (c *FakeCronJobSources) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(cronjobsourcesResource, c.ns, opts)) + +} + +// Create takes the representation of a cronJobSource and creates it. Returns the server's representation of the cronJobSource, and an error, if there is any. +func (c *FakeCronJobSources) Create(cronJobSource *v1alpha1.CronJobSource) (result *v1alpha1.CronJobSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(cronjobsourcesResource, c.ns, cronJobSource), &v1alpha1.CronJobSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CronJobSource), err +} + +// Update takes the representation of a cronJobSource and updates it. Returns the server's representation of the cronJobSource, and an error, if there is any. +func (c *FakeCronJobSources) Update(cronJobSource *v1alpha1.CronJobSource) (result *v1alpha1.CronJobSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(cronjobsourcesResource, c.ns, cronJobSource), &v1alpha1.CronJobSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CronJobSource), 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 *FakeCronJobSources) UpdateStatus(cronJobSource *v1alpha1.CronJobSource) (*v1alpha1.CronJobSource, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(cronjobsourcesResource, "status", c.ns, cronJobSource), &v1alpha1.CronJobSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CronJobSource), err +} + +// Delete takes name of the cronJobSource and deletes it. Returns an error if one occurs. +func (c *FakeCronJobSources) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(cronjobsourcesResource, c.ns, name), &v1alpha1.CronJobSource{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCronJobSources) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(cronjobsourcesResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.CronJobSourceList{}) + return err +} + +// Patch applies the patch and returns the patched cronJobSource. +func (c *FakeCronJobSources) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.CronJobSource, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(cronjobsourcesResource, c.ns, name, data, subresources...), &v1alpha1.CronJobSource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CronJobSource), err +} diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go new file mode 100644 index 00000000000..c2d9faeb94a --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/fake/fake_sources_client.go @@ -0,0 +1,40 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/knative/eventing/pkg/client/clientset/versioned/typed/sources/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeSourcesV1alpha1 struct { + *testing.Fake +} + +func (c *FakeSourcesV1alpha1) CronJobSources(namespace string) v1alpha1.CronJobSourceInterface { + return &FakeCronJobSources{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeSourcesV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go new file mode 100644 index 00000000000..e3e7bf27492 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type CronJobSourceExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go b/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go new file mode 100644 index 00000000000..31733ad3ea4 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sources/v1alpha1/sources_client.go @@ -0,0 +1,90 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + rest "k8s.io/client-go/rest" +) + +type SourcesV1alpha1Interface interface { + RESTClient() rest.Interface + CronJobSourcesGetter +} + +// SourcesV1alpha1Client is used to interact with features provided by the sources.eventing.knative.dev group. +type SourcesV1alpha1Client struct { + restClient rest.Interface +} + +func (c *SourcesV1alpha1Client) CronJobSources(namespace string) CronJobSourceInterface { + return newCronJobSources(c, namespace) +} + +// NewForConfig creates a new SourcesV1alpha1Client for the given config. +func NewForConfig(c *rest.Config) (*SourcesV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &SourcesV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new SourcesV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *SourcesV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new SourcesV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *SourcesV1alpha1Client { + return &SourcesV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + + 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 *SourcesV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index f5f0897b7c9..c591485735a 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -26,6 +26,7 @@ import ( versioned "github.com/knative/eventing/pkg/client/clientset/versioned" eventing "github.com/knative/eventing/pkg/client/informers/externalversions/eventing" internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" + sources "github.com/knative/eventing/pkg/client/informers/externalversions/sources" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -173,8 +174,13 @@ type SharedInformerFactory interface { WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool Eventing() eventing.Interface + Sources() sources.Interface } func (f *sharedInformerFactory) Eventing() eventing.Interface { return eventing.New(f, f.namespace, f.tweakListOptions) } + +func (f *sharedInformerFactory) Sources() sources.Interface { + return sources.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 8aa9b1ad555..013b0b1e9fc 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -22,6 +22,7 @@ import ( "fmt" v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -64,6 +65,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case v1alpha1.SchemeGroupVersion.WithResource("triggers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().Triggers().Informer()}, nil + // Group=sources.eventing.knative.dev, Version=v1alpha1 + case sourcesv1alpha1.SchemeGroupVersion.WithResource("cronjobsources"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sources().V1alpha1().CronJobSources().Informer()}, nil + } return nil, fmt.Errorf("no informer found for %v", resource) diff --git a/pkg/client/informers/externalversions/sources/interface.go b/pkg/client/informers/externalversions/sources/interface.go new file mode 100644 index 00000000000..d638f2a8578 --- /dev/null +++ b/pkg/client/informers/externalversions/sources/interface.go @@ -0,0 +1,46 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package sources + +import ( + internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/knative/eventing/pkg/client/informers/externalversions/sources/v1alpha1" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/sources/v1alpha1/cronjobsource.go b/pkg/client/informers/externalversions/sources/v1alpha1/cronjobsource.go new file mode 100644 index 00000000000..c6d831f8ecc --- /dev/null +++ b/pkg/client/informers/externalversions/sources/v1alpha1/cronjobsource.go @@ -0,0 +1,89 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + versioned "github.com/knative/eventing/pkg/client/clientset/versioned" + internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/knative/eventing/pkg/client/listers/sources/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CronJobSourceInformer provides access to a shared informer and lister for +// CronJobSources. +type CronJobSourceInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.CronJobSourceLister +} + +type cronJobSourceInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCronJobSourceInformer constructs a new informer for CronJobSource 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 NewCronJobSourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCronJobSourceInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCronJobSourceInformer constructs a new informer for CronJobSource 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 NewFilteredCronJobSourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SourcesV1alpha1().CronJobSources(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SourcesV1alpha1().CronJobSources(namespace).Watch(options) + }, + }, + &sourcesv1alpha1.CronJobSource{}, + resyncPeriod, + indexers, + ) +} + +func (f *cronJobSourceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCronJobSourceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *cronJobSourceInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&sourcesv1alpha1.CronJobSource{}, f.defaultInformer) +} + +func (f *cronJobSourceInformer) Lister() v1alpha1.CronJobSourceLister { + return v1alpha1.NewCronJobSourceLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/sources/v1alpha1/interface.go b/pkg/client/informers/externalversions/sources/v1alpha1/interface.go new file mode 100644 index 00000000000..244340025a8 --- /dev/null +++ b/pkg/client/informers/externalversions/sources/v1alpha1/interface.go @@ -0,0 +1,45 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // CronJobSources returns a CronJobSourceInformer. + CronJobSources() CronJobSourceInformer +} + +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} +} + +// CronJobSources returns a CronJobSourceInformer. +func (v *version) CronJobSources() CronJobSourceInformer { + return &cronJobSourceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/listers/sources/v1alpha1/cronjobsource.go b/pkg/client/listers/sources/v1alpha1/cronjobsource.go new file mode 100644 index 00000000000..a720fb0e9df --- /dev/null +++ b/pkg/client/listers/sources/v1alpha1/cronjobsource.go @@ -0,0 +1,94 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CronJobSourceLister helps list CronJobSources. +type CronJobSourceLister interface { + // List lists all CronJobSources in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.CronJobSource, err error) + // CronJobSources returns an object that can list and get CronJobSources. + CronJobSources(namespace string) CronJobSourceNamespaceLister + CronJobSourceListerExpansion +} + +// cronJobSourceLister implements the CronJobSourceLister interface. +type cronJobSourceLister struct { + indexer cache.Indexer +} + +// NewCronJobSourceLister returns a new CronJobSourceLister. +func NewCronJobSourceLister(indexer cache.Indexer) CronJobSourceLister { + return &cronJobSourceLister{indexer: indexer} +} + +// List lists all CronJobSources in the indexer. +func (s *cronJobSourceLister) List(selector labels.Selector) (ret []*v1alpha1.CronJobSource, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CronJobSource)) + }) + return ret, err +} + +// CronJobSources returns an object that can list and get CronJobSources. +func (s *cronJobSourceLister) CronJobSources(namespace string) CronJobSourceNamespaceLister { + return cronJobSourceNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// CronJobSourceNamespaceLister helps list and get CronJobSources. +type CronJobSourceNamespaceLister interface { + // List lists all CronJobSources in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.CronJobSource, err error) + // Get retrieves the CronJobSource from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.CronJobSource, error) + CronJobSourceNamespaceListerExpansion +} + +// cronJobSourceNamespaceLister implements the CronJobSourceNamespaceLister +// interface. +type cronJobSourceNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all CronJobSources in the indexer for a given namespace. +func (s cronJobSourceNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.CronJobSource, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CronJobSource)) + }) + return ret, err +} + +// Get retrieves the CronJobSource from the indexer for a given namespace and name. +func (s cronJobSourceNamespaceLister) Get(name string) (*v1alpha1.CronJobSource, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("cronjobsource"), name) + } + return obj.(*v1alpha1.CronJobSource), nil +} diff --git a/pkg/client/listers/sources/v1alpha1/expansion_generated.go b/pkg/client/listers/sources/v1alpha1/expansion_generated.go new file mode 100644 index 00000000000..444c53f7032 --- /dev/null +++ b/pkg/client/listers/sources/v1alpha1/expansion_generated.go @@ -0,0 +1,27 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// CronJobSourceListerExpansion allows custom methods to be added to +// CronJobSourceLister. +type CronJobSourceListerExpansion interface{} + +// CronJobSourceNamespaceListerExpansion allows custom methods to be added to +// CronJobSourceNamespaceLister. +type CronJobSourceNamespaceListerExpansion interface{} diff --git a/pkg/duck/sinks.go b/pkg/duck/sinks.go new file mode 100644 index 00000000000..e5a83686d94 --- /dev/null +++ b/pkg/duck/sinks.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package duck + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + + duckapis "github.com/knative/pkg/apis" + "github.com/knative/pkg/apis/duck" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" +) + +// GetSinkURI retrieves the sink URI from the object referenced by the given +// ObjectReference. +func GetSinkURI(ctx context.Context, dynamicClient dynamic.Interface, sink *corev1.ObjectReference, namespace string) (string, error) { + if sink == nil { + return "", fmt.Errorf("sink ref is nil") + } + + rc := dynamicClient.Resource(duckapis.KindToResource(sink.GroupVersionKind())) + if rc == nil { + return "", fmt.Errorf("failed to create dynamic client resource") + } + + u, err := rc.Namespace(namespace).Get(sink.Name, metav1.GetOptions{}) + if err != nil { + return "", err + } + + objIdentifier := fmt.Sprintf("\"%s/%s\" (%s)", u.GetNamespace(), u.GetName(), u.GroupVersionKind()) + + t := duckv1alpha1.AddressableType{} + err = duck.FromUnstructured(u, &t) + if err != nil { + return "", fmt.Errorf("failed to deserialize sink %s: %v", objIdentifier, err) + } + + if t.Status.Address == nil { + return "", fmt.Errorf("sink %s does not contain address", objIdentifier) + } + + if t.Status.Address.Hostname == "" { + return "", fmt.Errorf("sink %s contains an empty hostname", objIdentifier) + } + + return fmt.Sprintf("http://%s/", t.Status.Address.Hostname), nil +} diff --git a/pkg/duck/sinks_test.go b/pkg/duck/sinks_test.go new file mode 100644 index 00000000000..e52c94ce742 --- /dev/null +++ b/pkg/duck/sinks_test.go @@ -0,0 +1,210 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package duck + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +var ( + addressableDNS = "addressable.sink.svc.cluster.local" + + addressableName = "testsink" + addressableKind = "Sink" + addressableAPIVersion = "duck.knative.dev/v1alpha1" + + unaddressableName = "testunaddressable" + unaddressableKind = "KResource" + unaddressableAPIVersion = "duck.knative.dev/v1alpha1" + unaddressableResource = "kresources.duck.knative.dev" + + testNS = "testnamespace" +) + +func init() { + // Add types to scheme + duckv1alpha1.AddToScheme(scheme.Scheme) +} + +func TestGetSinkURI(t *testing.T) { + testCases := map[string]struct { + objects []runtime.Object + namespace string + want string + wantErr error + ref *corev1.ObjectReference + }{ + "happy": { + objects: []runtime.Object{ + getAddressable(), + }, + namespace: testNS, + ref: getAddressableRef(), + want: fmt.Sprintf("http://%s/", addressableDNS), + }, + "nil hostname": { + objects: []runtime.Object{ + getAddressableNilHostname(), + }, + namespace: testNS, + ref: getUnaddressableRef(), + wantErr: fmt.Errorf(`sink "testnamespace/testunaddressable" (duck.knative.dev/v1alpha1, Kind=KResource) contains an empty hostname`), + }, + "nil sink": { + objects: []runtime.Object{ + getAddressableNilHostname(), + }, + namespace: testNS, + ref: nil, + wantErr: fmt.Errorf(`sink ref is nil`), + }, + "nil address": { + objects: []runtime.Object{ + getAddressableNilAddress(), + }, + namespace: testNS, + ref: nil, + wantErr: fmt.Errorf(`sink ref is nil`), + }, + "notSink": { + objects: []runtime.Object{ + getAddressableNoStatus(), + }, + namespace: testNS, + ref: getUnaddressableRef(), + wantErr: fmt.Errorf(`sink "testnamespace/testunaddressable" (duck.knative.dev/v1alpha1, Kind=KResource) does not contain address`), + }, + "notFound": { + namespace: testNS, + ref: getUnaddressableRef(), + wantErr: fmt.Errorf(`%s "%s" not found`, unaddressableResource, unaddressableName), + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + ctx := context.Background() + client := fake.NewSimpleDynamicClient(scheme.Scheme, tc.objects...) + uri, gotErr := GetSinkURI(ctx, client, tc.ref, tc.namespace) + if gotErr != nil { + if tc.wantErr != nil { + if diff := cmp.Diff(tc.wantErr.Error(), gotErr.Error()); diff != "" { + t.Errorf("%s: unexpected error (-want, +got) = %v", n, diff) + } + } else { + t.Errorf("%s: unexpected error %v", n, gotErr.Error()) + } + } + if gotErr == nil { + got := uri + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("%s: unexpected object (-want, +got) = %v", n, diff) + } + } + }) + } +} + +func getAddressable() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": addressableAPIVersion, + "kind": addressableKind, + "metadata": map[string]interface{}{ + "namespace": testNS, + "name": addressableName, + }, + "status": map[string]interface{}{ + "address": map[string]interface{}{ + "hostname": addressableDNS, + }, + }, + }, + } +} + +func getAddressableNoStatus() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": unaddressableAPIVersion, + "kind": unaddressableKind, + "metadata": map[string]interface{}{ + "namespace": testNS, + "name": unaddressableName, + }, + }, + } +} + +func getAddressableNilAddress() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": unaddressableAPIVersion, + "kind": unaddressableKind, + "metadata": map[string]interface{}{ + "namespace": testNS, + "name": unaddressableName, + }, + "status": map[string]interface{}{ + "address": map[string]interface{}(nil), + }, + }, + } +} + +func getAddressableNilHostname() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": unaddressableAPIVersion, + "kind": unaddressableKind, + "metadata": map[string]interface{}{ + "namespace": testNS, + "name": unaddressableName, + }, + "status": map[string]interface{}{ + "address": map[string]interface{}{ + "hostname": nil, + }, + }, + }, + } +} + +func getAddressableRef() *corev1.ObjectReference { + return &corev1.ObjectReference{ + Kind: addressableKind, + Name: addressableName, + APIVersion: addressableAPIVersion, + } +} + +func getUnaddressableRef() *corev1.ObjectReference { + return &corev1.ObjectReference{ + Kind: unaddressableKind, + Name: unaddressableName, + APIVersion: unaddressableAPIVersion, + } +} diff --git a/pkg/utils/resolve/subscriber.go b/pkg/duck/subscriber.go similarity index 99% rename from pkg/utils/resolve/subscriber.go rename to pkg/duck/subscriber.go index 8fa5a3c47a0..9458868234b 100644 --- a/pkg/utils/resolve/subscriber.go +++ b/pkg/duck/subscriber.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resolve +package duck import ( "context" diff --git a/pkg/utils/resolve/subscriber_test.go b/pkg/duck/subscriber_test.go similarity index 99% rename from pkg/utils/resolve/subscriber_test.go rename to pkg/duck/subscriber_test.go index ec13a4f5fab..2ede971e1f1 100644 --- a/pkg/utils/resolve/subscriber_test.go +++ b/pkg/duck/subscriber_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resolve +package duck import ( "context" @@ -35,10 +35,6 @@ import ( "k8s.io/client-go/kubernetes/scheme" ) -const ( - testNS = "test-namespace" -) - var ( uri = "http://example.com" diff --git a/pkg/kncloudevents/good_client.go b/pkg/kncloudevents/good_client.go new file mode 100644 index 00000000000..9a000dba51a --- /dev/null +++ b/pkg/kncloudevents/good_client.go @@ -0,0 +1,30 @@ +package kncloudevents + +import ( + "github.com/cloudevents/sdk-go" + "github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http" +) + +func NewDefaultClient(target ...string) (cloudevents.Client, error) { + tOpts := []http.Option{cloudevents.WithBinaryEncoding()} + if len(target) > 0 && target[0] != "" { + tOpts = append(tOpts, cloudevents.WithTarget(target[0])) + } + + // Make an http transport for the CloudEvents client. + t, err := cloudevents.NewHTTPTransport(tOpts...) + if err != nil { + return nil, err + } + + // Use the transport to make a new CloudEvents client. + c, err := cloudevents.NewClient(t, + cloudevents.WithUUIDs(), + cloudevents.WithTimeNow(), + ) + + if err != nil { + return nil, err + } + return c, nil +} diff --git a/pkg/logconfig/config.go b/pkg/logconfig/config.go index 9ef2e550781..48b3d842cab 100644 --- a/pkg/logconfig/config.go +++ b/pkg/logconfig/config.go @@ -33,6 +33,9 @@ const ( // Controller is the name of the override key used inside of the logging config for Controller. Controller = "controller" + // SourcesController is the name of the override key used inside of the logging config for Sources Controller. + SourcesController = "sources-controller" + // Webhook is the name of the override key used inside of the logging config for Webhook Controller. WebhookNameEnv = "WEBHOOK_NAME" ) diff --git a/pkg/reconciler/cronjobsource/cronjobsource.go b/pkg/reconciler/cronjobsource/cronjobsource.go new file mode 100644 index 00000000000..488f144ca5b --- /dev/null +++ b/pkg/reconciler/cronjobsource/cronjobsource.go @@ -0,0 +1,287 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cronjobsource + +import ( + "context" + "fmt" + "os" + "reflect" + "sync" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + appsv1informers "k8s.io/client-go/informers/apps/v1" + appsv1listers "k8s.io/client-go/listers/apps/v1" + "k8s.io/client-go/tools/cache" + + "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + sourceinformers "github.com/knative/eventing/pkg/client/informers/externalversions/sources/v1alpha1" + listers "github.com/knative/eventing/pkg/client/listers/sources/v1alpha1" + "github.com/knative/eventing/pkg/duck" + "github.com/knative/eventing/pkg/reconciler" + "github.com/knative/eventing/pkg/reconciler/cronjobsource/resources" + "github.com/knative/pkg/controller" + "github.com/knative/pkg/logging" + "github.com/robfig/cron" + "go.uber.org/zap" +) + +const ( + // ReconcilerName is the name of the reconciler + ReconcilerName = "CronJobSources" + // controllerAgentName is the string used by this controller to identify + // itself when creating events. + controllerAgentName = "cronjob-source-controller" + + // Name of the corev1.Events emitted from the reconciliation process + cronjobReconciled = "CronJobSourceReconciled" + cronjobUpdateStatusFailed = "CronJobSourceUpdateStatusFailed" + + // raImageEnvVar is the name of the environment variable that contains the receive adapter's + // image. It must be defined. + raImageEnvVar = "CRONJOB_RA_IMAGE" +) + +type Reconciler struct { + *reconciler.Base + + receiveAdapterImage string + once sync.Once + + // listers index properties about resources + cronjobLister listers.CronJobSourceLister + deploymentLister appsv1listers.DeploymentLister +} + +// Check that our Reconciler implements controller.Reconciler +var _ controller.Reconciler = (*Reconciler)(nil) + +// NewController initializes the controller and is called by the generated code +// Registers event handlers to enqueue events +func NewController( + opt reconciler.Options, + cronjobsourceInformer sourceinformers.CronJobSourceInformer, + deploymentInformer appsv1informers.DeploymentInformer, +) *controller.Impl { + r := &Reconciler{ + Base: reconciler.NewBase(opt, controllerAgentName), + cronjobLister: cronjobsourceInformer.Lister(), + deploymentLister: deploymentInformer.Lister(), + } + impl := controller.NewImpl(r, r.Logger, ReconcilerName, reconciler.MustNewStatsReporter(ReconcilerName, r.Logger)) + + r.Logger.Info("Setting up event handlers") + cronjobsourceInformer.Informer().AddEventHandler(reconciler.Handler(impl.Enqueue)) + + deploymentInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.Filter(v1alpha1.SchemeGroupVersion.WithKind("CronJobSource")), + Handler: reconciler.Handler(impl.EnqueueControllerOf), + }) + + return impl +} + +// Reconcile compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the CronJobSource +// resource with the current status of the resource. +func (r *Reconciler) Reconcile(ctx context.Context, key string) error { + // Convert the namespace/name string into a distinct namespace and name + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + r.Logger.Errorf("invalid resource key: %s", key) + return nil + } + + // Get the CronJobSource resource with this namespace/name + original, err := r.cronjobLister.CronJobSources(namespace).Get(name) + if apierrs.IsNotFound(err) { + // The resource may no longer exist, in which case we stop processing. + logging.FromContext(ctx).Error("CronJobSource key in work queue no longer exists", zap.Any("key", key)) + return nil + } else if err != nil { + return err + } + + // Don't modify the informers copy + cronjob := original.DeepCopy() + + // Reconcile this copy of the CronJobSource and then write back any status + // updates regardless of whether the reconcile error out. + err = r.reconcile(ctx, cronjob) + if err != nil { + logging.FromContext(ctx).Warn("Error reconciling CronJobSource", zap.Error(err)) + } else { + logging.FromContext(ctx).Debug("CronJobSource reconciled") + r.Recorder.Eventf(cronjob, corev1.EventTypeNormal, cronjobReconciled, `CronJobSource reconciled: "%s/%s"`, cronjob.Namespace, cronjob.Name) + } + + if _, updateStatusErr := r.updateStatus(ctx, cronjob.DeepCopy()); updateStatusErr != nil { + logging.FromContext(ctx).Warn("Failed to update the CronJobSource", zap.Error(err)) + r.Recorder.Eventf(cronjob, corev1.EventTypeWarning, cronjobUpdateStatusFailed, "Failed to update CronJobSource's status: %v", err) + return updateStatusErr + } + + // Requeue if the resource is not ready: + return err +} + +func (r *Reconciler) reconcile(ctx context.Context, cronjob *v1alpha1.CronJobSource) error { + // This Source attempts to reconcile three things. + // 1. Determine the sink's URI. + // - Nothing to delete. + // 2. Create a receive adapter in the form of a Deployment. + // - Will be garbage collected by K8s when this CronJobSource is deleted. + + cronjob.Status.InitializeConditions() + + _, err := cron.ParseStandard(cronjob.Spec.Schedule) + if err != nil { + cronjob.Status.MarkInvalidSchedule("Invalid", "") + return err + } + cronjob.Status.MarkSchedule() + sinkURI, err := duck.GetSinkURI(ctx, r.DynamicClientSet, cronjob.Spec.Sink, cronjob.Namespace) + if err != nil { + cronjob.Status.MarkNoSink("NotFound", "") + return err + } + cronjob.Status.MarkSink(sinkURI) + + _, err = r.createReceiveAdapter(ctx, cronjob, sinkURI) + if err != nil { + r.Logger.Error("Unable to create the receive adapter", zap.Error(err)) + return err + } + cronjob.Status.MarkDeployed() + return nil +} + +func (r *Reconciler) getReceiveAdapterImage() string { + if r.receiveAdapterImage == "" { + r.once.Do(func() { + raImage, defined := os.LookupEnv(raImageEnvVar) + if !defined { + panic(fmt.Errorf("required environment variable %q not defined", raImageEnvVar)) + } + r.receiveAdapterImage = raImage + }) + } + return r.receiveAdapterImage +} + +func (r *Reconciler) createReceiveAdapter(ctx context.Context, src *v1alpha1.CronJobSource, sinkURI string) (*appsv1.Deployment, error) { + ra, err := r.getReceiveAdapter(ctx, src) + if err != nil && !apierrors.IsNotFound(err) { + logging.FromContext(ctx).Error("Unable to get an existing receive adapter", zap.Error(err)) + return nil, err + } + adapterArgs := resources.ReceiveAdapterArgs{ + Image: r.getReceiveAdapterImage(), + Source: src, + Labels: resources.Labels(src.Name), + SinkURI: sinkURI, + } + expected := resources.MakeReceiveAdapter(&adapterArgs) + if ra != nil { + if r.podSpecChanged(ra.Spec.Template.Spec, expected.Spec.Template.Spec) { + ra.Spec.Template.Spec = expected.Spec.Template.Spec + if ra, err = r.KubeClientSet.AppsV1().Deployments(src.Namespace).Update(ra); err != nil { + return ra, err + } + logging.FromContext(ctx).Desugar().Info("Receive Adapter updated.", zap.Any("receiveAdapter", ra)) + } else { + logging.FromContext(ctx).Desugar().Info("Reusing existing receive adapter", zap.Any("receiveAdapter", ra)) + } + return ra, nil + } + + if ra, err = r.KubeClientSet.AppsV1().Deployments(src.Namespace).Create(expected); err != nil { + return nil, err + } + logging.FromContext(ctx).Desugar().Info("Receive Adapter created.", zap.Any("receiveAdapter", expected)) + return ra, err +} + +func (r *Reconciler) podSpecChanged(oldPodSpec corev1.PodSpec, newPodSpec corev1.PodSpec) bool { + if !equality.Semantic.DeepDerivative(newPodSpec, oldPodSpec) { + return true + } + if len(oldPodSpec.Containers) != len(newPodSpec.Containers) { + return true + } + for i := range newPodSpec.Containers { + if !equality.Semantic.DeepEqual(newPodSpec.Containers[i].Env, oldPodSpec.Containers[i].Env) { + return true + } + } + return false +} + +func (r *Reconciler) getReceiveAdapter(ctx context.Context, src *v1alpha1.CronJobSource) (*appsv1.Deployment, error) { + dl, err := r.KubeClientSet.AppsV1().Deployments(src.Namespace).List(metav1.ListOptions{ + LabelSelector: r.getLabelSelector(src).String(), + }) + if err != nil { + logging.FromContext(ctx).Desugar().Error("Unable to list cronjobs: %v", zap.Error(err)) + return nil, err + } + for _, dep := range dl.Items { + if metav1.IsControlledBy(&dep, src) { + return &dep, nil + } + } + return nil, apierrors.NewNotFound(schema.GroupResource{}, "") +} + +func (r *Reconciler) getLabelSelector(src *v1alpha1.CronJobSource) labels.Selector { + return labels.SelectorFromSet(resources.Labels(src.Name)) +} + +func (r *Reconciler) updateStatus(ctx context.Context, desired *v1alpha1.CronJobSource) (*v1alpha1.CronJobSource, error) { + cronjob, err := r.cronjobLister.CronJobSources(desired.Namespace).Get(desired.Name) + if err != nil { + return nil, err + } + + // If there's nothing to update, just return. + if reflect.DeepEqual(cronjob.Status, desired.Status) { + return cronjob, nil + } + + becomesReady := desired.Status.IsReady() && !cronjob.Status.IsReady() + + // Don't modify the informers copy. + existing := cronjob.DeepCopy() + existing.Status = desired.Status + + cj, err := r.EventingClientSet.SourcesV1alpha1().CronJobSources(desired.Namespace).UpdateStatus(existing) + if err == nil && becomesReady { + duration := time.Since(cj.ObjectMeta.CreationTimestamp.Time) + r.Logger.Infof("CronJobSource %q became ready after %v", cronjob.Name, duration) + //r.StatsReporter.ReportServiceReady(subscription.Namespace, subscription.Name, duration) // TODO: stats + } + + return cj, err +} diff --git a/pkg/reconciler/cronjobsource/cronjobsource_test.go b/pkg/reconciler/cronjobsource/cronjobsource_test.go new file mode 100644 index 00000000000..a971cb1e4e9 --- /dev/null +++ b/pkg/reconciler/cronjobsource/cronjobsource_test.go @@ -0,0 +1,307 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Veroute.on 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 cronjobsource + +import ( + "github.com/knative/eventing/pkg/reconciler/cronjobsource/resources" + "github.com/knative/eventing/pkg/utils" + "k8s.io/apimachinery/pkg/runtime" + "os" + "testing" + + clientgotesting "k8s.io/client-go/testing" + + fakeclientset "github.com/knative/eventing/pkg/client/clientset/versioned/fake" + informers "github.com/knative/eventing/pkg/client/informers/externalversions" + "github.com/knative/eventing/pkg/reconciler" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + "github.com/knative/pkg/controller" + logtesting "github.com/knative/pkg/logging/testing" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + //"k8s.io/apimachinery/pkg/runtime" + kubeinformers "k8s.io/client-go/informers" + fakekubeclientset "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" + + . "github.com/knative/eventing/pkg/reconciler/testing" + . "github.com/knative/pkg/reconciler/testing" + + sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" + v1 "k8s.io/api/apps/v1" +) + +var ( + // deletionTime is used when objects are marked as deleted. Rfc3339Copy() + // truncates to seconds to match the loss of precision during serialization. + deletionTime = metav1.Now().Rfc3339Copy() + + trueVal = true + + sinkGVK = metav1.GroupVersionKind{ + Group: "eventing.knative.dev", + Version: "v1alpha1", + Kind: "Channel", + } + sinkRef = corev1.ObjectReference{ + Name: sinkName, + Kind: "Channel", + APIVersion: "eventing.knative.dev/v1alpha1", + } + sinkDNS = "sink.mynamespace.svc." + utils.GetClusterDomainName() + sinkURI = "http://" + sinkDNS + "/" +) + +const ( + image = "github.com/knative/test/image" + sourceName = "test-cronjob-source" + sourceUID = "1234-5678-90" + testNS = "testnamespace" + testSchedule = "*/2 * * * *" + testData = "data" + + sinkName = "testsink" +) + +func init() { + // Add types to scheme + _ = v1.AddToScheme(scheme.Scheme) + _ = corev1.AddToScheme(scheme.Scheme) + _ = duckv1alpha1.AddToScheme(scheme.Scheme) + + _ = os.Setenv("CRONJOB_RA_IMAGE", image) +} + +func TestAllCases(t *testing.T) { + table := TableTest{ + { + Name: "bad workqueue key", + // Make sure Reconcile handles bad keys. + Key: "too/many/parts", + }, { + Name: "key not found", + // Make sure Reconcile handles good keys that don't exist. + Key: "foo/not-found", + }, { + Name: "invalid schedule", + Objects: []runtime.Object{ + NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: "invalid schedule", + Data: testData, + Sink: &sinkRef, + }), + ), + }, + Key: testNS + "/" + sourceName, + WantErr: true, + //WantEvents: []string{ + // Eventf(corev1.EventTypeWarning, "Fail", ""), // TODO: BUGBUGBUG This should make an event. + //}, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: "invalid schedule", + Data: testData, + Sink: &sinkRef, + }), + // Status Update: + WithInitCronJobSourceConditions, + WithInvalidCronJobSourceSchedule, + ), + }}, + }, { + Name: "missing sink", + Objects: []runtime.Object{ + NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + ), + }, + Key: testNS + "/" + sourceName, + WantErr: true, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + // Status Update: + WithInitCronJobSourceConditions, + WithValidCronJobSourceSchedule, + WithCronJobSourceSinkNotFound, + ), + }}, + }, { + Name: "valid", + Objects: []runtime.Object{ + NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + ), + NewChannel(sinkName, testNS, + WithInitChannelConditions, + WithChannelAddress(sinkDNS), + ), + }, + Key: testNS + "/" + sourceName, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "CronJobSourceReconciled", `CronJobSource reconciled: "%s/%s"`, testNS, sourceName), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + // Status Update: + WithInitCronJobSourceConditions, + WithValidCronJobSourceSchedule, + WithCronJobSourceDeployed, + WithCronJobSourceSink(sinkURI), + ), + }}, + WantCreates: []metav1.Object{ + makeReceiveAdapter(), + }, + }, { + Name: "valid, existing ra", + Objects: []runtime.Object{ + NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + ), + NewChannel(sinkName, testNS, + WithInitChannelConditions, + WithChannelAddress(sinkDNS), + ), + makeReceiveAdapter(), + }, + Key: testNS + "/" + sourceName, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "CronJobSourceReconciled", `CronJobSource reconciled: "%s/%s"`, testNS, sourceName), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + // Status Update: + WithInitCronJobSourceConditions, + WithValidCronJobSourceSchedule, + WithCronJobSourceDeployed, + WithCronJobSourceSink(sinkURI), + ), + }}, + }, { + Name: "valid, no change", + Objects: []runtime.Object{ + NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }), + WithInitCronJobSourceConditions, + WithValidCronJobSourceSchedule, + WithCronJobSourceDeployed, + WithCronJobSourceSink(sinkURI), + ), + NewChannel(sinkName, testNS, + WithInitChannelConditions, + WithChannelAddress(sinkDNS), + ), + makeReceiveAdapter(), + }, + Key: testNS + "/" + sourceName, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "CronJobSourceReconciled", `CronJobSource reconciled: "%s/%s"`, testNS, sourceName), + }, + }, + } + + defer logtesting.ClearAll() + table.Test(t, MakeFactory(func(listers *Listers, opt reconciler.Options) controller.Reconciler { + return &Reconciler{ + Base: reconciler.NewBase(opt, controllerAgentName), + cronjobLister: listers.GetCronJobSourceLister(), + deploymentLister: listers.GetDeploymentLister(), + } + })) + +} + +func TestNew(t *testing.T) { + defer logtesting.ClearAll() + kubeClient := fakekubeclientset.NewSimpleClientset() + eventingClient := fakeclientset.NewSimpleClientset() + eventingInformer := informers.NewSharedInformerFactory(eventingClient, 0) + kubeInformer := kubeinformers.NewSharedInformerFactory(kubeClient, 0) + + cronjobInformer := eventingInformer.Sources().V1alpha1().CronJobSources() + deploymentInformer := kubeInformer.Apps().V1().Deployments() + + c := NewController(reconciler.Options{ + KubeClientSet: kubeClient, + EventingClientSet: eventingClient, + Logger: logtesting.TestLogger(t), + }, + cronjobInformer, + deploymentInformer, + ) + + if c == nil { + t.Fatal("Expected NewController to return a non-nil value") + } +} + +func makeReceiveAdapter() *v1.Deployment { + source := NewCronSourceJob(sourceName, testNS, + WithCronJobSourceSpec(sourcesv1alpha1.CronJobSourceSpec{ + Schedule: testSchedule, + Data: testData, + Sink: &sinkRef, + }, + ), + // Status Update: + WithInitCronJobSourceConditions, + WithValidCronJobSourceSchedule, + WithCronJobSourceDeployed, + WithCronJobSourceSink(sinkURI), + ) + + args := resources.ReceiveAdapterArgs{ + Image: image, + Source: source, + Labels: resources.Labels(sourceName), + SinkURI: sinkURI, + } + return resources.MakeReceiveAdapter(&args) +} diff --git a/pkg/reconciler/cronjobsource/doc.go b/pkg/reconciler/cronjobsource/doc.go new file mode 100644 index 00000000000..aa960fc7bad --- /dev/null +++ b/pkg/reconciler/cronjobsource/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package cronjobsource implements the CronJobSource controller. +package cronjobsource diff --git a/pkg/reconciler/cronjobsource/resources/labels.go b/pkg/reconciler/cronjobsource/resources/labels.go new file mode 100644 index 00000000000..04dc6098fda --- /dev/null +++ b/pkg/reconciler/cronjobsource/resources/labels.go @@ -0,0 +1,30 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +const ( + // controllerAgentName is the string used by this controller to identify + // itself when creating events. + controllerAgentName = "cronjob-source-controller" +) + +func Labels(name string) map[string]string { + return map[string]string{ + "knative-eventing-source": controllerAgentName, + "knative-eventing-source-name": name, + } +} diff --git a/pkg/reconciler/cronjobsource/resources/receive_adapter.go b/pkg/reconciler/cronjobsource/resources/receive_adapter.go new file mode 100644 index 00000000000..9f516db0cab --- /dev/null +++ b/pkg/reconciler/cronjobsource/resources/receive_adapter.go @@ -0,0 +1,90 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "fmt" + "github.com/knative/pkg/kmeta" + + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/knative/eventing/pkg/apis/sources/v1alpha1" +) + +// ReceiveAdapterArgs are the arguments needed to create a Cron Job Source Receive Adapter. Every +// field is required. +type ReceiveAdapterArgs struct { + Image string + Source *v1alpha1.CronJobSource + Labels map[string]string + SinkURI string +} + +// MakeReceiveAdapter generates (but does not insert into K8s) the Receive Adapter Deployment for +// Cron Job Sources. +func MakeReceiveAdapter(args *ReceiveAdapterArgs) *v1.Deployment { + replicas := int32(1) + return &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: args.Source.Namespace, + GenerateName: fmt.Sprintf("cronjob-%s-", args.Source.Name), + Labels: args.Labels, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(args.Source), + }, + }, + Spec: v1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: args.Labels, + }, + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "sidecar.istio.io/inject": "true", // TODO this might be removed. + }, + Labels: args.Labels, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: args.Source.Spec.ServiceAccountName, + Containers: []corev1.Container{ + { + Name: "receive-adapter", + Image: args.Image, + Env: []corev1.EnvVar{ + { + Name: "SCHEDULE", + Value: args.Source.Spec.Schedule, + }, + { + Name: "DATA", + Value: args.Source.Spec.Data, + }, + { + Name: "SINK_URI", + Value: args.SinkURI, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/pkg/reconciler/cronjobsource/resources/receive_adapter_test.go b/pkg/reconciler/cronjobsource/resources/receive_adapter_test.go new file mode 100644 index 00000000000..e8b2c5fd1df --- /dev/null +++ b/pkg/reconciler/cronjobsource/resources/receive_adapter_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "testing" + + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/google/go-cmp/cmp" + "github.com/knative/eventing/pkg/apis/sources/v1alpha1" +) + +func TestMakeReceiveAdapter(t *testing.T) { + src := &v1alpha1.CronJobSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "source-name", + Namespace: "source-namespace", + }, + Spec: v1alpha1.CronJobSourceSpec{ + ServiceAccountName: "source-svc-acct", + Schedule: "*/2 * * * *", + Data: "data", + }, + } + + got := MakeReceiveAdapter(&ReceiveAdapterArgs{ + Image: "test-image", + Source: src, + Labels: map[string]string{ + "test-key1": "test-value1", + "test-key2": "test-value2", + }, + SinkURI: "sink-uri", + }) + + one := int32(1) + yes := true + want := &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "source-namespace", + GenerateName: "cronjob-source-name-", + Labels: map[string]string{ + "test-key1": "test-value1", + "test-key2": "test-value2", + }, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "sources.eventing.knative.dev/v1alpha1", + Kind: "CronJobSource", + Name: "source-name", + Controller: &yes, + BlockOwnerDeletion: &yes, + }}, + }, + Spec: v1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "test-key1": "test-value1", + "test-key2": "test-value2", + }, + }, + Replicas: &one, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "sidecar.istio.io/inject": "true", + }, + Labels: map[string]string{ + "test-key1": "test-value1", + "test-key2": "test-value2", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "source-svc-acct", + Containers: []corev1.Container{ + { + Name: "receive-adapter", + Image: "test-image", + Env: []corev1.EnvVar{ + { + Name: "SCHEDULE", + Value: "*/2 * * * *", + }, + { + Name: "DATA", + Value: "data", + }, + { + Name: "SINK_URI", + Value: "sink-uri", + }, + }, + }, + }, + }, + }, + }, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected cron job (-want, +got) = %v", diff) + } +} diff --git a/pkg/reconciler/subscription/subscription.go b/pkg/reconciler/subscription/subscription.go index e241a1b0a70..9c0e9058469 100644 --- a/pkg/reconciler/subscription/subscription.go +++ b/pkg/reconciler/subscription/subscription.go @@ -20,17 +20,16 @@ import ( "context" "encoding/json" "fmt" - "k8s.io/apimachinery/pkg/labels" "reflect" "time" - eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" eventinginformers "github.com/knative/eventing/pkg/client/informers/externalversions/eventing/v1alpha1" listers "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + eventingduck "github.com/knative/eventing/pkg/duck" "github.com/knative/eventing/pkg/logging" "github.com/knative/eventing/pkg/reconciler" - "github.com/knative/eventing/pkg/utils/resolve" "github.com/knative/pkg/apis/duck" duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" "github.com/knative/pkg/controller" @@ -40,6 +39,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" apierrs "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/cache" @@ -157,7 +157,7 @@ func (r *Reconciler) reconcile(ctx context.Context, subscription *v1alpha1.Subsc } // Verify that `channel` exists. - if _, err := resolve.ObjectReference(ctx, r.DynamicClientSet, subscription.Namespace, &subscription.Spec.Channel); err != nil { + if _, err := eventingduck.ObjectReference(ctx, r.DynamicClientSet, subscription.Namespace, &subscription.Spec.Channel); err != nil { logging.FromContext(ctx).Warn("Failed to validate Channel exists", zap.Error(err), zap.Any("channel", subscription.Spec.Channel)) @@ -165,7 +165,7 @@ func (r *Reconciler) reconcile(ctx context.Context, subscription *v1alpha1.Subsc return err } - subscriberURI, err := resolve.SubscriberSpec(ctx, r.DynamicClientSet, subscription.Namespace, subscription.Spec.Subscriber) + subscriberURI, err := eventingduck.SubscriberSpec(ctx, r.DynamicClientSet, subscription.Namespace, subscription.Spec.Subscriber) if err != nil { logging.FromContext(ctx).Warn("Failed to resolve Subscriber", zap.Error(err), @@ -266,7 +266,7 @@ func (r *Reconciler) resolveResult(ctx context.Context, namespace string, replyS if isNilOrEmptyReply(replyStrategy) { return "", nil } - obj, err := resolve.ObjectReference(ctx, r.DynamicClientSet, namespace, replyStrategy.Channel) + obj, err := eventingduck.ObjectReference(ctx, r.DynamicClientSet, namespace, replyStrategy.Channel) if err != nil { logging.FromContext(ctx).Warn("Failed to fetch ReplyStrategy Channel", zap.Error(err), @@ -280,7 +280,7 @@ func (r *Reconciler) resolveResult(ctx context.Context, namespace string, replyS return "", err } if s.Status.Address != nil { - return resolve.DomainToURL(s.Status.Address.Hostname), nil + return eventingduck.DomainToURL(s.Status.Address.Hostname), nil } return "", fmt.Errorf("status does not contain address") } @@ -331,11 +331,11 @@ func (r *Reconciler) listAllSubscriptionsWithPhysicalChannel(ctx context.Context return subs, nil } -func (r *Reconciler) createSubscribable(subs []v1alpha1.Subscription) *eventingduck.Subscribable { - rv := &eventingduck.Subscribable{} +func (r *Reconciler) createSubscribable(subs []v1alpha1.Subscription) *eventingduckv1alpha1.Subscribable { + rv := &eventingduckv1alpha1.Subscribable{} for _, sub := range subs { if sub.Status.PhysicalSubscription.SubscriberURI != "" || sub.Status.PhysicalSubscription.ReplyURI != "" { - rv.Subscribers = append(rv.Subscribers, eventingduck.ChannelSubscriberSpec{ + rv.Subscribers = append(rv.Subscribers, eventingduckv1alpha1.ChannelSubscriberSpec{ DeprecatedRef: &corev1.ObjectReference{ APIVersion: sub.APIVersion, Kind: sub.Kind, @@ -352,13 +352,13 @@ func (r *Reconciler) createSubscribable(subs []v1alpha1.Subscription) *eventingd return rv } -func (r *Reconciler) patchPhysicalFrom(ctx context.Context, namespace string, physicalFrom corev1.ObjectReference, subs *eventingduck.Subscribable) error { +func (r *Reconciler) patchPhysicalFrom(ctx context.Context, namespace string, physicalFrom corev1.ObjectReference, subs *eventingduckv1alpha1.Subscribable) error { // First get the original object and convert it to only the bits we care about - s, err := resolve.ObjectReference(ctx, r.DynamicClientSet, namespace, &physicalFrom) + s, err := eventingduck.ObjectReference(ctx, r.DynamicClientSet, namespace, &physicalFrom) if err != nil { return err } - original := eventingduck.Channel{} + original := eventingduckv1alpha1.Channel{} err = duck.FromUnstructured(s, &original) if err != nil { return err @@ -372,7 +372,7 @@ func (r *Reconciler) patchPhysicalFrom(ctx context.Context, namespace string, ph return err } - resourceClient, err := resolve.ResourceInterface(r.DynamicClientSet, namespace, &physicalFrom) + resourceClient, err := eventingduck.ResourceInterface(r.DynamicClientSet, namespace, &physicalFrom) if err != nil { logging.FromContext(ctx).Warn("Failed to create dynamic resource client", zap.Error(err)) return err diff --git a/pkg/reconciler/testing/cronjobsource.go b/pkg/reconciler/testing/cronjobsource.go new file mode 100644 index 00000000000..97faf4dcf38 --- /dev/null +++ b/pkg/reconciler/testing/cronjobsource.go @@ -0,0 +1,81 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/knative/eventing/pkg/apis/sources/v1alpha1" +) + +// CronJobSourceOption enables further configuration of a CronJob. +type CronJobSourceOption func(*v1alpha1.CronJobSource) + +// NewCronJob creates a CronJob with CronJobOptions +func NewCronSourceJob(name, namespace string, o ...CronJobSourceOption) *v1alpha1.CronJobSource { + c := &v1alpha1.CronJobSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + for _, opt := range o { + opt(c) + } + //c.SetDefaults(context.Background()) // TODO: We should add defaults and validation. + return c +} + +// WithInitCronJobConditions initializes the CronJobSource's conditions. +func WithInitCronJobSourceConditions(s *v1alpha1.CronJobSource) { + s.Status.InitializeConditions() +} + +func WithValidCronJobSourceSchedule(s *v1alpha1.CronJobSource) { + s.Status.MarkSchedule() +} + +func WithInvalidCronJobSourceSchedule(s *v1alpha1.CronJobSource) { + s.Status.MarkInvalidSchedule("Invalid", "") +} + +func WithCronJobSourceSinkNotFound(s *v1alpha1.CronJobSource) { + s.Status.MarkNoSink("NotFound", "") +} + +func WithCronJobSourceSink(uri string) CronJobSourceOption { + return func(s *v1alpha1.CronJobSource) { + s.Status.MarkSink(uri) + } +} + +func WithCronJobSourceDeployed(s *v1alpha1.CronJobSource) { + s.Status.MarkDeployed() +} + +func WithCronJobSourceDeleted(c *v1alpha1.CronJobSource) { + t := metav1.NewTime(time.Unix(1e9, 0)) + c.ObjectMeta.SetDeletionTimestamp(&t) +} + +func WithCronJobSourceSpec(spec v1alpha1.CronJobSourceSpec) CronJobSourceOption { + return func(c *v1alpha1.CronJobSource) { + c.Spec = spec + } +} diff --git a/pkg/reconciler/testing/listers.go b/pkg/reconciler/testing/listers.go index 7ecff7d57e0..8d634d3a91c 100644 --- a/pkg/reconciler/testing/listers.go +++ b/pkg/reconciler/testing/listers.go @@ -18,8 +18,10 @@ package testing import ( eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" fakeeventingclientset "github.com/knative/eventing/pkg/client/clientset/versioned/fake" eventinglisters "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + sourcelisters "github.com/knative/eventing/pkg/client/listers/sources/v1alpha1" istiov1alpha3 "github.com/knative/pkg/apis/istio/v1alpha3" fakesharedclientset "github.com/knative/pkg/client/clientset/versioned/fake" istiolisters "github.com/knative/pkg/client/listers/istio/v1alpha3" @@ -114,6 +116,10 @@ func (l *Listers) GetVirtualServiceLister() istiolisters.VirtualServiceLister { return istiolisters.NewVirtualServiceLister(l.indexerFor(&istiov1alpha3.VirtualService{})) } +func (l *Listers) GetCronJobSourceLister() sourcelisters.CronJobSourceLister { + return sourcelisters.NewCronJobSourceLister(l.indexerFor(&sourcesv1alpha1.CronJobSource{})) +} + // GetGatewayLister gets lister for Istio Gateway resource. func (l *Listers) GetGatewayLister() istiolisters.GatewayLister { return istiolisters.NewGatewayLister(l.indexerFor(&istiov1alpha3.Gateway{})) diff --git a/pkg/reconciler/trigger/trigger.go b/pkg/reconciler/trigger/trigger.go index d681f47274f..7924cf35e0d 100644 --- a/pkg/reconciler/trigger/trigger.go +++ b/pkg/reconciler/trigger/trigger.go @@ -26,6 +26,7 @@ import ( "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" eventinginformers "github.com/knative/eventing/pkg/client/informers/externalversions/eventing/v1alpha1" listers "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + "github.com/knative/eventing/pkg/duck" "github.com/knative/eventing/pkg/logging" "github.com/knative/eventing/pkg/reconciler" "github.com/knative/eventing/pkg/reconciler/names" @@ -33,7 +34,6 @@ import ( "github.com/knative/eventing/pkg/reconciler/trigger/resources" "github.com/knative/eventing/pkg/reconciler/v1alpha1/broker" brokerresources "github.com/knative/eventing/pkg/reconciler/v1alpha1/broker/resources" - "github.com/knative/eventing/pkg/utils/resolve" "github.com/knative/pkg/controller" "github.com/knative/pkg/tracker" "go.uber.org/zap" @@ -250,7 +250,7 @@ func (r *Reconciler) reconcile(ctx context.Context, t *v1alpha1.Trigger) error { } } - subscriberURI, err := resolve.SubscriberSpec(ctx, r.DynamicClientSet, t.Namespace, t.Spec.Subscriber) + subscriberURI, err := duck.SubscriberSpec(ctx, r.DynamicClientSet, t.Namespace, t.Spec.Subscriber) if err != nil { logging.FromContext(ctx).Error("Unable to get the Subscriber's URI", zap.Error(err)) return err diff --git a/third_party/VENDOR-LICENSE b/third_party/VENDOR-LICENSE index 8b36c02a3be..dcf5fffc254 100644 --- a/third_party/VENDOR-LICENSE +++ b/third_party/VENDOR-LICENSE @@ -5898,6 +5898,33 @@ official policies, either expressed or implied, of Richard Crowley. +=========================================================== +Import: github.com/knative/eventing/vendor/github.com/robfig/cron + +Copyright (C) 2012 Rob Figueiredo +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + =========================================================== Import: github.com/knative/eventing/vendor/github.com/spf13/pflag diff --git a/vendor/github.com/robfig/cron/LICENSE b/vendor/github.com/robfig/cron/LICENSE new file mode 100644 index 00000000000..3a0f627ffeb --- /dev/null +++ b/vendor/github.com/robfig/cron/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2012 Rob Figueiredo +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/robfig/cron/constantdelay.go b/vendor/github.com/robfig/cron/constantdelay.go new file mode 100644 index 00000000000..cd6e7b1be91 --- /dev/null +++ b/vendor/github.com/robfig/cron/constantdelay.go @@ -0,0 +1,27 @@ +package cron + +import "time" + +// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". +// It does not support jobs more frequent than once a second. +type ConstantDelaySchedule struct { + Delay time.Duration +} + +// Every returns a crontab Schedule that activates once every duration. +// Delays of less than a second are not supported (will round up to 1 second). +// Any fields less than a Second are truncated. +func Every(duration time.Duration) ConstantDelaySchedule { + if duration < time.Second { + duration = time.Second + } + return ConstantDelaySchedule{ + Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, + } +} + +// Next returns the next time this should be run. +// This rounds so that the next activation time will be on the second. +func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { + return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) +} diff --git a/vendor/github.com/robfig/cron/cron.go b/vendor/github.com/robfig/cron/cron.go new file mode 100644 index 00000000000..2318aeb2e7d --- /dev/null +++ b/vendor/github.com/robfig/cron/cron.go @@ -0,0 +1,259 @@ +package cron + +import ( + "log" + "runtime" + "sort" + "time" +) + +// Cron keeps track of any number of entries, invoking the associated func as +// specified by the schedule. It may be started, stopped, and the entries may +// be inspected while running. +type Cron struct { + entries []*Entry + stop chan struct{} + add chan *Entry + snapshot chan []*Entry + running bool + ErrorLog *log.Logger + location *time.Location +} + +// Job is an interface for submitted cron jobs. +type Job interface { + Run() +} + +// The Schedule describes a job's duty cycle. +type Schedule interface { + // Return the next activation time, later than the given time. + // Next is invoked initially, and then each time the job is run. + Next(time.Time) time.Time +} + +// Entry consists of a schedule and the func to execute on that schedule. +type Entry struct { + // The schedule on which this job should be run. + Schedule Schedule + + // The next time the job will run. This is the zero time if Cron has not been + // started or this entry's schedule is unsatisfiable + Next time.Time + + // The last time this job was run. This is the zero time if the job has never + // been run. + Prev time.Time + + // The Job to run. + Job Job +} + +// byTime is a wrapper for sorting the entry array by time +// (with zero time at the end). +type byTime []*Entry + +func (s byTime) Len() int { return len(s) } +func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byTime) Less(i, j int) bool { + // Two zero times should return false. + // Otherwise, zero is "greater" than any other time. + // (To sort it at the end of the list.) + if s[i].Next.IsZero() { + return false + } + if s[j].Next.IsZero() { + return true + } + return s[i].Next.Before(s[j].Next) +} + +// New returns a new Cron job runner, in the Local time zone. +func New() *Cron { + return NewWithLocation(time.Now().Location()) +} + +// NewWithLocation returns a new Cron job runner. +func NewWithLocation(location *time.Location) *Cron { + return &Cron{ + entries: nil, + add: make(chan *Entry), + stop: make(chan struct{}), + snapshot: make(chan []*Entry), + running: false, + ErrorLog: nil, + location: location, + } +} + +// A wrapper that turns a func() into a cron.Job +type FuncJob func() + +func (f FuncJob) Run() { f() } + +// AddFunc adds a func to the Cron to be run on the given schedule. +func (c *Cron) AddFunc(spec string, cmd func()) error { + return c.AddJob(spec, FuncJob(cmd)) +} + +// AddJob adds a Job to the Cron to be run on the given schedule. +func (c *Cron) AddJob(spec string, cmd Job) error { + schedule, err := Parse(spec) + if err != nil { + return err + } + c.Schedule(schedule, cmd) + return nil +} + +// Schedule adds a Job to the Cron to be run on the given schedule. +func (c *Cron) Schedule(schedule Schedule, cmd Job) { + entry := &Entry{ + Schedule: schedule, + Job: cmd, + } + if !c.running { + c.entries = append(c.entries, entry) + return + } + + c.add <- entry +} + +// Entries returns a snapshot of the cron entries. +func (c *Cron) Entries() []*Entry { + if c.running { + c.snapshot <- nil + x := <-c.snapshot + return x + } + return c.entrySnapshot() +} + +// Location gets the time zone location +func (c *Cron) Location() *time.Location { + return c.location +} + +// Start the cron scheduler in its own go-routine, or no-op if already started. +func (c *Cron) Start() { + if c.running { + return + } + c.running = true + go c.run() +} + +// Run the cron scheduler, or no-op if already running. +func (c *Cron) Run() { + if c.running { + return + } + c.running = true + c.run() +} + +func (c *Cron) runWithRecovery(j Job) { + defer func() { + if r := recover(); r != nil { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + c.logf("cron: panic running job: %v\n%s", r, buf) + } + }() + j.Run() +} + +// Run the scheduler. this is private just due to the need to synchronize +// access to the 'running' state variable. +func (c *Cron) run() { + // Figure out the next activation times for each entry. + now := c.now() + for _, entry := range c.entries { + entry.Next = entry.Schedule.Next(now) + } + + for { + // Determine the next entry to run. + sort.Sort(byTime(c.entries)) + + var timer *time.Timer + if len(c.entries) == 0 || c.entries[0].Next.IsZero() { + // If there are no entries yet, just sleep - it still handles new entries + // and stop requests. + timer = time.NewTimer(100000 * time.Hour) + } else { + timer = time.NewTimer(c.entries[0].Next.Sub(now)) + } + + for { + select { + case now = <-timer.C: + now = now.In(c.location) + // Run every entry whose next time was less than now + for _, e := range c.entries { + if e.Next.After(now) || e.Next.IsZero() { + break + } + go c.runWithRecovery(e.Job) + e.Prev = e.Next + e.Next = e.Schedule.Next(now) + } + + case newEntry := <-c.add: + timer.Stop() + now = c.now() + newEntry.Next = newEntry.Schedule.Next(now) + c.entries = append(c.entries, newEntry) + + case <-c.snapshot: + c.snapshot <- c.entrySnapshot() + continue + + case <-c.stop: + timer.Stop() + return + } + + break + } + } +} + +// Logs an error to stderr or to the configured error log +func (c *Cron) logf(format string, args ...interface{}) { + if c.ErrorLog != nil { + c.ErrorLog.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +// Stop stops the cron scheduler if it is running; otherwise it does nothing. +func (c *Cron) Stop() { + if !c.running { + return + } + c.stop <- struct{}{} + c.running = false +} + +// entrySnapshot returns a copy of the current cron entry list. +func (c *Cron) entrySnapshot() []*Entry { + entries := []*Entry{} + for _, e := range c.entries { + entries = append(entries, &Entry{ + Schedule: e.Schedule, + Next: e.Next, + Prev: e.Prev, + Job: e.Job, + }) + } + return entries +} + +// now returns current time in c location +func (c *Cron) now() time.Time { + return time.Now().In(c.location) +} diff --git a/vendor/github.com/robfig/cron/doc.go b/vendor/github.com/robfig/cron/doc.go new file mode 100644 index 00000000000..d02ec2f3b56 --- /dev/null +++ b/vendor/github.com/robfig/cron/doc.go @@ -0,0 +1,129 @@ +/* +Package cron implements a cron spec parser and job runner. + +Usage + +Callers may register Funcs to be invoked on a given schedule. Cron will run +them in their own goroutines. + + c := cron.New() + c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) + c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) + c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) + c.Start() + .. + // Funcs are invoked in their own goroutine, asynchronously. + ... + // Funcs may also be added to a running Cron + c.AddFunc("@daily", func() { fmt.Println("Every day") }) + .. + // Inspect the cron job entries' next and previous run times. + inspect(c.Entries()) + .. + c.Stop() // Stop the scheduler (does not stop any jobs already running). + +CRON Expression Format + +A cron expression represents a set of times, using 6 space-separated fields. + + Field name | Mandatory? | Allowed values | Allowed special characters + ---------- | ---------- | -------------- | -------------------------- + Seconds | Yes | 0-59 | * / , - + Minutes | Yes | 0-59 | * / , - + Hours | Yes | 0-23 | * / , - + Day of month | Yes | 1-31 | * / , - ? + Month | Yes | 1-12 or JAN-DEC | * / , - + Day of week | Yes | 0-6 or SUN-SAT | * / , - ? + +Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", +and "sun" are equally accepted. + +Special Characters + +Asterisk ( * ) + +The asterisk indicates that the cron expression will match for all values of the +field; e.g., using an asterisk in the 5th field (month) would indicate every +month. + +Slash ( / ) + +Slashes are used to describe increments of ranges. For example 3-59/15 in the +1st field (minutes) would indicate the 3rd minute of the hour and every 15 +minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", +that is, an increment over the largest possible range of the field. The form +"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the +increment until the end of that specific range. It does not wrap around. + +Comma ( , ) + +Commas are used to separate items of a list. For example, using "MON,WED,FRI" in +the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. + +Hyphen ( - ) + +Hyphens are used to define ranges. For example, 9-17 would indicate every +hour between 9am and 5pm inclusive. + +Question mark ( ? ) + +Question mark may be used instead of '*' for leaving either day-of-month or +day-of-week blank. + +Predefined schedules + +You may use one of several pre-defined schedules in place of a cron expression. + + Entry | Description | Equivalent To + ----- | ----------- | ------------- + @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + @weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0 + @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + @hourly | Run once an hour, beginning of hour | 0 0 * * * * + +Intervals + +You may also schedule a job to execute at fixed intervals, starting at the time it's added +or cron is run. This is supported by formatting the cron spec like this: + + @every + +where "duration" is a string accepted by time.ParseDuration +(http://golang.org/pkg/time/#ParseDuration). + +For example, "@every 1h30m10s" would indicate a schedule that activates after +1 hour, 30 minutes, 10 seconds, and then every interval after that. + +Note: The interval does not take the job runtime into account. For example, +if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, +it will have only 2 minutes of idle time between each run. + +Time zones + +All interpretation and scheduling is done in the machine's local time zone (as +provided by the Go time package (http://www.golang.org/pkg/time). + +Be aware that jobs scheduled during daylight-savings leap-ahead transitions will +not be run! + +Thread safety + +Since the Cron service runs concurrently with the calling code, some amount of +care must be taken to ensure proper synchronization. + +All cron methods are designed to be correctly synchronized as long as the caller +ensures that invocations have a clear happens-before ordering between them. + +Implementation + +Cron entries are stored in an array, sorted by their next activation time. Cron +sleeps until the next job is due to be run. + +Upon waking: + - it runs each entry that is active on that second + - it calculates the next run times for the jobs that were run + - it re-sorts the array of entries by next activation time. + - it goes to sleep until the soonest job. +*/ +package cron diff --git a/vendor/github.com/robfig/cron/parser.go b/vendor/github.com/robfig/cron/parser.go new file mode 100644 index 00000000000..a5e83c0a8dc --- /dev/null +++ b/vendor/github.com/robfig/cron/parser.go @@ -0,0 +1,380 @@ +package cron + +import ( + "fmt" + "math" + "strconv" + "strings" + "time" +) + +// Configuration options for creating a parser. Most options specify which +// fields should be included, while others enable features. If a field is not +// included the parser will assume a default value. These options do not change +// the order fields are parse in. +type ParseOption int + +const ( + Second ParseOption = 1 << iota // Seconds field, default 0 + Minute // Minutes field, default 0 + Hour // Hours field, default 0 + Dom // Day of month field, default * + Month // Month field, default * + Dow // Day of week field, default * + DowOptional // Optional day of week field, default * + Descriptor // Allow descriptors such as @monthly, @weekly, etc. +) + +var places = []ParseOption{ + Second, + Minute, + Hour, + Dom, + Month, + Dow, +} + +var defaults = []string{ + "0", + "0", + "0", + "*", + "*", + "*", +} + +// A custom Parser that can be configured. +type Parser struct { + options ParseOption + optionals int +} + +// Creates a custom Parser with custom options. +// +// // Standard parser without descriptors +// specParser := NewParser(Minute | Hour | Dom | Month | Dow) +// sched, err := specParser.Parse("0 0 15 */3 *") +// +// // Same as above, just excludes time fields +// subsParser := NewParser(Dom | Month | Dow) +// sched, err := specParser.Parse("15 */3 *") +// +// // Same as above, just makes Dow optional +// subsParser := NewParser(Dom | Month | DowOptional) +// sched, err := specParser.Parse("15 */3") +// +func NewParser(options ParseOption) Parser { + optionals := 0 + if options&DowOptional > 0 { + options |= Dow + optionals++ + } + return Parser{options, optionals} +} + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// It accepts crontab specs and features configured by NewParser. +func (p Parser) Parse(spec string) (Schedule, error) { + if len(spec) == 0 { + return nil, fmt.Errorf("Empty spec string") + } + if spec[0] == '@' && p.options&Descriptor > 0 { + return parseDescriptor(spec) + } + + // Figure out how many fields we need + max := 0 + for _, place := range places { + if p.options&place > 0 { + max++ + } + } + min := max - p.optionals + + // Split fields on whitespace + fields := strings.Fields(spec) + + // Validate number of fields + if count := len(fields); count < min || count > max { + if min == max { + return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec) + } + return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec) + } + + // Fill in missing fields + fields = expandFields(fields, p.options) + + var err error + field := func(field string, r bounds) uint64 { + if err != nil { + return 0 + } + var bits uint64 + bits, err = getField(field, r) + return bits + } + + var ( + second = field(fields[0], seconds) + minute = field(fields[1], minutes) + hour = field(fields[2], hours) + dayofmonth = field(fields[3], dom) + month = field(fields[4], months) + dayofweek = field(fields[5], dow) + ) + if err != nil { + return nil, err + } + + return &SpecSchedule{ + Second: second, + Minute: minute, + Hour: hour, + Dom: dayofmonth, + Month: month, + Dow: dayofweek, + }, nil +} + +func expandFields(fields []string, options ParseOption) []string { + n := 0 + count := len(fields) + expFields := make([]string, len(places)) + copy(expFields, defaults) + for i, place := range places { + if options&place > 0 { + expFields[i] = fields[n] + n++ + } + if n == count { + break + } + } + return expFields +} + +var standardParser = NewParser( + Minute | Hour | Dom | Month | Dow | Descriptor, +) + +// ParseStandard returns a new crontab schedule representing the given standardSpec +// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always +// pass 5 entries representing: minute, hour, day of month, month and day of week, +// in that order. It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Standard crontab specs, e.g. "* * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func ParseStandard(standardSpec string) (Schedule, error) { + return standardParser.Parse(standardSpec) +} + +var defaultParser = NewParser( + Second | Minute | Hour | Dom | Month | DowOptional | Descriptor, +) + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Full crontab specs, e.g. "* * * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func Parse(spec string) (Schedule, error) { + return defaultParser.Parse(spec) +} + +// getField returns an Int with the bits set representing all of the times that +// the field represents or error parsing field value. A "field" is a comma-separated +// list of "ranges". +func getField(field string, r bounds) (uint64, error) { + var bits uint64 + ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) + for _, expr := range ranges { + bit, err := getRange(expr, r) + if err != nil { + return bits, err + } + bits |= bit + } + return bits, nil +} + +// getRange returns the bits indicated by the given expression: +// number | number "-" number [ "/" number ] +// or error parsing range. +func getRange(expr string, r bounds) (uint64, error) { + var ( + start, end, step uint + rangeAndStep = strings.Split(expr, "/") + lowAndHigh = strings.Split(rangeAndStep[0], "-") + singleDigit = len(lowAndHigh) == 1 + err error + ) + + var extra uint64 + if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { + start = r.min + end = r.max + extra = starBit + } else { + start, err = parseIntOrName(lowAndHigh[0], r.names) + if err != nil { + return 0, err + } + switch len(lowAndHigh) { + case 1: + end = start + case 2: + end, err = parseIntOrName(lowAndHigh[1], r.names) + if err != nil { + return 0, err + } + default: + return 0, fmt.Errorf("Too many hyphens: %s", expr) + } + } + + switch len(rangeAndStep) { + case 1: + step = 1 + case 2: + step, err = mustParseInt(rangeAndStep[1]) + if err != nil { + return 0, err + } + + // Special handling: "N/step" means "N-max/step". + if singleDigit { + end = r.max + } + default: + return 0, fmt.Errorf("Too many slashes: %s", expr) + } + + if start < r.min { + return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) + } + if end > r.max { + return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr) + } + if start > end { + return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) + } + if step == 0 { + return 0, fmt.Errorf("Step of range should be a positive number: %s", expr) + } + + return getBits(start, end, step) | extra, nil +} + +// parseIntOrName returns the (possibly-named) integer contained in expr. +func parseIntOrName(expr string, names map[string]uint) (uint, error) { + if names != nil { + if namedInt, ok := names[strings.ToLower(expr)]; ok { + return namedInt, nil + } + } + return mustParseInt(expr) +} + +// mustParseInt parses the given expression as an int or returns an error. +func mustParseInt(expr string) (uint, error) { + num, err := strconv.Atoi(expr) + if err != nil { + return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err) + } + if num < 0 { + return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr) + } + + return uint(num), nil +} + +// getBits sets all bits in the range [min, max], modulo the given step size. +func getBits(min, max, step uint) uint64 { + var bits uint64 + + // If step is 1, use shifts. + if step == 1 { + return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) + } + + // Else, use a simple loop. + for i := min; i <= max; i += step { + bits |= 1 << i + } + return bits +} + +// all returns all bits within the given bounds. (plus the star bit) +func all(r bounds) uint64 { + return getBits(r.min, r.max, 1) | starBit +} + +// parseDescriptor returns a predefined schedule for the expression, or error if none matches. +func parseDescriptor(descriptor string) (Schedule, error) { + switch descriptor { + case "@yearly", "@annually": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: 1 << months.min, + Dow: all(dow), + }, nil + + case "@monthly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: all(months), + Dow: all(dow), + }, nil + + case "@weekly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: 1 << dow.min, + }, nil + + case "@daily", "@midnight": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: all(dow), + }, nil + + case "@hourly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: all(hours), + Dom: all(dom), + Month: all(months), + Dow: all(dow), + }, nil + } + + const every = "@every " + if strings.HasPrefix(descriptor, every) { + duration, err := time.ParseDuration(descriptor[len(every):]) + if err != nil { + return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err) + } + return Every(duration), nil + } + + return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor) +} diff --git a/vendor/github.com/robfig/cron/spec.go b/vendor/github.com/robfig/cron/spec.go new file mode 100644 index 00000000000..aac9a60b954 --- /dev/null +++ b/vendor/github.com/robfig/cron/spec.go @@ -0,0 +1,158 @@ +package cron + +import "time" + +// SpecSchedule specifies a duty cycle (to the second granularity), based on a +// traditional crontab specification. It is computed initially and stored as bit sets. +type SpecSchedule struct { + Second, Minute, Hour, Dom, Month, Dow uint64 +} + +// bounds provides a range of acceptable values (plus a map of name to value). +type bounds struct { + min, max uint + names map[string]uint +} + +// The bounds for each field. +var ( + seconds = bounds{0, 59, nil} + minutes = bounds{0, 59, nil} + hours = bounds{0, 23, nil} + dom = bounds{1, 31, nil} + months = bounds{1, 12, map[string]uint{ + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + }} + dow = bounds{0, 6, map[string]uint{ + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, + }} +) + +const ( + // Set the top bit if a star was included in the expression. + starBit = 1 << 63 +) + +// Next returns the next time this schedule is activated, greater than the given +// time. If no time can be found to satisfy the schedule, return the zero time. +func (s *SpecSchedule) Next(t time.Time) time.Time { + // General approach: + // For Month, Day, Hour, Minute, Second: + // Check if the time value matches. If yes, continue to the next field. + // If the field doesn't match the schedule, then increment the field until it matches. + // While incrementing the field, a wrap-around brings it back to the beginning + // of the field list (since it is necessary to re-verify previous field + // values) + + // Start at the earliest possible time (the upcoming second). + t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) + + // This flag indicates whether a field has been incremented. + added := false + + // If no time is found within five years, return zero. + yearLimit := t.Year() + 5 + +WRAP: + if t.Year() > yearLimit { + return time.Time{} + } + + // Find the first applicable month. + // If it's this month, then do nothing. + for 1< 0 + dowMatch bool = 1< 0 + ) + if s.Dom&starBit > 0 || s.Dow&starBit > 0 { + return domMatch && dowMatch + } + return domMatch || dowMatch +} diff --git a/vendor/golang.org/x/oauth2/google/appengine.go b/vendor/golang.org/x/oauth2/google/appengine.go index 50d918b8788..feb1157b15b 100644 --- a/vendor/golang.org/x/oauth2/google/appengine.go +++ b/vendor/golang.org/x/oauth2/google/appengine.go @@ -5,85 +5,34 @@ package google import ( - "sort" - "strings" - "sync" + "context" "time" - "golang.org/x/net/context" "golang.org/x/oauth2" ) -// appengineFlex is set at init time by appengineflex_hook.go. If true, we are on App Engine Flex. -var appengineFlex bool - -// Set at init time by appengine_hook.go. If nil, we're not on App Engine. +// Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible. var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error) -// Set at init time by appengine_hook.go. If nil, we're not on App Engine. +// Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible. var appengineAppIDFunc func(c context.Context) string -// AppEngineTokenSource returns a token source that fetches tokens -// issued to the current App Engine application's service account. -// If you are implementing a 3-legged OAuth 2.0 flow on App Engine -// that involves user accounts, see oauth2.Config instead. +// AppEngineTokenSource returns a token source that fetches tokens from either +// the current application's service account or from the metadata server, +// depending on the App Engine environment. See below for environment-specific +// details. If you are implementing a 3-legged OAuth 2.0 flow on App Engine that +// involves user accounts, see oauth2.Config instead. +// +// First generation App Engine runtimes (<= Go 1.9): +// AppEngineTokenSource returns a token source that fetches tokens issued to the +// current App Engine application's service account. The provided context must have +// come from appengine.NewContext. // -// The provided context must have come from appengine.NewContext. +// Second generation App Engine runtimes (>= Go 1.11) and App Engine flexible: +// AppEngineTokenSource is DEPRECATED on second generation runtimes and on the +// flexible environment. It delegates to ComputeTokenSource, and the provided +// context and scopes are not used. Please use DefaultTokenSource (or ComputeTokenSource, +// which DefaultTokenSource will use in this case) instead. func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { - if appengineTokenFunc == nil { - panic("google: AppEngineTokenSource can only be used on App Engine.") - } - scopes := append([]string{}, scope...) - sort.Strings(scopes) - return &appEngineTokenSource{ - ctx: ctx, - scopes: scopes, - key: strings.Join(scopes, " "), - } -} - -// aeTokens helps the fetched tokens to be reused until their expiration. -var ( - aeTokensMu sync.Mutex - aeTokens = make(map[string]*tokenLock) // key is space-separated scopes -) - -type tokenLock struct { - mu sync.Mutex // guards t; held while fetching or updating t - t *oauth2.Token -} - -type appEngineTokenSource struct { - ctx context.Context - scopes []string - key string // to aeTokens map; space-separated scopes -} - -func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) { - if appengineTokenFunc == nil { - panic("google: AppEngineTokenSource can only be used on App Engine.") - } - - aeTokensMu.Lock() - tok, ok := aeTokens[ts.key] - if !ok { - tok = &tokenLock{} - aeTokens[ts.key] = tok - } - aeTokensMu.Unlock() - - tok.mu.Lock() - defer tok.mu.Unlock() - if tok.t.Valid() { - return tok.t, nil - } - access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...) - if err != nil { - return nil, err - } - tok.t = &oauth2.Token{ - AccessToken: access, - Expiry: exp, - } - return tok.t, nil + return appEngineTokenSource(ctx, scope...) } diff --git a/vendor/golang.org/x/oauth2/google/appengine_gen1.go b/vendor/golang.org/x/oauth2/google/appengine_gen1.go new file mode 100644 index 00000000000..83dacac320a --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/appengine_gen1.go @@ -0,0 +1,77 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build appengine + +// This file applies to App Engine first generation runtimes (<= Go 1.9). + +package google + +import ( + "context" + "sort" + "strings" + "sync" + + "golang.org/x/oauth2" + "google.golang.org/appengine" +) + +func init() { + appengineTokenFunc = appengine.AccessToken + appengineAppIDFunc = appengine.AppID +} + +// See comment on AppEngineTokenSource in appengine.go. +func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { + scopes := append([]string{}, scope...) + sort.Strings(scopes) + return &gaeTokenSource{ + ctx: ctx, + scopes: scopes, + key: strings.Join(scopes, " "), + } +} + +// aeTokens helps the fetched tokens to be reused until their expiration. +var ( + aeTokensMu sync.Mutex + aeTokens = make(map[string]*tokenLock) // key is space-separated scopes +) + +type tokenLock struct { + mu sync.Mutex // guards t; held while fetching or updating t + t *oauth2.Token +} + +type gaeTokenSource struct { + ctx context.Context + scopes []string + key string // to aeTokens map; space-separated scopes +} + +func (ts *gaeTokenSource) Token() (*oauth2.Token, error) { + aeTokensMu.Lock() + tok, ok := aeTokens[ts.key] + if !ok { + tok = &tokenLock{} + aeTokens[ts.key] = tok + } + aeTokensMu.Unlock() + + tok.mu.Lock() + defer tok.mu.Unlock() + if tok.t.Valid() { + return tok.t, nil + } + access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...) + if err != nil { + return nil, err + } + tok.t = &oauth2.Token{ + AccessToken: access, + Expiry: exp, + } + return tok.t, nil +} diff --git a/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go b/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go new file mode 100644 index 00000000000..04c2c2216af --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go @@ -0,0 +1,27 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !appengine + +// This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible. + +package google + +import ( + "context" + "log" + "sync" + + "golang.org/x/oauth2" +) + +var logOnce sync.Once // only spam about deprecation once + +// See comment on AppEngineTokenSource in appengine.go. +func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { + logOnce.Do(func() { + log.Print("google: AppEngineTokenSource is deprecated on App Engine standard second generation runtimes (>= Go 1.11) and App Engine flexible. Please use DefaultTokenSource or ComputeTokenSource.") + }) + return ComputeTokenSource("") +} diff --git a/vendor/golang.org/x/oauth2/google/appengine_hook.go b/vendor/golang.org/x/oauth2/google/appengine_hook.go deleted file mode 100644 index 56669eaa98d..00000000000 --- a/vendor/golang.org/x/oauth2/google/appengine_hook.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build appengine appenginevm - -package google - -import "google.golang.org/appengine" - -func init() { - appengineTokenFunc = appengine.AccessToken - appengineAppIDFunc = appengine.AppID -} diff --git a/vendor/golang.org/x/oauth2/google/appengineflex_hook.go b/vendor/golang.org/x/oauth2/google/appengineflex_hook.go deleted file mode 100644 index 5d0231af2dd..00000000000 --- a/vendor/golang.org/x/oauth2/google/appengineflex_hook.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build appenginevm - -package google - -func init() { - appengineFlex = true // Flex doesn't support appengine.AccessToken; depend on metadata server. -} diff --git a/vendor/golang.org/x/oauth2/google/default.go b/vendor/golang.org/x/oauth2/google/default.go index a31607437d3..ad2c09236c5 100644 --- a/vendor/golang.org/x/oauth2/google/default.go +++ b/vendor/golang.org/x/oauth2/google/default.go @@ -5,6 +5,7 @@ package google import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -14,10 +15,28 @@ import ( "runtime" "cloud.google.com/go/compute/metadata" - "golang.org/x/net/context" "golang.org/x/oauth2" ) +// Credentials holds Google credentials, including "Application Default Credentials". +// For more details, see: +// https://developers.google.com/accounts/docs/application-default-credentials +type Credentials struct { + ProjectID string // may be empty + TokenSource oauth2.TokenSource + + // JSON contains the raw bytes from a JSON credentials file. + // This field may be nil if authentication is provided by the + // environment and not with a credentials file, e.g. when code is + // running on Google Cloud Platform. + JSON []byte +} + +// DefaultCredentials is the old name of Credentials. +// +// Deprecated: use Credentials instead. +type DefaultCredentials = Credentials + // DefaultClient returns an HTTP Client that uses the // DefaultTokenSource to obtain authentication credentials. func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { @@ -39,8 +58,22 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc return creds.TokenSource, nil } -// Common implementation for FindDefaultCredentials. -func findDefaultCredentials(ctx context.Context, scopes []string) (*DefaultCredentials, error) { +// FindDefaultCredentials searches for "Application Default Credentials". +// +// It looks for credentials in the following places, +// preferring the first location found: +// +// 1. A JSON file whose path is specified by the +// GOOGLE_APPLICATION_CREDENTIALS environment variable. +// 2. A JSON file in a location known to the gcloud command-line tool. +// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. +// On other systems, $HOME/.config/gcloud/application_default_credentials.json. +// 3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses +// the appengine.AccessToken function. +// 4. On Google Compute Engine, Google App Engine standard second generation runtimes +// (>= Go 1.11), and Google App Engine flexible environment, it fetches +// credentials from the metadata server. +func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { // First, try the environment variable. const envVar = "GOOGLE_APPLICATION_CREDENTIALS" if filename := os.Getenv(envVar); filename != "" { @@ -59,20 +92,23 @@ func findDefaultCredentials(ctx context.Context, scopes []string) (*DefaultCrede return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) } - // Third, if we're on Google App Engine use those credentials. - if appengineTokenFunc != nil && !appengineFlex { + // Third, if we're on a Google App Engine standard first generation runtime (<= Go 1.9) + // use those credentials. App Engine standard second generation runtimes (>= Go 1.11) + // and App Engine flexible use ComputeTokenSource and the metadata server. + if appengineTokenFunc != nil { return &DefaultCredentials{ ProjectID: appengineAppIDFunc(ctx), TokenSource: AppEngineTokenSource(ctx, scopes...), }, nil } - // Fourth, if we're on Google Compute Engine use the metadata server. + // Fourth, if we're on Google Compute Engine, an App Engine standard second generation runtime, + // or App Engine flexible, use the metadata server. if metadata.OnGCE() { id, _ := metadata.ProjectID() return &DefaultCredentials{ ProjectID: id, - TokenSource: ComputeTokenSource(""), + TokenSource: ComputeTokenSource("", scopes...), }, nil } @@ -81,8 +117,11 @@ func findDefaultCredentials(ctx context.Context, scopes []string) (*DefaultCrede return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url) } -// Common implementation for CredentialsFromJSON. -func credentialsFromJSON(ctx context.Context, jsonData []byte, scopes []string) (*DefaultCredentials, error) { +// CredentialsFromJSON obtains Google credentials from a JSON value. The JSON can +// represent either a Google Developers Console client_credentials.json file (as in +// ConfigFromJSON) or a Google Developers service account key file (as in +// JWTConfigFromJSON). +func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { var f credentialsFile if err := json.Unmarshal(jsonData, &f); err != nil { return nil, err diff --git a/vendor/golang.org/x/oauth2/google/doc_go19.go b/vendor/golang.org/x/oauth2/google/doc.go similarity index 99% rename from vendor/golang.org/x/oauth2/google/doc_go19.go rename to vendor/golang.org/x/oauth2/google/doc.go index 2a86325fe3b..73be629033d 100644 --- a/vendor/golang.org/x/oauth2/google/doc_go19.go +++ b/vendor/golang.org/x/oauth2/google/doc.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.9 - // Package google provides support for making OAuth2 authorized and authenticated // HTTP requests to Google APIs. It supports the Web server flow, client-side // credentials, service accounts, Google Compute Engine service accounts, and Google diff --git a/vendor/golang.org/x/oauth2/google/doc_not_go19.go b/vendor/golang.org/x/oauth2/google/doc_not_go19.go deleted file mode 100644 index 5c3c6e14812..00000000000 --- a/vendor/golang.org/x/oauth2/google/doc_not_go19.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.9 - -// Package google provides support for making OAuth2 authorized and authenticated -// HTTP requests to Google APIs. It supports the Web server flow, client-side -// credentials, service accounts, Google Compute Engine service accounts, and Google -// App Engine service accounts. -// -// A brief overview of the package follows. For more information, please read -// https://developers.google.com/accounts/docs/OAuth2 -// and -// https://developers.google.com/accounts/docs/application-default-credentials. -// -// OAuth2 Configs -// -// Two functions in this package return golang.org/x/oauth2.Config values from Google credential -// data. Google supports two JSON formats for OAuth2 credentials: one is handled by ConfigFromJSON, -// the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or -// create an http.Client. -// -// -// Credentials -// -// The DefaultCredentials type represents Google Application Default Credentials, as -// well as other forms of credential. -// -// Use FindDefaultCredentials to obtain Application Default Credentials. -// FindDefaultCredentials looks in some well-known places for a credentials file, and -// will call AppEngineTokenSource or ComputeTokenSource as needed. -// -// DefaultClient and DefaultTokenSource are convenience methods. They first call FindDefaultCredentials, -// then use the credentials to construct an http.Client or an oauth2.TokenSource. -// -// Use CredentialsFromJSON to obtain credentials from either of the two JSON -// formats described in OAuth2 Configs, above. (The DefaultCredentials returned may -// not be "Application Default Credentials".) The TokenSource in the returned value -// is the same as the one obtained from the oauth2.Config returned from -// ConfigFromJSON or JWTConfigFromJSON, but the DefaultCredentials may contain -// additional information that is useful is some circumstances. -package google // import "golang.org/x/oauth2/google" diff --git a/vendor/golang.org/x/oauth2/google/go19.go b/vendor/golang.org/x/oauth2/google/go19.go deleted file mode 100644 index 4d0318b1e16..00000000000 --- a/vendor/golang.org/x/oauth2/google/go19.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.9 - -package google - -import ( - "golang.org/x/net/context" - "golang.org/x/oauth2" -) - -// Credentials holds Google credentials, including "Application Default Credentials". -// For more details, see: -// https://developers.google.com/accounts/docs/application-default-credentials -type Credentials struct { - ProjectID string // may be empty - TokenSource oauth2.TokenSource - - // JSON contains the raw bytes from a JSON credentials file. - // This field may be nil if authentication is provided by the - // environment and not with a credentials file, e.g. when code is - // running on Google Cloud Platform. - JSON []byte -} - -// DefaultCredentials is the old name of Credentials. -// -// Deprecated: use Credentials instead. -type DefaultCredentials = Credentials - -// FindDefaultCredentials searches for "Application Default Credentials". -// -// It looks for credentials in the following places, -// preferring the first location found: -// -// 1. A JSON file whose path is specified by the -// GOOGLE_APPLICATION_CREDENTIALS environment variable. -// 2. A JSON file in a location known to the gcloud command-line tool. -// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. -// On other systems, $HOME/.config/gcloud/application_default_credentials.json. -// 3. On Google App Engine it uses the appengine.AccessToken function. -// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches -// credentials from the metadata server. -// (In this final case any provided scopes are ignored.) -func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { - return findDefaultCredentials(ctx, scopes) -} - -// CredentialsFromJSON obtains Google credentials from a JSON value. The JSON can -// represent either a Google Developers Console client_credentials.json file (as in -// ConfigFromJSON) or a Google Developers service account key file (as in -// JWTConfigFromJSON). -func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { - return credentialsFromJSON(ctx, jsonData, scopes) -} diff --git a/vendor/golang.org/x/oauth2/google/google.go b/vendor/golang.org/x/oauth2/google/google.go index f7481fbcc63..6eb2aa95f5b 100644 --- a/vendor/golang.org/x/oauth2/google/google.go +++ b/vendor/golang.org/x/oauth2/google/google.go @@ -5,26 +5,28 @@ package google import ( + "context" "encoding/json" "errors" "fmt" + "net/url" "strings" "time" "cloud.google.com/go/compute/metadata" - "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/jwt" ) // Endpoint is Google's OAuth 2.0 endpoint. var Endpoint = oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", + AuthURL: "https://accounts.google.com/o/oauth2/auth", + TokenURL: "https://oauth2.googleapis.com/token", + AuthStyle: oauth2.AuthStyleInParams, } // JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow. -const JWTTokenURL = "https://accounts.google.com/o/oauth2/token" +const JWTTokenURL = "https://oauth2.googleapis.com/token" // ConfigFromJSON uses a Google Developers Console client_credentials.json // file to construct a config. @@ -150,14 +152,16 @@ func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oau // from Google Compute Engine (GCE)'s metadata server. It's only valid to use // this token source if your program is running on a GCE instance. // If no account is specified, "default" is used. +// If no scopes are specified, a set of default scopes are automatically granted. // Further information about retrieving access tokens from the GCE metadata // server can be found at https://cloud.google.com/compute/docs/authentication. -func ComputeTokenSource(account string) oauth2.TokenSource { - return oauth2.ReuseTokenSource(nil, computeSource{account: account}) +func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, computeSource{account: account, scopes: scope}) } type computeSource struct { account string + scopes []string } func (cs computeSource) Token() (*oauth2.Token, error) { @@ -168,7 +172,13 @@ func (cs computeSource) Token() (*oauth2.Token, error) { if acct == "" { acct = "default" } - tokenJSON, err := metadata.Get("instance/service-accounts/" + acct + "/token") + tokenURI := "instance/service-accounts/" + acct + "/token" + if len(cs.scopes) > 0 { + v := url.Values{} + v.Set("scopes", strings.Join(cs.scopes, ",")) + tokenURI = tokenURI + "?" + v.Encode() + } + tokenJSON, err := metadata.Get(tokenURI) if err != nil { return nil, err } diff --git a/vendor/golang.org/x/oauth2/google/not_go19.go b/vendor/golang.org/x/oauth2/google/not_go19.go deleted file mode 100644 index 544e40624e1..00000000000 --- a/vendor/golang.org/x/oauth2/google/not_go19.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.9 - -package google - -import ( - "golang.org/x/net/context" - "golang.org/x/oauth2" -) - -// DefaultCredentials holds Google credentials, including "Application Default Credentials". -// For more details, see: -// https://developers.google.com/accounts/docs/application-default-credentials -type DefaultCredentials struct { - ProjectID string // may be empty - TokenSource oauth2.TokenSource - - // JSON contains the raw bytes from a JSON credentials file. - // This field may be nil if authentication is provided by the - // environment and not with a credentials file, e.g. when code is - // running on Google Cloud Platform. - JSON []byte -} - -// FindDefaultCredentials searches for "Application Default Credentials". -// -// It looks for credentials in the following places, -// preferring the first location found: -// -// 1. A JSON file whose path is specified by the -// GOOGLE_APPLICATION_CREDENTIALS environment variable. -// 2. A JSON file in a location known to the gcloud command-line tool. -// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. -// On other systems, $HOME/.config/gcloud/application_default_credentials.json. -// 3. On Google App Engine it uses the appengine.AccessToken function. -// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches -// credentials from the metadata server. -// (In this final case any provided scopes are ignored.) -func FindDefaultCredentials(ctx context.Context, scopes ...string) (*DefaultCredentials, error) { - return findDefaultCredentials(ctx, scopes) -} - -// CredentialsFromJSON obtains Google credentials from a JSON value. The JSON can -// represent either a Google Developers Console client_credentials.json file (as in -// ConfigFromJSON) or a Google Developers service account key file (as in -// JWTConfigFromJSON). -// -// Note: despite the name, the returned credentials may not be Application Default Credentials. -func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*DefaultCredentials, error) { - return credentialsFromJSON(ctx, jsonData, scopes) -} diff --git a/vendor/golang.org/x/oauth2/google/sdk.go b/vendor/golang.org/x/oauth2/google/sdk.go index b9660caddf0..456224bc789 100644 --- a/vendor/golang.org/x/oauth2/google/sdk.go +++ b/vendor/golang.org/x/oauth2/google/sdk.go @@ -6,6 +6,7 @@ package google import ( "bufio" + "context" "encoding/json" "errors" "fmt" @@ -18,7 +19,6 @@ import ( "strings" "time" - "golang.org/x/net/context" "golang.org/x/oauth2" ) diff --git a/vendor/golang.org/x/oauth2/internal/oauth2.go b/vendor/golang.org/x/oauth2/internal/oauth2.go index fc63fcab3ff..c0ab196cf46 100644 --- a/vendor/golang.org/x/oauth2/internal/oauth2.go +++ b/vendor/golang.org/x/oauth2/internal/oauth2.go @@ -26,7 +26,7 @@ func ParseKey(key []byte) (*rsa.PrivateKey, error) { if err != nil { parsedKey, err = x509.ParsePKCS1PrivateKey(key) if err != nil { - return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err) + return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err) } } parsed, ok := parsedKey.(*rsa.PrivateKey) diff --git a/vendor/golang.org/x/oauth2/internal/token.go b/vendor/golang.org/x/oauth2/internal/token.go index 30fb315d139..355c386961d 100644 --- a/vendor/golang.org/x/oauth2/internal/token.go +++ b/vendor/golang.org/x/oauth2/internal/token.go @@ -5,19 +5,21 @@ package internal import ( + "context" "encoding/json" "errors" "fmt" "io" "io/ioutil" + "math" "mime" "net/http" "net/url" "strconv" "strings" + "sync" "time" - "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" ) @@ -61,22 +63,21 @@ type tokenJSON struct { TokenType string `json:"token_type"` RefreshToken string `json:"refresh_token"` ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number - Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in } func (e *tokenJSON) expiry() (t time.Time) { if v := e.ExpiresIn; v != 0 { return time.Now().Add(time.Duration(v) * time.Second) } - if v := e.Expires; v != 0 { - return time.Now().Add(time.Duration(v) * time.Second) - } return } type expirationTime int32 func (e *expirationTime) UnmarshalJSON(b []byte) error { + if len(b) == 0 || string(b) == "null" { + return nil + } var n json.Number err := json.Unmarshal(b, &n) if err != nil { @@ -86,97 +87,78 @@ func (e *expirationTime) UnmarshalJSON(b []byte) error { if err != nil { return err } + if i > math.MaxInt32 { + i = math.MaxInt32 + } *e = expirationTime(i) return nil } -var brokenAuthHeaderProviders = []string{ - "https://accounts.google.com/", - "https://api.codeswholesale.com/oauth/token", - "https://api.dropbox.com/", - "https://api.dropboxapi.com/", - "https://api.instagram.com/", - "https://api.netatmo.net/", - "https://api.odnoklassniki.ru/", - "https://api.pushbullet.com/", - "https://api.soundcloud.com/", - "https://api.twitch.tv/", - "https://app.box.com/", - "https://connect.stripe.com/", - "https://login.mailchimp.com/", - "https://login.microsoftonline.com/", - "https://login.salesforce.com/", - "https://login.windows.net", - "https://login.live.com/", - "https://oauth.sandbox.trainingpeaks.com/", - "https://oauth.trainingpeaks.com/", - "https://oauth.vk.com/", - "https://openapi.baidu.com/", - "https://slack.com/", - "https://test-sandbox.auth.corp.google.com", - "https://test.salesforce.com/", - "https://user.gini.net/", - "https://www.douban.com/", - "https://www.googleapis.com/", - "https://www.linkedin.com/", - "https://www.strava.com/oauth/", - "https://www.wunderlist.com/oauth/", - "https://api.patreon.com/", - "https://sandbox.codeswholesale.com/oauth/token", - "https://api.sipgate.com/v1/authorization/oauth", - "https://api.medium.com/v1/tokens", - "https://log.finalsurge.com/oauth/token", - "https://multisport.todaysplan.com.au/rest/oauth/access_token", - "https://whats.todaysplan.com.au/rest/oauth/access_token", -} +// RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op. +// +// Deprecated: this function no longer does anything. Caller code that +// wants to avoid potential extra HTTP requests made during +// auto-probing of the provider's auth style should set +// Endpoint.AuthStyle. +func RegisterBrokenAuthHeaderProvider(tokenURL string) {} + +// AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type. +type AuthStyle int -// brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints. -var brokenAuthHeaderDomains = []string{ - ".auth0.com", - ".force.com", - ".myshopify.com", - ".okta.com", - ".oktapreview.com", +const ( + AuthStyleUnknown AuthStyle = 0 + AuthStyleInParams AuthStyle = 1 + AuthStyleInHeader AuthStyle = 2 +) + +// authStyleCache is the set of tokenURLs we've successfully used via +// RetrieveToken and which style auth we ended up using. +// It's called a cache, but it doesn't (yet?) shrink. It's expected that +// the set of OAuth2 servers a program contacts over time is fixed and +// small. +var authStyleCache struct { + sync.Mutex + m map[string]AuthStyle // keyed by tokenURL } -func RegisterBrokenAuthHeaderProvider(tokenURL string) { - brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL) +// ResetAuthCache resets the global authentication style cache used +// for AuthStyleUnknown token requests. +func ResetAuthCache() { + authStyleCache.Lock() + defer authStyleCache.Unlock() + authStyleCache.m = nil } -// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL -// implements the OAuth2 spec correctly -// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. -// In summary: -// - Reddit only accepts client secret in the Authorization header -// - Dropbox accepts either it in URL param or Auth header, but not both. -// - Google only accepts URL param (not spec compliant?), not Auth header -// - Stripe only accepts client secret in Auth header with Bearer method, not Basic -func providerAuthHeaderWorks(tokenURL string) bool { - for _, s := range brokenAuthHeaderProviders { - if strings.HasPrefix(tokenURL, s) { - // Some sites fail to implement the OAuth2 spec fully. - return false - } - } +// lookupAuthStyle reports which auth style we last used with tokenURL +// when calling RetrieveToken and whether we have ever done so. +func lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) { + authStyleCache.Lock() + defer authStyleCache.Unlock() + style, ok = authStyleCache.m[tokenURL] + return +} - if u, err := url.Parse(tokenURL); err == nil { - for _, s := range brokenAuthHeaderDomains { - if strings.HasSuffix(u.Host, s) { - return false - } - } +// setAuthStyle adds an entry to authStyleCache, documented above. +func setAuthStyle(tokenURL string, v AuthStyle) { + authStyleCache.Lock() + defer authStyleCache.Unlock() + if authStyleCache.m == nil { + authStyleCache.m = make(map[string]AuthStyle) } - - // Assume the provider implements the spec properly - // otherwise. We can add more exceptions as they're - // discovered. We will _not_ be adding configurable hooks - // to this package to let users select server bugs. - return true + authStyleCache.m[tokenURL] = v } -func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) { - bustedAuth := !providerAuthHeaderWorks(tokenURL) - if bustedAuth { +// newTokenRequest returns a new *http.Request to retrieve a new token +// from tokenURL using the provided clientID, clientSecret, and POST +// body parameters. +// +// inParams is whether the clientID & clientSecret should be encoded +// as the POST body. An 'inParams' value of true means to send it in +// the POST body (along with any values in v); false means to send it +// in the Authorization header. +func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) { + if authStyle == AuthStyleInParams { + v = cloneURLValues(v) if clientID != "" { v.Set("client_id", clientID) } @@ -189,15 +171,70 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if !bustedAuth { + if authStyle == AuthStyleInHeader { req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret)) } + return req, nil +} + +func cloneURLValues(v url.Values) url.Values { + v2 := make(url.Values, len(v)) + for k, vv := range v { + v2[k] = append([]string(nil), vv...) + } + return v2 +} + +func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle) (*Token, error) { + needsAuthStyleProbe := authStyle == 0 + if needsAuthStyleProbe { + if style, ok := lookupAuthStyle(tokenURL); ok { + authStyle = style + needsAuthStyleProbe = false + } else { + authStyle = AuthStyleInHeader // the first way we'll try + } + } + req, err := newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle) + if err != nil { + return nil, err + } + token, err := doTokenRoundTrip(ctx, req) + if err != nil && needsAuthStyleProbe { + // If we get an error, assume the server wants the + // clientID & clientSecret in a different form. + // See https://code.google.com/p/goauth2/issues/detail?id=31 for background. + // In summary: + // - Reddit only accepts client secret in the Authorization header + // - Dropbox accepts either it in URL param or Auth header, but not both. + // - Google only accepts URL param (not spec compliant?), not Auth header + // - Stripe only accepts client secret in Auth header with Bearer method, not Basic + // + // We used to maintain a big table in this code of all the sites and which way + // they went, but maintaining it didn't scale & got annoying. + // So just try both ways. + authStyle = AuthStyleInParams // the second way we'll try + req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle) + token, err = doTokenRoundTrip(ctx, req) + } + if needsAuthStyleProbe && err == nil { + setAuthStyle(tokenURL, authStyle) + } + // Don't overwrite `RefreshToken` with an empty value + // if this was a token refreshing request. + if token != nil && token.RefreshToken == "" { + token.RefreshToken = v.Get("refresh_token") + } + return token, err +} + +func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) { r, err := ctxhttp.Do(ctx, ContextClient(ctx), req) if err != nil { return nil, err } - defer r.Body.Close() body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) + r.Body.Close() if err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } @@ -223,12 +260,6 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, Raw: vals, } e := vals.Get("expires_in") - if e == "" { - // TODO(jbd): Facebook's OAuth2 implementation is broken and - // returns expires_in field in expires. Remove the fallback to expires, - // when Facebook fixes their implementation. - e = vals.Get("expires") - } expires, _ := strconv.Atoi(e) if expires != 0 { token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) @@ -247,13 +278,8 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, } json.Unmarshal(body, &token.Raw) // no error checks for optional fields } - // Don't overwrite `RefreshToken` with an empty value - // if this was a token refreshing request. - if token.RefreshToken == "" { - token.RefreshToken = v.Get("refresh_token") - } if token.AccessToken == "" { - return token, errors.New("oauth2: server response missing access_token") + return nil, errors.New("oauth2: server response missing access_token") } return token, nil } diff --git a/vendor/golang.org/x/oauth2/internal/transport.go b/vendor/golang.org/x/oauth2/internal/transport.go index d16f9ae1fea..572074a637d 100644 --- a/vendor/golang.org/x/oauth2/internal/transport.go +++ b/vendor/golang.org/x/oauth2/internal/transport.go @@ -5,9 +5,8 @@ package internal import ( + "context" "net/http" - - "golang.org/x/net/context" ) // HTTPClient is the context key to use with golang.org/x/net/context's diff --git a/vendor/golang.org/x/oauth2/jwt/jwt.go b/vendor/golang.org/x/oauth2/jwt/jwt.go index e08f3159590..99f3e0a32c8 100644 --- a/vendor/golang.org/x/oauth2/jwt/jwt.go +++ b/vendor/golang.org/x/oauth2/jwt/jwt.go @@ -9,6 +9,7 @@ package jwt import ( + "context" "encoding/json" "fmt" "io" @@ -18,7 +19,6 @@ import ( "strings" "time" - "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/internal" "golang.org/x/oauth2/jws" @@ -61,6 +61,11 @@ type Config struct { // Expires optionally specifies how long the token is valid for. Expires time.Duration + + // Audience optionally specifies the intended audience of the + // request. If empty, the value of TokenURL is used as the + // intended audience. + Audience string } // TokenSource returns a JWT TokenSource using the configuration @@ -105,6 +110,9 @@ func (js jwtSource) Token() (*oauth2.Token, error) { if t := js.conf.Expires; t > 0 { claimSet.Exp = time.Now().Add(t).Unix() } + if aud := js.conf.Audience; aud != "" { + claimSet.Aud = aud + } h := *defaultHeader h.KeyID = js.conf.PrivateKeyID payload, err := jws.Encode(&h, claimSet, pk) diff --git a/vendor/golang.org/x/oauth2/oauth2.go b/vendor/golang.org/x/oauth2/oauth2.go index a047a5f98b6..428283f0b01 100644 --- a/vendor/golang.org/x/oauth2/oauth2.go +++ b/vendor/golang.org/x/oauth2/oauth2.go @@ -3,19 +3,20 @@ // license that can be found in the LICENSE file. // Package oauth2 provides support for making -// OAuth2 authorized and authenticated HTTP requests. +// OAuth2 authorized and authenticated HTTP requests, +// as specified in RFC 6749. // It can additionally grant authorization with Bearer JWT. package oauth2 // import "golang.org/x/oauth2" import ( "bytes" + "context" "errors" "net/http" "net/url" "strings" "sync" - "golang.org/x/net/context" "golang.org/x/oauth2/internal" ) @@ -25,17 +26,13 @@ import ( // Deprecated: Use context.Background() or context.TODO() instead. var NoContext = context.TODO() -// RegisterBrokenAuthHeaderProvider registers an OAuth2 server -// identified by the tokenURL prefix as an OAuth2 implementation -// which doesn't support the HTTP Basic authentication -// scheme to authenticate with the authorization server. -// Once a server is registered, credentials (client_id and client_secret) -// will be passed as query parameters rather than being present -// in the Authorization header. -// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. -func RegisterBrokenAuthHeaderProvider(tokenURL string) { - internal.RegisterBrokenAuthHeaderProvider(tokenURL) -} +// RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op. +// +// Deprecated: this function no longer does anything. Caller code that +// wants to avoid potential extra HTTP requests made during +// auto-probing of the provider's auth style should set +// Endpoint.AuthStyle. +func RegisterBrokenAuthHeaderProvider(tokenURL string) {} // Config describes a typical 3-legged OAuth2 flow, with both the // client application information and the server's endpoint URLs. @@ -70,13 +67,38 @@ type TokenSource interface { Token() (*Token, error) } -// Endpoint contains the OAuth 2.0 provider's authorization and token +// Endpoint represents an OAuth 2.0 provider's authorization and token // endpoint URLs. type Endpoint struct { AuthURL string TokenURL string + + // AuthStyle optionally specifies how the endpoint wants the + // client ID & client secret sent. The zero value means to + // auto-detect. + AuthStyle AuthStyle } +// AuthStyle represents how requests for tokens are authenticated +// to the server. +type AuthStyle int + +const ( + // AuthStyleAutoDetect means to auto-detect which authentication + // style the provider wants by trying both ways and caching + // the successful way for the future. + AuthStyleAutoDetect AuthStyle = 0 + + // AuthStyleInParams sends the "client_id" and "client_secret" + // in the POST body as application/x-www-form-urlencoded parameters. + AuthStyleInParams AuthStyle = 1 + + // AuthStyleInHeader sends the client_id and client_password + // using HTTP Basic Authorization. This is an optional style + // described in the OAuth2 RFC 6749 section 2.3.1. + AuthStyleInHeader AuthStyle = 2 +) + var ( // AccessTypeOnline and AccessTypeOffline are options passed // to the Options.AuthCodeURL method. They modify the @@ -123,6 +145,8 @@ func SetAuthURLParam(key, value string) AuthCodeOption { // // Opts may include AccessTypeOnline or AccessTypeOffline, as well // as ApprovalForce. +// It can also be used to pass the PKCE challenge. +// See https://www.oauth.com/oauth2-servers/pkce/ for more info. func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { var buf bytes.Buffer buf.WriteString(c.Endpoint.AuthURL) @@ -161,8 +185,7 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { // and when other authorization grant types are not available." // See https://tools.ietf.org/html/rfc6749#section-4.3 for more info. // -// The HTTP client to use is derived from the context. -// If nil, http.DefaultClient is used. +// The provided context optionally controls which HTTP client is used. See the HTTPClient variable. func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) { v := url.Values{ "grant_type": {"password"}, @@ -180,12 +203,14 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor // It is used after a resource provider redirects the user back // to the Redirect URI (the URL obtained from AuthCodeURL). // -// The HTTP client to use is derived from the context. -// If a client is not provided via the context, http.DefaultClient is used. +// The provided context optionally controls which HTTP client is used. See the HTTPClient variable. // // The code will be in the *http.Request.FormValue("code"). Before // calling Exchange, be sure to validate FormValue("state"). -func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { +// +// Opts may include the PKCE verifier code if previously used in AuthCodeURL. +// See https://www.oauth.com/oauth2-servers/pkce/ for more info. +func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) { v := url.Values{ "grant_type": {"authorization_code"}, "code": {code}, @@ -193,6 +218,9 @@ func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { if c.RedirectURL != "" { v.Set("redirect_uri", c.RedirectURL) } + for _, opt := range opts { + opt.setValue(v) + } return retrieveToken(ctx, c, v) } diff --git a/vendor/golang.org/x/oauth2/token.go b/vendor/golang.org/x/oauth2/token.go index 34db8cdc8a3..822720341af 100644 --- a/vendor/golang.org/x/oauth2/token.go +++ b/vendor/golang.org/x/oauth2/token.go @@ -5,6 +5,7 @@ package oauth2 import ( + "context" "fmt" "net/http" "net/url" @@ -12,7 +13,6 @@ import ( "strings" "time" - "golang.org/x/net/context" "golang.org/x/oauth2/internal" ) @@ -118,13 +118,16 @@ func (t *Token) Extra(key string) interface{} { return v } +// timeNow is time.Now but pulled out as a variable for tests. +var timeNow = time.Now + // expired reports whether the token is expired. // t must be non-nil. func (t *Token) expired() bool { if t.Expiry.IsZero() { return false } - return t.Expiry.Round(0).Add(-expiryDelta).Before(time.Now()) + return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow()) } // Valid reports whether t is non-nil, has an AccessToken, and is not expired. @@ -151,7 +154,7 @@ func tokenFromInternal(t *internal.Token) *Token { // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along // with an error.. func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { - tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) + tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle)) if err != nil { if rErr, ok := err.(*internal.RetrieveError); ok { return nil, (*RetrieveError)(rErr) diff --git a/vendor/golang.org/x/oauth2/transport.go b/vendor/golang.org/x/oauth2/transport.go index 92ac7e2531f..aa0d34f1e0e 100644 --- a/vendor/golang.org/x/oauth2/transport.go +++ b/vendor/golang.org/x/oauth2/transport.go @@ -31,9 +31,17 @@ type Transport struct { } // RoundTrip authorizes and authenticates the request with an -// access token. If no token exists or token is expired, -// tries to refresh/fetch a new token. +// access token from Transport's Source. func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + reqBodyClosed := false + if req.Body != nil { + defer func() { + if !reqBodyClosed { + req.Body.Close() + } + }() + } + if t.Source == nil { return nil, errors.New("oauth2: Transport's Source is nil") } @@ -46,6 +54,10 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { token.SetAuthHeader(req2) t.setModReq(req, req2) res, err := t.base().RoundTrip(req2) + + // req.Body is assumed to have been closed by the base RoundTripper. + reqBodyClosed = true + if err != nil { t.setModReq(req, nil) return nil, err