Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -656,3 +656,5 @@ tool (
sigs.k8s.io/controller-tools/cmd/controller-gen
sigs.k8s.io/kind
)

replace istio.io/istio => github.com/howardjohn/istio v0.0.0-20251208184906-ad002a472915
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/howardjohn/istio v0.0.0-20251208184906-ad002a472915 h1:rvuuLqu0TxdSFJ/nr1WqexIeazmWQQ1hgFg8aF/nlGM=
github.com/howardjohn/istio v0.0.0-20251208184906-ad002a472915/go.mod h1:DMD7pP2Tq+xb8ec/bP1Juiaec2c0nHMXYN9rStiufhU=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
Expand Down Expand Up @@ -2242,8 +2244,6 @@ istio.io/api v1.28.0-alpha.0.0.20251126150010-62ed4ff08e1b h1:04Yd01+oJWSxIMLDdc
istio.io/api v1.28.0-alpha.0.0.20251126150010-62ed4ff08e1b/go.mod h1:BD3qv/ekm16kvSgvSpuiDawgKhEwG97wx849CednJSg=
istio.io/client-go v1.28.0-alpha.0.0.20251126150310-56900da3b60f h1:4bgNcoNI2tMitzWd86J0rbwR4PlKdOzLTFZj1NdTU9s=
istio.io/client-go v1.28.0-alpha.0.0.20251126150310-56900da3b60f/go.mod h1:DUnHjxAs5VvvE/4UCSFr4e+O59Y6FsseMFpQnxxEDuw=
istio.io/istio v0.0.0-20251201142120-783e855f1e67 h1:u3y0jUklzTp1nuqPQaPxyKdfP8UAoEiJ44fDTMia8LM=
istio.io/istio v0.0.0-20251201142120-783e855f1e67/go.mod h1:DMD7pP2Tq+xb8ec/bP1Juiaec2c0nHMXYN9rStiufhU=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
Expand Down
6 changes: 3 additions & 3 deletions pkg/utils/kubeutils/kubectl/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (c *Cli) Namespaces(ctx context.Context) ([]string, error) {

// Apply applies the resources defined in the bytes, and returns an error if one occurred
func (c *Cli) Apply(ctx context.Context, content []byte, extraArgs ...string) error {
args := append([]string{"apply", "-f", "-"}, extraArgs...)
args := append([]string{"apply", "-f", "-", "--server-side", "--field-manager=istio-ci", "--force-conflicts"}, extraArgs...)
return c.Command(ctx, args...).
WithStdin(bytes.NewBuffer(content)).
Run().
Expand All @@ -134,7 +134,7 @@ func (c *Cli) ApplyFilePath(ctx context.Context, filePath string, extraArgs ...s
return err
}

args := append([]string{"apply", "-f", filePath}, extraArgs...)
args := append([]string{"apply", "-f", filePath, "--server-side", "--field-manager=istio-ci", "--force-conflicts"}, extraArgs...)
return c.Command(ctx, args...).
Run().
Cause()
Expand All @@ -143,7 +143,7 @@ func (c *Cli) ApplyFilePath(ctx context.Context, filePath string, extraArgs ...s
// ApplyFileWithOutput applies the resources defined in a file,
// if an error occurred, it will be returned along with the output of the command
func (c *Cli) ApplyFileWithOutput(ctx context.Context, fileName string, extraArgs ...string) (string, error) {
applyArgs := append([]string{"apply", "-f", fileName}, extraArgs...)
applyArgs := append([]string{"apply", "-f", fileName, "--server-side", "--field-manager=istio-ci", "--force-conflicts"}, extraArgs...)

fileInput, err := os.Open(fileName)
if err != nil {
Expand Down
257 changes: 257 additions & 0 deletions pkg/utils/requestutils/curl/native_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package curl

import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
)

// ExecuteRequest accepts a set of Option and executes a native Go HTTP request
// If multiple Option modify the same parameter, the last defined one will win
//
// Example:
//
// resp, err := ExecuteRequest(WithMethod("GET"), WithMethod("POST"))
// will executeNative a POST request
//
// A notable exception is the WithHeader option, which accumulates headers
func ExecuteRequest(options ...Option) (*http.Response, error) {
config := &requestConfig{
verbose: false,
ignoreServerCert: false,
connectionTimeout: 0,
headersOnly: false,
method: "GET",
host: "127.0.0.1",
port: 80,
headers: make(map[string][]string),
scheme: "http",
sni: "",
caFile: "",
path: "",
retry: 0,
retryDelay: -1,
retryMaxTime: 0,
ipv4Only: false,
ipv6Only: false,
cookie: "",
queryParameters: make(map[string]string),
}

for _, opt := range options {
opt(config)
}

return config.executeNative()
}

func (c *requestConfig) executeNative() (*http.Response, error) {
// Build URL
fullURL := c.buildURL()

// Create HTTP client with custom transport
client := c.buildHTTPClient()

// Prepare request body
var bodyReader io.Reader
if c.body != "" {
bodyReader = bytes.NewBufferString(c.body)
}

// Create context with timeout
ctx := context.Background()
if c.connectionTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(c.connectionTimeout)*time.Second)
defer cancel()
}

// Create request
req, err := http.NewRequestWithContext(ctx, c.method, fullURL, bodyReader)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

// Add headers
for key, values := range c.headers {
for _, value := range values {
// Host header must be set on req.Host, not in req.Header
if strings.EqualFold(key, "Host") {
req.Host = value
} else {
req.Header.Add(key, value)
}
}
}

// Add cookies
if c.cookie != "" {
req.Header.Add("Cookie", c.cookie)
}

// Handle HEAD-only requests
if c.headersOnly {
req.Method = "HEAD"
}

// Execute request
if c.verbose {
fmt.Printf("> %s %s\n", req.Method, req.URL.String())
fmt.Printf("> Host: %s\n", req.Host)
for k, v := range req.Header {
fmt.Printf("> %s: %s\n", k, strings.Join(v, ", "))
}
}

resp, err := client.Do(req)
if err != nil {
if c.verbose {
fmt.Printf("Request failed: %v\n", err)
}
return nil, err
}

if c.verbose {
fmt.Printf("< HTTP %s\n", resp.Status)
for k, v := range resp.Header {
fmt.Printf("< %s: %s\n", k, strings.Join(v, ", "))
}
}

return resp, nil
}

func (c *requestConfig) buildURL() string {
path := c.path
if path != "" && !strings.HasPrefix(path, "/") {
path = "/" + path
}

baseURL := fmt.Sprintf("%s://%s:%d%s", c.scheme, c.host, c.port, path)

if len(c.queryParameters) > 0 {
values := url.Values{}
for k, v := range c.queryParameters {
values.Add(k, v)
}
return fmt.Sprintf("%s?%s", baseURL, values.Encode())
}

return baseURL
}

func (c *requestConfig) buildHTTPClient() *http.Client {
transport := &http.Transport{
DialContext: c.buildDialer(),
}

// Configure TLS
if c.scheme == "https" || c.ignoreServerCert || c.sni != "" {
tlsConfig := &tls.Config{
InsecureSkipVerify: c.ignoreServerCert,
}

if c.sni != "" {
tlsConfig.ServerName = c.sni
}

// Configure TLS version
if c.tlsVersion != "" {
tlsConfig.MinVersion = parseTLSVersion(c.tlsVersion)
}
if c.tlsMaxVersion != "" {
tlsConfig.MaxVersion = parseTLSVersion(c.tlsMaxVersion)
}

// Configure cipher suites (simplified)
if c.ciphers != "" {
// Note: Go's TLS implementation uses predefined cipher suites
// This would require parsing the cipher string and mapping to Go's constants
// For simplicity, this is left as a placeholder
}

// Configure curves (simplified)
if c.curves != "" {
// Similar to ciphers, this would require parsing and mapping
}

transport.TLSClientConfig = tlsConfig
}

// Configure HTTP version
if c.http2 {
// HTTP/2 is enabled by default in Go's transport
transport.ForceAttemptHTTP2 = true
} else if c.http11 {
// Disable HTTP/2 to force HTTP/1.1
transport.ForceAttemptHTTP2 = false
transport.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
}

client := &http.Client{
Transport: transport,
}

// Set timeout (client-level timeout)
if c.connectionTimeout > 0 {
client.Timeout = time.Duration(c.connectionTimeout) * time.Second
}

client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// Disable redirects
return http.ErrUseLastResponse
}
return client
}

func (c *requestConfig) buildDialer() func(context.Context, string, string) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
}

