github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/integration_test/testdata/apiserveraccess/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net" 8 "net/http" 9 "net/url" 10 "os" 11 "os/signal" 12 "strconv" 13 "strings" 14 15 "golang.org/x/sys/unix" 16 17 "github.com/datawire/dlib/dhttp" 18 "github.com/datawire/dlib/dlog" 19 "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" 20 "github.com/telepresenceio/telepresence/v2/pkg/log" 21 "github.com/telepresenceio/telepresence/v2/pkg/matcher" 22 "github.com/telepresenceio/telepresence/v2/pkg/restapi" 23 ) 24 25 // This service is meant for testing the cluster side Telepresence API service. 26 // 27 // Publish image to cluster: 28 // 29 // ko publish -B ./integration_test/testdata/apiserveraccess [--insecure-registry] 30 // 31 // Deploy it: 32 // 33 // kubectl apply -f ./k8s/apitest.yaml 34 // 35 // Run it locally using an intercept with -- so that TELEPRESENCE_INTERCEPT_ID is propagated in the env 36 func main() { 37 c, cancel := context.WithCancel(log.MakeBaseLogger(context.Background(), "DEBUG")) 38 sigs := make(chan os.Signal, 1) 39 signal.Notify(sigs, os.Interrupt, unix.SIGTERM) 40 defer func() { 41 signal.Stop(sigs) 42 cancel() 43 }() 44 45 go func() { 46 select { 47 case <-sigs: 48 cancel() 49 case <-c.Done(): 50 } 51 <-sigs 52 os.Exit(1) 53 }() 54 55 if err := run(c); err != nil { 56 fmt.Fprintln(os.Stderr, err) 57 os.Exit(1) 58 } 59 } 60 61 func run(c context.Context) error { 62 if lv, ok := os.LookupEnv("LOG_LEVEL"); ok { 63 log.SetLevel(c, lv) 64 } 65 66 ap, ok := os.LookupEnv("APP_PORT") 67 if !ok { 68 ap = "8080" 69 } 70 _, err := strconv.ParseUint(ap, 10, 16) 71 if err != nil { 72 return fmt.Errorf("the value %q of env APP_PORT is not a valid port number", ap) 73 } 74 75 ln, err := net.Listen("tcp", "localhost:"+ap) 76 if err != nil { 77 return err 78 } 79 80 mux := http.NewServeMux() 81 mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 82 w.WriteHeader(http.StatusOK) 83 }) 84 85 if apiUrl, err := apiURL(); err == nil { 86 consumeHereURL := apiUrl + "/consume-here" 87 interceptInfoURL := apiUrl + "/intercept-info" 88 mux.HandleFunc("/consume-here", func(w http.ResponseWriter, r *http.Request) { 89 var b bool 90 intercepted(c, consumeHereURL, r.FormValue("path"), w, r, &b) 91 }) 92 mux.HandleFunc("/intercept-info", func(w http.ResponseWriter, r *http.Request) { 93 ii := restapi.InterceptInfo{} 94 intercepted(c, interceptInfoURL, r.FormValue("path"), w, r, &ii) 95 }) 96 } else { 97 mux.HandleFunc("/consume-here", func(w http.ResponseWriter, r *http.Request) { 98 w.Header().Set("Content-Type", "application/json") 99 _ = json.NewEncoder(w).Encode(false) 100 }) 101 mux.HandleFunc("/intercept-info", func(w http.ResponseWriter, r *http.Request) { 102 ii := restapi.InterceptInfo{} 103 _ = json.NewEncoder(w).Encode(&ii) 104 }) 105 } 106 107 server := &dhttp.ServerConfig{Handler: mux} 108 info := fmt.Sprintf("API test server on %v", ln.Addr()) 109 dlog.Infof(c, "%s started", info) 110 defer dlog.Infof(c, "%s ended", info) 111 if err := server.Serve(c, ln); err != nil && err != c.Err() { 112 return fmt.Errorf("%s stopped: %w", info, err) 113 } 114 return nil 115 } 116 117 const interceptIdEnv = "TELEPRESENCE_INTERCEPT_ID" 118 119 // apiURL creates the generic URL needed to access the service 120 func apiURL() (string, error) { 121 pe := os.Getenv(agentconfig.EnvAPIPort) 122 if _, err := strconv.ParseUint(pe, 10, 16); err != nil { 123 return "", fmt.Errorf("value %q of env %s does not represent a valid port number", pe, agentconfig.EnvAPIPort) 124 } 125 return "http://localhost:" + pe, nil 126 } 127 128 // doRequest calls the consume-here endpoint with the given headers and returns the result 129 func doRequest(c context.Context, rqUrl string, path string, hm map[string]string, objTemplate any, er *restapi.ErrorResponse) (int, error) { 130 rq, err := http.NewRequest("GET", rqUrl+"?path="+url.QueryEscape(path), nil) 131 if err != nil { 132 return 0, err 133 } 134 rq.Header = make(http.Header, len(hm)+1) 135 rq.Header.Set("X-Telepresence-Caller-Intercept-Id", os.Getenv(interceptIdEnv)) 136 for k, v := range hm { 137 rq.Header.Set(k, v) 138 } 139 dlog.Debugf(c, "%s with headers\n%s", rqUrl, matcher.HeaderStringer(rq.Header)) 140 rs, err := http.DefaultClient.Do(rq) 141 if err != nil { 142 return 0, err 143 } 144 defer rs.Body.Close() 145 146 ec := json.NewDecoder(rs.Body) 147 if rs.StatusCode == http.StatusOK { 148 err = ec.Decode(objTemplate) 149 } else { 150 // Make an attempt to decode a json error. 151 _ = ec.Decode(er) 152 } 153 return rs.StatusCode, err 154 } 155 156 func intercepted(c context.Context, url string, path string, w http.ResponseWriter, r *http.Request, objTemplate any) { 157 hm := make(map[string]string, len(r.Header)) 158 159 // The "X-With-" prefix is used as a backdoor to avoid triggering intercepts during test. It's 160 // stripped off here. 161 for h := range r.Header { 162 hm[strings.TrimPrefix(h, "X-With-")] = r.Header.Get(h) 163 } 164 165 // The "X-Without-" prefix is used when the headers that trigger an intercept must be included in 166 // order for the intercept to take place, but should be removed in the subsequent query. Both 167 // the "X-Without-" headers and the header they refer to are stripped off here. 168 for h := range hm { 169 if hw := strings.TrimPrefix(h, "X-Without-"); h != hw { 170 delete(hm, h) 171 delete(hm, hw) 172 } 173 } 174 w.Header().Set("Content-Type", "application/json") 175 176 er := restapi.ErrorResponse{} 177 if status, err := doRequest(c, url, path, hm, objTemplate, &er); err != nil { 178 w.WriteHeader(http.StatusInternalServerError) 179 fmt.Fprintf(w, "failed to execute http request: %v", err) 180 } else { 181 w.WriteHeader(status) 182 if status == http.StatusOK { 183 err = json.NewEncoder(w).Encode(objTemplate) 184 } else if er.Error != "" { 185 err = json.NewEncoder(w).Encode(er) 186 } 187 if err != nil { 188 dlog.Error(c, err) 189 } 190 } 191 }