Add Time Trigger API and client (#153) (#161)

This change adds a timer trigger API, client, and implementation.  Users can trigger a function with a cron-compatible string. 

This change also moves the publisher interface and webhook-based publisher implementation out of kubewatcher and into a separate `publisher` package.
This commit is contained in:
yang qf
2017-05-17 15:13:56 -07:00
committed by Soam Vasani
parent e5f5050ea6
commit a8c6347ed0
20 changed files with 892 additions and 45 deletions
+23 -5
View File
@@ -35,6 +35,8 @@ import (
"k8s.io/client-go/1.5/pkg/watch"
"github.com/fission/fission"
"github.com/fission/fission/publisher"
"reflect"
)
type requestType int
@@ -48,7 +50,8 @@ type (
watches map[string]watchSubscription
kubernetesClient *kubernetes.Clientset
requestChannel chan *kubeWatcherRequest
publisher Publisher
publisher publisher.Publisher
routerUrl string
}
watchSubscription struct {
@@ -57,7 +60,7 @@ type (
lastResourceVersion string
stopped *int32
kubernetesClient *kubernetes.Clientset
publisher Publisher
publisher publisher.Publisher
}
kubeWatcherRequest struct {
@@ -70,7 +73,7 @@ type (
}
)
func MakeKubeWatcher(kubernetesClient *kubernetes.Clientset, publisher Publisher) *KubeWatcher {
func MakeKubeWatcher(kubernetesClient *kubernetes.Clientset, publisher publisher.Publisher) *KubeWatcher {
kw := &KubeWatcher{
watches: make(map[string]watchSubscription),
kubernetesClient: kubernetesClient,
@@ -204,7 +207,7 @@ func (kw *KubeWatcher) removeWatch(w *fission.Watch) error {
// return nil
// }
func MakeWatchSubscription(w *fission.Watch, kubeClient *kubernetes.Clientset, publisher Publisher) (*watchSubscription, error) {
func MakeWatchSubscription(w *fission.Watch, kubeClient *kubernetes.Clientset, publisher publisher.Publisher) (*watchSubscription, error) {
var stopped int32 = 0
ws := &watchSubscription{
Watch: *w,
@@ -277,7 +280,22 @@ func (ws *watchSubscription) eventDispatchLoop() {
ws.lastResourceVersion = rv
}
ws.publisher.Publish(ev, ws.Watch.Target)
// Serialize the object
var buf bytes.Buffer
err = printKubernetesObject(ev.Object, &buf)
if err != nil {
log.Printf("Failed to serialize object: %v", err)
// TODO send a POST request indicating error
}
// Event and object type aren't in the serialized object
headers := map[string]string{
"Content-Type": "application/json",
"X-Kubernetes-Event-Type": string(ev.Type),
"X-Kubernetes-Object-Type": reflect.TypeOf(ev.Object).Elem().Name(),
}
// Event and object type aren't in the serialized object
ws.publisher.Publish(buf.String(), headers, ws.Watch.Target)
}
if atomic.LoadInt32(ws.stopped) == 0 {
err := ws.restartWatch()
+3 -3
View File
@@ -23,6 +23,7 @@ import (
"k8s.io/client-go/1.5/rest"
"github.com/fission/fission/controller/client"
"github.com/fission/fission/publisher"
)
// Get a kubernetes client using the pod's service account.
@@ -49,11 +50,10 @@ func Start(controllerUrl string, routerUrl string) error {
if err != nil {
return err
}
poster := MakeWebhookPublisher(routerUrl)
poster := publisher.MakeWebhookPublisher(routerUrl)
kubeWatch := MakeKubeWatcher(kubeClient, poster)
client := client.MakeClient(controllerUrl)
MakeWatchSync(client, kubeWatch)
MakeWatchSync(client.MakeClient(controllerUrl), kubeWatch)
return nil
}
-30
View File
@@ -1,30 +0,0 @@
/*
Copyright 2016 The Fission 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 kubewatcher
import (
"k8s.io/client-go/1.5/pkg/watch"
)
type (
Publisher interface {
// Publish an event to a "target". Target's meaning depends on the
// publisher: it's a URL in the case of a webhook publisher, or a queue
// name in a queue-based publisher such as NATS.
Publish(event watch.Event, target string)
}
)
-135
View File
@@ -1,135 +0,0 @@
/*
Copyright 2016 The Fission 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 kubewatcher
import (
"bytes"
"io/ioutil"
"log"
"net/http"
"reflect"
"strings"
"time"
"k8s.io/client-go/1.5/pkg/watch"
)
type (
// A webhook publisher for a single URL. Satisifies the Publisher interface.
WebhookPublisher struct {
requestChannel chan *publishRequest
maxRetries int
retryDelay time.Duration
baseUrl string
}
publishRequest struct {
url string
watchEvent watch.Event
retries int
retryDelay time.Duration
}
)
func MakeWebhookPublisher(baseUrl string) *WebhookPublisher {
p := &WebhookPublisher{
baseUrl: baseUrl,
requestChannel: make(chan *publishRequest, 32), // buffered channel
// TODO make this configurable
maxRetries: 10,
retryDelay: 500 * time.Millisecond,
}
go p.svc()
return p
}
func (p *WebhookPublisher) Publish(watchEvent watch.Event, url string) {
p.requestChannel <- &publishRequest{
watchEvent: watchEvent,
url: url,
retries: p.maxRetries,
retryDelay: p.retryDelay,
}
}
func (p *WebhookPublisher) svc() {
for {
r := <-p.requestChannel
p.makeHttpRequest(r)
}
}
func (p *WebhookPublisher) makeHttpRequest(r *publishRequest) {
url := p.baseUrl + "/" + strings.TrimPrefix(r.url, "/")
log.Printf("Making HTTP request to %v", url)
// Serialize the object
var buf bytes.Buffer
err := printKubernetesObject(r.watchEvent.Object, &buf)
if err != nil {
log.Printf("Failed to serialize object: %v", err)
// TODO send a POST request indicating error
}
// Create request
req, err := http.NewRequest("POST", url, &buf)
if err != nil {
log.Printf("Failed to create request to %v", url)
// can't do anything more, drop the event.
return
}
// Event and object type aren't in the serialized object
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Kubernetes-Event-Type", string(r.watchEvent.Type))
req.Header.Add("X-Kubernetes-Object-Type", reflect.TypeOf(r.watchEvent.Object).Elem().Name())
// Make the request
resp, err := http.DefaultClient.Do(req)
// All done if the request succeeded with 200 OK.
if err == nil && resp.StatusCode == 200 {
resp.Body.Close()
return
}
// Log errors
if err != nil {
log.Printf("Request failed: %v", r)
} else if resp.StatusCode != 200 {
log.Printf("Request returned failure: %v", resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err == nil {
log.Printf("request error: %v", string(body))
}
}
// Schedule a retry, or give up if out of retries
r.retries--
if r.retries > 0 {
r.retryDelay *= time.Duration(2)
time.AfterFunc(r.retryDelay, func() {
p.requestChannel <- r
})
} else {
log.Printf("Final retry failed, giving up on %v", url)
// Event dropped
}
}