if c.connectionTimeout > 0 {
dialer.Timeout = time.Duration(c.connectionTimeout) * time.Second
}

// Handle IPv4/IPv6 restrictions
if c.ipv4Only {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp4", addr)
}
}
if c.ipv6Only {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp6", addr)
}
}

// Handle SNI with custom host resolution
// TODO
if c.sni != "" {
panic("sni is not implemented")
}
Comment on lines +235 to +239
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SNI support is not implemented and will panic at runtime if used. Either implement this feature or remove SNI as an option until it can be properly supported.

Copilot uses AI. Check for mistakes.

return dialer.DialContext
}

func parseTLSVersion(version string) uint16 {
switch version {
case "1.0":
return tls.VersionTLS10
case "1.1":
return tls.VersionTLS11
case "1.2":
return tls.VersionTLS12
case "1.3":
return tls.VersionTLS13
default:
return tls.VersionTLS12 // default
}
}
70 changes: 70 additions & 0 deletions test/e2e/common/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package common

import (
"context"
"fmt"
"net/http"
"testing"

"istio.io/istio/pkg/log"
"istio.io/istio/pkg/test/util/assert"
"istio.io/istio/pkg/test/util/retry"
"k8s.io/apimachinery/pkg/types"

"github.com/kgateway-dev/kgateway/v2/pkg/utils/requestutils/curl"
"github.com/kgateway-dev/kgateway/v2/test/e2e"
"github.com/kgateway-dev/kgateway/v2/test/gomega/matchers"
testmatchers "github.com/kgateway-dev/kgateway/v2/test/gomega/matchers"
)

