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  }