github.imxd.top/operator-framework/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 }