func SetupBaseConfig(ctx context.Context, t *testing.T, installation *e2e.TestInstallation, manifests ...string) {
for _, s := range log.Scopes() {
s.SetOutputLevel(log.DebugLevel)
}
err := installation.ClusterContext.IstioClient.ApplyYAMLFiles("", manifests...)
assert.NoError(t, err)
//for _, manifest := range manifests {
//err := installation.Actions.Kubectl().ApplyFile(ctx, manifest)
//}
}

func SetupBaseGateway(ctx context.Context, installation *e2e.TestInstallation, name types.NamespacedName) {
address := installation.Assertions.EventuallyGatewayAddress(
ctx,
name.Name,
name.Namespace,
)
BaseGateway = Gateway{
NamespacedName: name,
Address: address,
}
}

type Gateway struct {
types.NamespacedName
Address string
}

var BaseGateway Gateway

func (g *Gateway) Send(t *testing.T, match *testmatchers.HttpResponse, opts ...curl.Option) *http.Response {
fullOpts := append([]curl.Option{curl.WithHost(g.Address)}, opts...)
var resp *http.Response
retry.UntilSuccessOrFail(t, func() error {
r, err := curl.ExecuteRequest(fullOpts...)

Check failure on line 54 in test/e2e/common/base.go

View workflow job for this annotation

GitHub Actions / Go

response body must be closed (bodyclose)
if err != nil {
return err
}
resp = r
mm := matchers.HaveHttpResponse(match)
success, err := mm.Match(resp)
if err != nil {
return err
}
if !success {
return fmt.Errorf("match failed: %v", mm.FailureMessage(resp))
}
return nil
})
return resp
}
Loading
Loading