github.com/lablabs/operator-sdk@v0.8.2/pkg/ansible/runner/eventapi/eventapi.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package eventapi
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"io/ioutil"
    22  	"net"
    23  	"net/http"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"github.com/operator-framework/operator-sdk/internal/util/fileutil"
    29  
    30  	"github.com/go-logr/logr"
    31  	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
    32  )
    33  
    34  // EventReceiver serves the event API
    35  type EventReceiver struct {
    36  	// Events is the channel used by the event API handler to send JobEvents
    37  	// back to the runner, or whatever code is using this receiver.
    38  	Events chan JobEvent
    39  
    40  	// SocketPath is the path on the filesystem to a unix streaming socket
    41  	SocketPath string
    42  
    43  	// URLPath is the path portion of the url at which events should be
    44  	// received. For example, "/events/"
    45  	URLPath string
    46  
    47  	// server is the http.Server instance that serves the event API. It must be
    48  	// closed.
    49  	server io.Closer
    50  
    51  	// stopped indicates if this receiver has permanently stopped receiving
    52  	// events. When true, requests to POST an event will receive a "410 Gone"
    53  	// response, and the body will be ignored.
    54  	stopped bool
    55  
    56  	// mutex controls access to the "stopped" bool above, ensuring that writes
    57  	// are goroutine-safe.
    58  	mutex sync.RWMutex
    59  
    60  	// ident is the unique identifier for a particular run of ansible-runner
    61  	ident string
    62  
    63  	// logger holds a logger that has some fields already set
    64  	logger logr.Logger
    65  }
    66  
    67  func New(ident string, errChan chan<- error) (*EventReceiver, error) {
    68  	sockPath := fmt.Sprintf("/tmp/ansibleoperator-%s", ident)
    69  	listener, err := net.Listen("unix", sockPath)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	rec := EventReceiver{
    75  		Events:     make(chan JobEvent, 1000),
    76  		SocketPath: sockPath,
    77  		URLPath:    "/events/",
    78  		ident:      ident,
    79  		logger:     logf.Log.WithName("eventapi").WithValues("job", ident),
    80  	}
    81  
    82  	mux := http.NewServeMux()
    83  	mux.HandleFunc(rec.URLPath, rec.handleEvents)
    84  	srv := http.Server{Handler: mux}
    85  	rec.server = &srv
    86  
    87  	go func() {
    88  		errChan <- srv.Serve(listener)
    89  	}()
    90  	return &rec, nil
    91  }
    92  
    93  // Close ensures that appropriate resources are cleaned up, such as any unix
    94  // streaming socket that may be in use. Close must be called.
    95  func (e *EventReceiver) Close() {
    96  	e.mutex.Lock()
    97  	e.stopped = true
    98  	e.mutex.Unlock()
    99  	e.logger.V(1).Info("Event API stopped")
   100  	if err := e.server.Close(); err != nil && !fileutil.IsClosedError(err) {
   101  		e.logger.Error(err, "Failed to close event receiver")
   102  	}
   103  	close(e.Events)
   104  }
   105  
   106  func (e *EventReceiver) handleEvents(w http.ResponseWriter, r *http.Request) {
   107  	if r.URL.Path != e.URLPath {
   108  		http.NotFound(w, r)
   109  		e.logger.Info("Path not found", "code", "404", "Request.Path", r.URL.Path)
   110  		return
   111  	}
   112  
   113  	if r.Method != http.MethodPost {
   114  		e.logger.Info("Method not allowed", "code", "405", "Request.Method", r.Method)
   115  		w.WriteHeader(http.StatusMethodNotAllowed)
   116  		return
   117  	}
   118  
   119  	ct := r.Header.Get("content-type")
   120  	if strings.Split(ct, ";")[0] != "application/json" {
   121  		e.logger.Info("Wrong content type", "code", "415", "Request.Content-Type", ct)
   122  		w.WriteHeader(http.StatusUnsupportedMediaType)
   123  		if _, err := w.Write([]byte("The content-type must be \"application/json\"")); err != nil {
   124  			e.logger.Error(err, "Failed to write response body")
   125  		}
   126  		return
   127  	}
   128  
   129  	body, err := ioutil.ReadAll(r.Body)
   130  	if err != nil {
   131  		e.logger.Error(err, "Could not read request body", "code", "500")
   132  		w.WriteHeader(http.StatusInternalServerError)
   133  		return
   134  	}
   135  
   136  	event := JobEvent{}
   137  	err = json.Unmarshal(body, &event)
   138  	if err != nil {
   139  		e.logger.Info("Could not deserialize body.", "code", "400", "Error", err)
   140  		w.WriteHeader(http.StatusBadRequest)
   141  		if _, err := w.Write([]byte("Could not deserialize body as JSON")); err != nil {
   142  			e.logger.Error(err, "Failed to write response body")
   143  		}
   144  		return
   145  	}
   146  
   147  	// Guarantee that the Events channel will not be written to if stopped ==
   148  	// true, because in that case the channel has been closed.
   149  	e.mutex.RLock()
   150  	defer e.mutex.RUnlock()
   151  	if e.stopped {
   152  		e.mutex.RUnlock()
   153  		w.WriteHeader(http.StatusGone)
   154  		e.logger.Info("Stopped and not accepting additional events for this job", "code", "410")
   155  		return
   156  	}
   157  	// ansible-runner sends "status events" and "ansible events". The "status
   158  	// events" signify a change in the state of ansible-runner itself, which
   159  	// we're not currently interested in.
   160  	// https://ansible-runner.readthedocs.io/en/latest/external_interface.html#event-structure
   161  	if event.UUID == "" {
   162  		e.logger.V(1).Info("Dropping event that is not a JobEvent")
   163  	} else {
   164  		// timeout if the channel blocks for too long
   165  		timeout := time.NewTimer(10 * time.Second)
   166  		select {
   167  		case e.Events <- event:
   168  		case <-timeout.C:
   169  			e.logger.Info("Timed out writing event to channel", "code", "500")
   170  			w.WriteHeader(http.StatusInternalServerError)
   171  			return
   172  		}
   173  		_ = timeout.Stop()
   174  	}
   175  	w.WriteHeader(http.StatusNoContent)
   176  }