Repository: envoyproxy/gateway

Issue:
Implement fuzzer for Gateway API to XDS


Hints from issue comments:

Solution patch:
diff --git a/internal/cmd/egctl/translate.go b/internal/cmd/egctl/translate.go
index 1500c1ac1..843a24c59 100644
--- a/internal/cmd/egctl/translate.go
+++ b/internal/cmd/egctl/translate.go
@@ -239,14 +239,14 @@ func translate(w io.Writer, inFile, inType string, outTypes []string, output, re
 				}
 			}
 			if outType == xdsType {
-				res, err := translateGatewayAPIToXds(namespace, dnsDomain, resourceType, resources)
+				res, err := TranslateGatewayAPIToXds(namespace, dnsDomain, resourceType, resources)
 				if err != nil {
 					return err
 				}
 				result.Xds = res
 			}
 			if outType == irType {
-				res, err := TranslateGatewayAPIToIR(resources)
+				res, err := translateGatewayAPIToIR(resources)
 				if err != nil {
 					return err
 				}
@@ -265,7 +265,7 @@ func translate(w io.Writer, inFile, inType string, outTypes []string, output, re
 	return fmt.Errorf("unable to find translate from input type %s to output type %s", inType, outTypes)
 }
 
-func TranslateGatewayAPIToIR(resources *resource.Resources) (*gatewayapi.TranslateResult, error) {
+func translateGatewayAPIToIR(resources *resource.Resources) (*gatewayapi.TranslateResult, error) {
 	if resources.GatewayClass == nil {
 		return nil, fmt.Errorf("the GatewayClass resource is required")
 	}
@@ -331,7 +331,7 @@ func translateGatewayAPIToGatewayAPI(resources *resource.Resources) (resource.Re
 	return gRes.Resources, nil
 }
 
-func translateGatewayAPIToXds(namespace, dnsDomain string, resourceType string, resources *resource.Resources) (map[string]any, error) {
+func TranslateGatewayAPIToXds(namespace, dnsDomain, resourceType string, resources *resource.Resources) (map[string]any, error) {
 	if resources.GatewayClass == nil {
 		return nil, fmt.Errorf("the GatewayClass resource is required")
 	}
diff --git a/internal/cmd/server.go b/internal/cmd/server.go
index 528170fb8..1ef3d41d8 100644
--- a/internal/cmd/server.go
+++ b/internal/cmd/server.go
@@ -7,6 +7,7 @@ package cmd
 
 import (
 	"context"
+	"io"
 
 	"github.com/spf13/cobra"
 
@@ -37,7 +38,7 @@ func GetServerCommand() *cobra.Command {
 		Aliases: []string{"serve"},
 		Short:   "Serve Envoy Gateway",
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return server(cmd.Context())
+			return server(cmd.Context(), cmd.OutOrStdout())
 		},
 	}
 	cmd.PersistentFlags().StringVarP(&cfgPath, "config-path", "c", "",
@@ -47,8 +48,8 @@ func GetServerCommand() *cobra.Command {
 }
 
 // server serves Envoy Gateway.
-func server(ctx context.Context) error {
-	cfg, err := getConfig()
+func server(ctx context.Context, logOut io.Writer) error {
+	cfg, err := getConfig(logOut)
 	if err != nil {
 		return err
 	}
@@ -84,12 +85,12 @@ func server(ctx context.Context) error {
 }
 
 // getConfig gets the Server configuration
-func getConfig() (*config.Server, error) {
-	return getConfigByPath(cfgPath)
+func getConfig(logOut io.Writer) (*config.Server, error) {
+	return getConfigByPath(logOut, cfgPath)
 }
 
 // make `cfgPath` an argument to test it without polluting the global var
-func getConfigByPath(cfgPath string) (*config.Server, error) {
+func getConfigByPath(logOut io.Writer, cfgPath string) (*config.Server, error) {
 	// Initialize with default config parameters.
 	cfg, err := config.New()
 	if err != nil {
@@ -114,7 +115,7 @@ func getConfigByPath(cfgPath string) (*config.Server, error) {
 		cfg.EnvoyGateway = eg
 		// update cfg logger
 		eg.Logging.SetEnvoyGatewayLoggingDefaults()
-		cfg.Logger = logging.NewLogger(eg.Logging)
+		cfg.Logger = logging.NewLogger(logOut, eg.Logging)
 	}
 
 	if err := cfg.Validate(); err != nil {
diff --git a/internal/envoygateway/config/loader/configloader.go b/internal/envoygateway/config/loader/configloader.go
index 9523c7a43..5c381127a 100644
--- a/internal/envoygateway/config/loader/configloader.go
+++ b/internal/envoygateway/config/loader/configloader.go
@@ -7,6 +7,7 @@ package loader
 
 import (
 	"context"
+	"os"
 	"time"
 
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
@@ -71,7 +72,7 @@ func (r *Loader) Start(ctx context.Context) error {
 				r.cfg.EnvoyGateway = eg
 				// update cfg logger
 				eg.Logging.SetEnvoyGatewayLoggingDefaults()
-				r.cfg.Logger = logging.NewLogger(eg.Logging)
+				r.cfg.Logger = logging.NewLogger(os.Stdout, eg.Logging)
 
 				// cancel last
 				if r.cancel != nil {
diff --git a/internal/gatewayapi/resource/load.go b/internal/gatewayapi/resource/load.go
index b3d1a523a..23f64c96a 100644
--- a/internal/gatewayapi/resource/load.go
+++ b/internal/gatewayapi/resource/load.go
@@ -46,9 +46,7 @@ func LoadResourcesFromYAMLBytes(yamlBytes []byte, addMissingResources bool) (*Re
 
 // loadKubernetesYAMLToResources converts a Kubernetes YAML string into GatewayAPI Resources.
 // TODO: add support for kind:
-//   - EnvoyExtensionPolicy (gateway.envoyproxy.io/v1alpha1)
 //   - BackendLPPolicy (gateway.networking.k8s.io/v1alpha2)
-//   - BackendTLSPolicy (gateway.networking.k8s.io/v1alpha3)
 //   - ReferenceGrant (gateway.networking.k8s.io/v1alpha2)
 func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Resources, error) {
 	resources := NewResources()
@@ -355,6 +353,19 @@ func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Res
 				Spec: typedSpec.(gwapiv1a3.BackendTLSPolicySpec),
 			}
 			resources.BackendTLSPolicies = append(resources.BackendTLSPolicies, backendTLSPolicy)
+		case KindEnvoyExtensionPolicy:
+			typedSpec := spec.Interface()
+			envoyExtensionPolicy := &egv1a1.EnvoyExtensionPolicy{
+				TypeMeta: metav1.TypeMeta{
+					Kind: KindEnvoyExtensionPolicy,
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      name,
+					Namespace: namespace,
+				},
+				Spec: typedSpec.(egv1a1.EnvoyExtensionPolicySpec),
+			}
+			resources.EnvoyExtensionPolicies = append(resources.EnvoyExtensionPolicies, envoyExtensionPolicy)
 		}
 
 		return nil
diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go
index 9c2bf9c5d..f182f10ed 100644
--- a/internal/gatewayapi/runner/runner.go
+++ b/internal/gatewayapi/runner/runner.go
@@ -11,7 +11,9 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
+	"path"
 	"reflect"
+	"runtime"
 
 	"github.com/docker/docker/pkg/fileutils"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -36,8 +38,6 @@ import (
 )
 
 const (
-	wasmCacheDir = "/var/lib/eg/wasm"
-
 	// Default certificates path for envoy-gateway with Kubernetes provider.
 	serveTLSCertFilepath = "/certs/tls.crt"
 	serveTLSKeyFilepath  = "/certs/tls.key"
@@ -96,9 +96,17 @@ func (r *Runner) startWasmCache(ctx context.Context) {
 		r.Logger.Error(err, "failed to start wasm cache")
 		return
 	}
-
+	cacheOption := wasm.CacheOptions{}
+	switch runtime.GOOS {
+	case "darwin":
+		// On darwin, /var/lib/eg/wasm is not writable by non-root users.
+		h, _ := os.UserHomeDir() // Assume we always get the home directory.
+		cacheOption.CacheDir = path.Join(h, ".eg", "wasm")
+	default:
+		cacheOption.CacheDir = "/var/lib/eg/wasm"
+	}
 	// Create the file directory if it does not exist.
-	if err = fileutils.CreateIfNotExists(wasmCacheDir, true); err != nil {
+	if err = fileutils.CreateIfNotExists(cacheOption.CacheDir, true); err != nil {
 		r.Logger.Error(err, "Failed to create Wasm cache directory")
 		return
 	}
@@ -108,10 +116,7 @@ func (r *Runner) startWasmCache(ctx context.Context) {
 			Salt:      salt,
 			TLSConfig: tlsConfig,
 		},
-		// Wasm cache options
-		wasm.CacheOptions{
-			CacheDir: wasmCacheDir,
-		}, r.Logger)
+		cacheOption, r.Logger)
 	r.wasmCache.Start(ctx)
 }
 
diff --git a/internal/logging/log.go b/internal/logging/log.go
index 274f11f05..e38af9082 100644
--- a/internal/logging/log.go
+++ b/internal/logging/log.go
@@ -23,8 +23,8 @@ type Logger struct {
 	sugaredLogger *zap.SugaredLogger
 }
 
-func NewLogger(logging *egv1a1.EnvoyGatewayLogging) Logger {
-	logger := initZapLogger(os.Stdout, logging, logging.Level[egv1a1.LogComponentGatewayDefault])
+func NewLogger(w io.Writer, logging *egv1a1.EnvoyGatewayLogging) Logger {
+	logger := initZapLogger(w, logging, logging.Level[egv1a1.LogComponentGatewayDefault])
 
 	return Logger{
 		Logger:        zapr.NewLogger(logger),
diff --git a/internal/provider/file/resources.go b/internal/provider/file/resources.go
index ac80863f7..576d9665a 100644
--- a/internal/provider/file/resources.go
+++ b/internal/provider/file/resources.go
@@ -21,7 +21,7 @@ func loadFromFilesAndDirs(files, dirs []string) ([]*resource.Resources, error) {
 	for _, file := range files {
 		r, err := loadFromFile(file)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("failed to load resources from file %s: %w", file, err)
 		}
 		rs = append(rs, r)
 	}
@@ -67,10 +67,10 @@ func loadFromDir(path string) ([]*resource.Resources, error) {
 		if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
 			continue
 		}
-
-		r, err := loadFromFile(filepath.Join(path, entry.Name()))
+		full := filepath.Join(path, entry.Name())
+		r, err := loadFromFile(full)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("failed to load resources from file %s: %w", full, err)
 		}
 
 		rs = append(rs, r)
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index b404cfffc..5a42fb7da 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -29,7 +29,7 @@ bug fixes: |
   Bypass overload manager for stats and ready listeners
   Fix translating backendSettings for extAuth
   Fix an issue that stats compressor was not working.
-  Added support for BackendTLSPolicy parsing in Standalone mode.
+  Added support for BackendTLSPolicy and EnvoyExtensionPolicy parsing in Standalone mode.
 
 # Enhancements that improve performance.
 performance improvements: |



Test patch:
diff --git a/internal/admin/server_test.go b/internal/admin/server_test.go
index a07291d8b..7a2b3ede8 100644
--- a/internal/admin/server_test.go
+++ b/internal/admin/server_test.go
@@ -6,6 +6,7 @@
 package admin
 
 import (
+	"os"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -22,7 +23,7 @@ func TestInitAdminServer(t *testing.T) {
 		},
 	}
 
-	svrConfig.Logger = logging.NewLogger(egv1a1.DefaultEnvoyGatewayLogging())
+	svrConfig.Logger = logging.NewLogger(os.Stdout, egv1a1.DefaultEnvoyGatewayLogging())
 	err := Init(svrConfig)
 	require.NoError(t, err)
 }
diff --git a/internal/cmd/certgen_test.go b/internal/cmd/certgen_test.go
index f19afa359..8f315325f 100644
--- a/internal/cmd/certgen_test.go
+++ b/internal/cmd/certgen_test.go
@@ -6,6 +6,7 @@
 package cmd
 
 import (
+	"os"
 	"path/filepath"
 	"testing"
 
@@ -21,7 +22,7 @@ func TestGetCertgenCommand(t *testing.T) {
 }
 
 func TestOutputCertsForLocal(t *testing.T) {
-	cfg, err := getConfig()
+	cfg, err := getConfig(os.Stdout)
 	require.NoError(t, err)
 
 	certs, err := crypto.GenerateCerts(cfg)
diff --git a/internal/cmd/egctl/testdata/translate/in/backend-endpoint.yaml b/internal/cmd/egctl/testdata/translate/in/backend-endpoint.yaml
index 0f1f3ab69..f845c0941 100644
--- a/internal/cmd/egctl/testdata/translate/in/backend-endpoint.yaml
+++ b/internal/cmd/egctl/testdata/translate/in/backend-endpoint.yaml
@@ -57,3 +57,21 @@ spec:
   validation:
     wellKnownCACertificates: "System"
     hostname: www.example.com
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: EnvoyExtensionPolicy
+metadata:
+  name: ext-proc-example
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: backend
+  extProc:
+    - backendRefs:
+        - name: grpc-ext-proc
+          port: 9002
+      processingMode:
+        request: {}
+        response:
+          body: Streamed
diff --git a/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml b/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml
index 41a84b3d8..1085a7aa2 100644
--- a/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml
+++ b/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml
@@ -43,6 +43,43 @@ backends:
       reason: Accepted
       status: "True"
       type: Accepted
+envoyExtensionPolicies:
+- kind: EnvoyExtensionPolicy
+  metadata:
+    creationTimestamp: null
+    name: ext-proc-example
+    namespace: envoy-gateway-system
+  spec:
+    extProc:
+    - backendRefs:
+      - group: ""
+        kind: Service
+        name: grpc-ext-proc
+        port: 9002
+      processingMode:
+        request: {}
+        response:
+          body: Streamed
+    targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: backend
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: eg
+        namespace: envoy-gateway-system
+      conditions:
+      - lastTransitionTime: null
+        message: |-
+          Wasm: wasm cache is not initialized
+          ExtProc: service envoy-gateway-system/grpc-ext-proc not found.
+        reason: Invalid
+        status: "False"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
 gatewayClass:
   kind: GatewayClass
   metadata:
diff --git a/internal/cmd/server_test.go b/internal/cmd/server_test.go
index 83660d6d5..f04be348b 100644
--- a/internal/cmd/server_test.go
+++ b/internal/cmd/server_test.go
@@ -60,7 +60,7 @@ func TestGetConfigValidate(t *testing.T) {
 			_, err = file.Write([]byte(test.input))
 			require.NoError(t, err)
 
-			_, err = getConfigByPath(file.Name())
+			_, err = getConfigByPath(os.Stderr, file.Name())
 			if test.errors == nil {
 				require.NoError(t, err)
 			} else {
diff --git a/internal/logging/log_test.go b/internal/logging/log_test.go
index 0942910f7..265df4111 100644
--- a/internal/logging/log_test.go
+++ b/internal/logging/log_test.go
@@ -32,7 +32,7 @@ func TestZapLogLevel(t *testing.T) {
 }
 
 func TestLogger(t *testing.T) {
-	logger := NewLogger(egv1a1.DefaultEnvoyGatewayLogging())
+	logger := NewLogger(os.Stdout, egv1a1.DefaultEnvoyGatewayLogging())
 	logger.Info("kv msg", "key", "value")
 	logger.Sugar().Infof("template %s %d", "string", 123)
 
@@ -62,7 +62,7 @@ func TestLoggerWithName(t *testing.T) {
 	config := egv1a1.DefaultEnvoyGatewayLogging()
 	config.Level[egv1a1.LogComponentInfrastructureRunner] = egv1a1.LogLevelDebug
 
-	logger := NewLogger(config).WithName(string(egv1a1.LogComponentInfrastructureRunner))
+	logger := NewLogger(os.Stdout, config).WithName(string(egv1a1.LogComponentInfrastructureRunner))
 	logger.Info("info message")
 	logger.Sugar().Debugf("debug message")
 
@@ -93,7 +93,7 @@ func TestLoggerSugarName(t *testing.T) {
 	config := egv1a1.DefaultEnvoyGatewayLogging()
 	config.Level[logName] = egv1a1.LogLevelDebug
 
-	logger := NewLogger(config).WithName(logName)
+	logger := NewLogger(os.Stdout, config).WithName(logName)
 
 	logger.Sugar().Debugf("debugging message")
 
diff --git a/internal/provider/file/resources_test.go b/internal/provider/file/resources_test.go
new file mode 100644
index 000000000..cecaa85b7
--- /dev/null
+++ b/internal/provider/file/resources_test.go
@@ -0,0 +1,87 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package file
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_loadFromFilesAndDirs(t *testing.T) {
+	t.Run("non-existent file", func(t *testing.T) {
+		_, err := loadFromFilesAndDirs([]string{"non-existent-file"}, nil)
+		require.ErrorContains(t, err, "file non-existent-file is not exist")
+	})
+	t.Run("invalid content in a file", func(t *testing.T) {
+		tmpfile := t.TempDir() + "/invalid.yaml"
+		err := os.WriteFile(tmpfile, []byte("invalid"), 0o600)
+		require.NoError(t, err)
+		_, err = loadFromFilesAndDirs([]string{tmpfile}, nil)
+		require.ErrorContains(t, err,
+			fmt.Sprintf("failed to load resources from file %s", tmpfile))
+	})
+	t.Run("non-existent directory", func(t *testing.T) {
+		_, err := loadFromFilesAndDirs(nil, []string{"non-existent-directory"})
+		require.ErrorContains(t, err, "no such file or directory")
+	})
+	t.Run("invalid content in a file in a directory", func(t *testing.T) {
+		tmpdir := t.TempDir()
+		err := os.WriteFile(filepath.Join(tmpdir, "invalid.yaml"), []byte("invalid"), 0o600)
+		require.NoError(t, err)
+		_, err = loadFromFilesAndDirs(nil, []string{tmpdir})
+		require.ErrorContains(t, err,
+			fmt.Sprintf("failed to load resources from file %s", filepath.Join(tmpdir, "invalid.yaml")))
+	})
+	t.Run("ok", func(t *testing.T) {
+		tmpfile := t.TempDir() + "/valid.yaml"
+		err := os.WriteFile(tmpfile, []byte(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: GatewayClass
+metadata:
+  name: aigw-run
+spec:
+  controllerName: gateway.envoyproxy.io/gatewayclass-controller
+`), 0o600)
+		require.NoError(t, err)
+		rs, err := loadFromFilesAndDirs([]string{tmpfile}, nil)
+		require.NoError(t, err)
+		require.Len(t, rs, 1)
+	})
+}
+
+func Test_loadFromDir(t *testing.T) {
+	t.Run("non-existent directory", func(t *testing.T) {
+		_, err := loadFromDir("non-existent-directory")
+		require.ErrorContains(t, err, "no such file or directory")
+	})
+	t.Run("invalid content in a file", func(t *testing.T) {
+		tmpdir := t.TempDir()
+		err := os.WriteFile(filepath.Join(tmpdir, "invalid.yaml"), []byte("invalid"), 0o600)
+		require.NoError(t, err)
+		_, err = loadFromDir(tmpdir)
+		require.ErrorContains(t, err,
+			fmt.Sprintf("failed to load resources from file %s", filepath.Join(tmpdir, "invalid.yaml")))
+	})
+	t.Run("ok", func(t *testing.T) {
+		tmpdir := t.TempDir()
+		err := os.WriteFile(filepath.Join(tmpdir, "valid.yaml"), []byte(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: GatewayClass
+metadata:
+  name: aigw-run
+spec:
+  controllerName: gateway.envoyproxy.io/gatewayclass-controller
+`), 0o600)
+		require.NoError(t, err)
+		rs, err := loadFromDir(tmpdir)
+		require.NoError(t, err)
+		require.Len(t, rs, 1)
+	})
+}
diff --git a/test/fuzz/ir_fuzz_test.go b/test/fuzz/xds_fuzz_test.go
similarity index 72%
rename from test/fuzz/ir_fuzz_test.go
rename to test/fuzz/xds_fuzz_test.go
index 3107dd02f..7bde70604 100644
--- a/test/fuzz/ir_fuzz_test.go
+++ b/test/fuzz/xds_fuzz_test.go
@@ -15,14 +15,22 @@ import (
 	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
 )
 
-func FuzzGatewayAPIToIR(f *testing.F) {
+func FuzzGatewayAPIToXDS(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
 		fc := fuzz.NewConsumer(data)
 		resources := &resource.Resources{}
 		if err := fc.GenerateStruct(resources); err != nil {
 			return
 		}
-		addMissingResources, err := fc.GetBool()
+		namespace, err := fc.GetString()
+		if err != nil {
+			return
+		}
+		dnsDomain, err := fc.GetString()
+		if err != nil {
+			return
+		}
+		resourceType, err := fc.GetString()
 		if err != nil {
 			return
 		}
@@ -32,11 +40,15 @@ func FuzzGatewayAPIToIR(f *testing.F) {
 		if err != nil {
 			return
 		}
+		addMissingResources, err := fc.GetBool()
+		if err != nil {
+			return
+		}
 		rs, err := resource.LoadResourcesFromYAMLBytes(yamlBytes, addMissingResources)
 		if err != nil {
 			return
 		}
 
-		_, _ = egctl.TranslateGatewayAPIToIR(rs)
+		_, _ = egctl.TranslateGatewayAPIToXds(namespace, dnsDomain, resourceType, rs)
 	})
 }