Skip to content
Merged
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
4 changes: 2 additions & 2 deletions examples/extension-server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ go 1.25.3

require (
github.com/envoyproxy/gateway v1.3.1
github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f
github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7
github.com/urfave/cli/v2 v2.27.7
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
Expand Down
2 changes: 2 additions & 0 deletions examples/extension-server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f h1:36vvJBe/wXWfD7qrTb1WnbPVPMxNFDfEygztH8wgebw=
github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f/go.mod h1:PTY7yDlLxB4bW7rEOO7e79uTDr9yXzpuI1QGIDfxEzc=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f h1:4efYrIQgVRwCmwCveby6ck+VpxqzibdOL1Out1rJqqc=
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f/go.mod h1:2LcmvJoXsDSrsGZIxGM0Gah9ykiwTn/kgjyQdnNH8Jc=
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
Expand Down
4 changes: 4 additions & 0 deletions internal/crypto/certgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func GenerateCerts(cfg *config.Server) (*Certificates, error) {
case egv1a1.ProviderTypeKubernetes:
egDNSNames = kubeServiceNames(DefaultEnvoyGatewayDNSPrefix, cfg.ControllerNamespace, cfg.DNSDomain)
envoyDNSNames = append(envoyDNSNames, fmt.Sprintf("*.%s", cfg.ControllerNamespace))
case egv1a1.ProviderTypeCustom:
// For custom provider (host mode), use localhost for xDS communication
egDNSNames = []string{"localhost"}
envoyDNSNames = []string{"localhost"}
default:
// Kubernetes is the only supported Envoy Gateway provider.
return nil, fmt.Errorf("unsupported provider type %v", egProvider)
Expand Down
30 changes: 23 additions & 7 deletions internal/crypto/certgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,24 @@ import (

"github.com/stretchr/testify/require"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/envoygateway/config"
)

func TestGenerateCerts(t *testing.T) {
type testcase struct {
certConfig *Configuration
cfg *config.Server
wantEnvoyGatewayDNSName string
wantEnvoyDNSName string
}

cfg, err := config.New(os.Stdout, os.Stderr)
require.NoError(t, err)

run := func(t *testing.T, name string, tc testcase) {
t.Helper()

t.Run(name, func(t *testing.T) {
t.Helper()

got, err := GenerateCerts(cfg)
got, err := GenerateCerts(tc.cfg)
require.NoError(t, err)

roots := x509.NewCertPool()
Expand All @@ -52,11 +50,29 @@ func TestGenerateCerts(t *testing.T) {
})
}

run(t, "no configuration - use defaults", testcase{
certConfig: &Configuration{},
// Test Kubernetes provider (default)
kubeCfg, err := config.New(os.Stdout, os.Stderr)
require.NoError(t, err)

run(t, "kubernetes provider - use defaults", testcase{
cfg: kubeCfg,
wantEnvoyGatewayDNSName: DefaultEnvoyGatewayDNSPrefix,
wantEnvoyDNSName: fmt.Sprintf("*.%s", config.DefaultNamespace),
})

// Test Custom provider
customCfg, err := config.New(os.Stdout, os.Stderr)
require.NoError(t, err)
// Set provider type to Custom
customCfg.EnvoyGateway.Provider = &egv1a1.EnvoyGatewayProvider{
Type: egv1a1.ProviderTypeCustom,
}

run(t, "custom provider - use localhost", testcase{
cfg: customCfg,
wantEnvoyGatewayDNSName: "localhost",
wantEnvoyDNSName: "localhost",
})
}

func TestGeneratedValidKubeCerts(t *testing.T) {
Expand Down
67 changes: 62 additions & 5 deletions internal/infrastructure/host/infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import (
"io"
"os"
"path/filepath"
"sync"

func_e "github.com/tetratelabs/func-e"
func_e_api "github.com/tetratelabs/func-e/api"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/crypto"
"github.com/envoyproxy/gateway/internal/envoygateway/config"
"github.com/envoyproxy/gateway/internal/infrastructure/common"
"github.com/envoyproxy/gateway/internal/logging"
Expand Down Expand Up @@ -42,7 +47,7 @@ type Infra struct {
EnvoyGateway *egv1a1.EnvoyGateway

// proxyContextMap store the context of each running proxy by its name for lifecycle management.
proxyContextMap map[string]*proxyContext
proxyContextMap sync.Map

// sdsConfigPath is the path to SDS configuration files.
sdsConfigPath string
Expand All @@ -54,6 +59,9 @@ type Infra struct {
Stdout io.Writer
// Stderr is the writer for error output (for Envoy stderr).
Stderr io.Writer

// envoyRunner runs Envoy (can be overridden in tests).
envoyRunner func_e_api.RunFunc
}

func NewInfra(runnerCtx context.Context, cfg *config.Server, logger logging.Logger) (*Infra, error) {
Expand All @@ -75,10 +83,10 @@ func NewInfra(runnerCtx context.Context, cfg *config.Server, logger logging.Logg
return nil, fmt.Errorf("failed to create data directory: %w", err)
}

// Check local certificates dir exist
// Check if certificates exist, generate them if not
certPath := paths.CertDir("envoy")
if _, err := os.Lstat(certPath); err != nil {
return nil, fmt.Errorf("failed to stat cert dir: %w", err)
if err := maybeGenerateCertificates(cfg, certPath); err != nil {
Comment on lines 87 to +88
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this might be wrong -- paths.CertDir("") instead of paths.CertDir("envoy"). let me confirm

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not sure on how to fix but envoyproxy/ai-gateway#1470 tests are failing without something

return nil, err
}

// Ensure the sds config exist
Expand All @@ -90,11 +98,11 @@ func NewInfra(runnerCtx context.Context, cfg *config.Server, logger logging.Logg
Paths: paths,
Logger: logger,
EnvoyGateway: cfg.EnvoyGateway,
proxyContextMap: make(map[string]*proxyContext),
sdsConfigPath: certPath,
defaultEnvoyImage: egv1a1.DefaultEnvoyProxyImage,
Stdout: cfg.Stdout,
Stderr: cfg.Stderr,
envoyRunner: func_e.Run,
}
return infra, nil
}
Expand All @@ -116,3 +124,52 @@ func createSdsConfig(dir string) error {

return nil
}

// maybeGenerateCertificates checks if all required certificate files exist and generates them if any is missing.
func maybeGenerateCertificates(cfg *config.Server, certPath string) error {
certFiles := []string{"ca.crt", "tls.crt", "tls.key"}

// Check if any cert file is missing
var missing bool
for _, filename := range certFiles {
filePath := filepath.Join(certPath, filename)
_, err := os.Lstat(filePath)
if os.IsNotExist(err) {
missing = true
break
}
if err != nil {
return fmt.Errorf("failed to stat %s: %w", filename, err)
}
}

if !missing {
// All files exist, nothing to do
return nil
}

// Generate certificates automatically
certs, err := crypto.GenerateCerts(cfg)
if err != nil {
return fmt.Errorf("failed to generate certificates: %w", err)
}

// Create the cert directory
if err := os.MkdirAll(certPath, 0o750); err != nil {
return fmt.Errorf("failed to create cert directory: %w", err)
}

// Write cert files
certMap := map[string][]byte{
"ca.crt": certs.CACertificate,
"tls.crt": certs.EnvoyCertificate,
"tls.key": certs.EnvoyPrivateKey,
}

for filename, content := range certMap {
if err := file.Write(string(content), filepath.Join(certPath, filename)); err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err)
}
}
return nil
}
131 changes: 131 additions & 0 deletions internal/infrastructure/host/infra_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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 host

import (
"io"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/envoyproxy/gateway/internal/envoygateway/config"
"github.com/envoyproxy/gateway/internal/infrastructure/common"
"github.com/envoyproxy/gateway/internal/utils/file"
)

func TestMaybeGenerateCertificates(t *testing.T) {
cfg, err := config.New(io.Discard, io.Discard)
require.NoError(t, err)

certFiles := []string{"ca.crt", "tls.crt", "tls.key"}

t.Run("all_files_exist", func(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "envoy")

// Create directory and dummy files
require.NoError(t, os.MkdirAll(certPath, 0o750))
for _, filename := range certFiles {
fpath := filepath.Join(certPath, filename)
require.NoError(t, os.WriteFile(fpath, []byte("dummy"), 0o600))
}

err := maybeGenerateCertificates(cfg, certPath)
require.NoError(t, err)

// Verify files still exist and unchanged size
for _, filename := range certFiles {
data, err := os.ReadFile(filepath.Join(certPath, filename))
require.NoError(t, err)
require.Len(t, data, 5) // "dummy"
}
})

t.Run("missing_files", func(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "envoy")

err := maybeGenerateCertificates(cfg, certPath)
require.NoError(t, err)

// Verify directory created
info, err := os.Stat(certPath)
require.NoError(t, err)
require.True(t, info.IsDir())

// Verify all files created and non-empty
for _, filename := range certFiles {
data, err := os.ReadFile(filepath.Join(certPath, filename))
require.NoError(t, err)
require.NotEmpty(t, data, filename)
}
})

t.Run("partial_files_missing", func(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "envoy")

require.NoError(t, os.MkdirAll(certPath, 0o750))

// Create only one file
require.NoError(t, os.WriteFile(filepath.Join(certPath, "ca.crt"), []byte("dummy"), 0o600))

err := maybeGenerateCertificates(cfg, certPath)
require.NoError(t, err)

// Verify all files created and non-empty
for _, filename := range certFiles {
data, err := os.ReadFile(filepath.Join(certPath, filename))
require.NoError(t, err)
require.NotEmpty(t, data, filename)
}
})

t.Run("cert_generation_fails", func(t *testing.T) {
tmpDir := t.TempDir()
// This tests mkdir fail by making parent unwritable
unwritableDir := filepath.Join(tmpDir, "unwritable")
require.NoError(t, os.Mkdir(unwritableDir, 0o555)) // Read-only

badCertPath := filepath.Join(unwritableDir, "envoy")
err := maybeGenerateCertificates(cfg, badCertPath)
require.ErrorContains(t, err, "failed to create cert directory")
})
}

func TestCreateSdsConfig(t *testing.T) {
t.Run("success", func(t *testing.T) {
dir := t.TempDir()
// Create required cert files
require.NoError(t, file.Write("test ca", filepath.Join(dir, XdsTLSCaFilename)))
require.NoError(t, file.Write("test cert", filepath.Join(dir, XdsTLSCertFilename)))
require.NoError(t, file.Write("test key", filepath.Join(dir, XdsTLSKeyFilename)))

err := createSdsConfig(dir)
require.NoError(t, err)

// Verify CA config was created
caConfigPath := filepath.Join(dir, common.SdsCAFilename)
actualCAConfig, err := os.ReadFile(caConfigPath)
require.NoError(t, err)
require.NotEmpty(t, actualCAConfig)

// Verify cert config was created
certConfigPath := filepath.Join(dir, common.SdsCertFilename)
actualCertConfig, err := os.ReadFile(certConfigPath)
require.NoError(t, err)
require.NotEmpty(t, actualCertConfig)
})

t.Run("error_writing_ca_config", func(t *testing.T) {
// Use invalid path to force file.Write to fail
invalidDir := filepath.Join("/", "nonexistent", "invalid", "path")
err := createSdsConfig(invalidDir)
require.Error(t, err)
})
}
Loading
Loading