Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kube Gateway Policy Validation #9456

Merged
merged 54 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
dd1bada
add status changed predicate
lgadban Apr 25, 2024
7e81455
wip: validation plumbing; allow validation to create a ggv2 translator
lgadban Apr 25, 2024
8901239
Merge branch 'main' into ggv2-validation
lgadban Apr 25, 2024
1b75c15
fix merge
lgadban Apr 25, 2024
6792cff
wip: ggv2 validator impl
lgadban May 1, 2024
fb683d0
hermetic dummy proxy validation
lgadban May 3, 2024
91edb59
use simple ggv2 validation helper for hermetic validation
lgadban May 7, 2024
b8a5574
wrap new validation in k8s gw feature gate
lgadban May 7, 2024
89f8141
Merge branch 'main' into ggv2-validation
lgadban May 7, 2024
62c8e5c
cleanup
lgadban May 7, 2024
b0906aa
cleanup
lgadban May 7, 2024
2cced1d
cleanup
lgadban May 7, 2024
53d90a5
changelog
lgadban May 7, 2024
3ca4200
add note to docs for allowWarnings
lgadban May 7, 2024
5c42919
add kubeGateway flag to validationWebhook, update tests
lgadban May 7, 2024
92ad0da
Merge branch 'main' into ggv2-validation
lgadban May 7, 2024
5796bd8
test cleanup
lgadban May 7, 2024
b8f0bd3
better testing
lgadban May 7, 2024
ac0c639
cleanup install script
lgadban May 7, 2024
cc1c83a
cleanup and comments
lgadban May 7, 2024
6cd5259
Merge branch 'main' into ggv2-validation
lgadban May 8, 2024
f356a63
update vhost plugin error msg
lgadban May 8, 2024
e953de0
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 8, 2024
594f085
remove redundant kubegateway var
lgadban May 8, 2024
f41f8bc
Merge branch 'ggv2-validation' of github.com:solo-io/gloo into ggv2-v…
lgadban May 8, 2024
49ca0f1
commentary on webhook manifest
lgadban May 8, 2024
cd28885
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 9, 2024
bd59982
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 9, 2024
52435cf
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 9, 2024
5da5aa2
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 9, 2024
a63992a
Adding changelog file to new location
May 9, 2024
26a5a6f
Deleting changelog file from old location
May 9, 2024
9c50160
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 10, 2024
781597c
wip: e2e test for validation webhook
lgadban May 10, 2024
baed12a
Merge branch 'ggv2-validation' of github.com:solo-io/gloo into ggv2-v…
lgadban May 10, 2024
cd3c72e
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 10, 2024
72a60ae
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 10, 2024
007a03a
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 10, 2024
3c2c392
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 10, 2024
575f462
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 13, 2024
d1988d2
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 13, 2024
db19e58
Adding changelog file to new location
May 13, 2024
f7544ec
Deleting changelog file from old location
May 13, 2024
292ccd8
add assertions for webhook validation testing
lgadban May 13, 2024
851b4a0
Merge branch 'ggv2-validation' of github.com:solo-io/gloo into ggv2-v…
lgadban May 13, 2024
1ac7920
remove redundant clustername fallback
lgadban May 13, 2024
eb12f4c
add TestK8sGatewayNoValidation to CI
lgadban May 13, 2024
d5b7bb6
better documentation for validation-based assertions
lgadban May 13, 2024
9f5f3c5
Merge branch 'main' into ggv2-validation
lgadban May 13, 2024
aef0c33
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 14, 2024
84cd5e3
Merge branch 'main' into ggv2-validation
sam-heilbron May 15, 2024
664ef7a
proper separator in regex
sam-heilbron May 15, 2024
db5179c
Merge branch 'main' into ggv2-validation
sam-heilbron May 16, 2024
79f32a6
Merge refs/heads/main into ggv2-validation
soloio-bulldozer[bot] May 16, 2024
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: 1 addition & 1 deletion .github/workflows/pr-kubernetes-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
kubectl-version: 'v1.28.4'
helm-version: 'v3.13.2'
go-test-args: '-v -timeout=25m'
go-test-run-regex: '(^TestK8sGatewayIstio$$|^TestGlooGatewayEdgeGateway$$|^TestGlooctlIstioInjectEdgeApiGateway$$)'
go-test-run-regex: '(^TestK8sGatewayIstio$$|^TestGlooGatewayEdgeGateway$$|^TestGlooctlIstioInjectEdgeApiGateway$$|^TestK8sGatewayNoValidation$$)'

steps:
- id: auto-succeed-tests
Expand Down
6 changes: 6 additions & 0 deletions changelog/v1.17.0-beta28/ggv2-validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
changelog:
- type: NEW_FEATURE
issueLink: https://github.com/solo-io/solo-projects/issues/6063
resolvesIssue: true
description: |
Adds webhook validation for Gloo Gateway Policies (e.g. RouteOption and VirtualHostOption) when used with Kubernetes Gateway API

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ webhooks:
path: "/validation"
caBundle: "" # update manually or use certgen job or cert-manager's ca-injector
rules:
{{- if .Values.kubeGateway.enabled }}
sam-heilbron marked this conversation as resolved.
Show resolved Hide resolved
- operations: [ "CREATE", "UPDATE" ]
# RouteOption and VirtualHostOption DELETEs are not supported.
# Their validation is currently limited to usage as Kube Gateway API Policies
# and are hermetically validated for semantic correctness only. This means there
# is no validation needed for DELETEs, as a DELETE will never result be semantically invalid
apiGroups: ["gateway.solo.io"]
apiVersions: ["v1"]
resources: ["routeoptions", "virtualhostoptions"]
lgadban marked this conversation as resolved.
Show resolved Hide resolved
{{- end }}{{/* if .Values.kubeGateway.enabled */}}
- operations: {{ include "gloo.webhookvalidation.operationsForResource" (list "virtualservices" .Values.gateway.validation.webhook.skipDeleteValidationResources) }}
apiGroups: ["gateway.solo.io"]
apiVersions: ["v1"]
Expand Down
30 changes: 22 additions & 8 deletions pkg/utils/kubeutils/kubectl/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,27 @@ func (c *Cli) Apply(ctx context.Context, content []byte, extraArgs ...string) er

// ApplyFile applies the resources defined in a file, and returns an error if one occurred
func (c *Cli) ApplyFile(ctx context.Context, fileName string, extraArgs ...string) error {
_, err := c.ApplyFileWithOutput(ctx, fileName, extraArgs...)
return err
}

// 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...)

fileInput, err := os.Open(fileName)
if err != nil {
return err
return "", err
}
defer func() {
_ = fileInput.Close()
}()

return c.Command(ctx, applyArgs...).
runErr := c.Command(ctx, applyArgs...).
WithStdin(fileInput).
Run().
Cause()
Run()
return runErr.OutputString(), runErr.Cause()
}

// Delete deletes the resources defined in the bytes, and returns an error if one occurred
Expand All @@ -114,20 +121,27 @@ func (c *Cli) Delete(ctx context.Context, content []byte, extraArgs ...string) e

// DeleteFile deletes the resources defined in a file, and returns an error if one occurred
func (c *Cli) DeleteFile(ctx context.Context, fileName string, extraArgs ...string) error {
_, err := c.DeleteFileWithOutput(ctx, fileName, extraArgs...)
return err
}

// DeleteFileWithOutput deletes the resources defined in a file,
// if an error occurred, it will be returned along with the output of the command
func (c *Cli) DeleteFileWithOutput(ctx context.Context, fileName string, extraArgs ...string) (string, error) {
applyArgs := append([]string{"delete", "-f", fileName}, extraArgs...)

fileInput, err := os.Open(fileName)
if err != nil {
return err
return "", err
}
defer func() {
_ = fileInput.Close()
}()

return c.Command(ctx, applyArgs...).
runErr := c.Command(ctx, applyArgs...).
WithStdin(fileInput).
Run().
Cause()
Run()
return runErr.OutputString(), runErr.Cause()
}

// DeleteFileSafe deletes the resources defined in a file, and returns an error if one occurred
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,19 @@ type WebhookConfig struct {
alwaysAccept bool // accept all resources
readGatewaysFromAllNamespaces bool
webhookNamespace string
kubeGatewayEnabled bool
}

func NewWebhookConfig(ctx context.Context, validator validation.Validator, watchNamespaces []string, port int, serverCertPath, serverKeyPath string, alwaysAccept, readGatewaysFromAllNamespaces bool, webhookNamespace string) WebhookConfig {
func NewWebhookConfig(
ctx context.Context,
validator validation.Validator,
watchNamespaces []string,
port int,
serverCertPath, serverKeyPath string,
alwaysAccept, readGatewaysFromAllNamespaces bool,
webhookNamespace string,
kubeGatewayEnabled bool,
) WebhookConfig {
return WebhookConfig{
ctx: ctx,
validator: validator,
Expand All @@ -111,7 +121,9 @@ func NewWebhookConfig(ctx context.Context, validator validation.Validator, watch
serverKeyPath: serverKeyPath,
alwaysAccept: alwaysAccept,
readGatewaysFromAllNamespaces: readGatewaysFromAllNamespaces,
webhookNamespace: webhookNamespace}
webhookNamespace: webhookNamespace,
kubeGatewayEnabled: kubeGatewayEnabled,
}
}

func NewGatewayValidatingWebhook(cfg WebhookConfig) (*http.Server, error) {
Expand All @@ -124,6 +136,7 @@ func NewGatewayValidatingWebhook(cfg WebhookConfig) (*http.Server, error) {
alwaysAccept := cfg.alwaysAccept
readGatewaysFromAllNamespaces := cfg.readGatewaysFromAllNamespaces
webhookNamespace := cfg.webhookNamespace
kubeGatewayEnabled := cfg.kubeGatewayEnabled

certProvider, err := NewCertificateProvider(serverCertPath, serverKeyPath, log.New(&debugLogger{ctx: ctx}, "validation-webhook-certificate-watcher", log.LstdFlags), ctx, 10*time.Second)
if err != nil {
Expand All @@ -137,6 +150,7 @@ func NewGatewayValidatingWebhook(cfg WebhookConfig) (*http.Server, error) {
alwaysAccept,
readGatewaysFromAllNamespaces,
webhookNamespace,
kubeGatewayEnabled,
)

mux := http.NewServeMux()
Expand Down Expand Up @@ -165,6 +179,7 @@ type gatewayValidationWebhook struct {
alwaysAccept bool // read only so no races
readGatewaysFromAllNamespaces bool // read only so no races
webhookNamespace string // read only so no races
kubeGatewayEnabled bool // read only so no races
}

type AdmissionReviewWithProxies struct {
Expand All @@ -183,13 +198,24 @@ type AdmissionResponseWithProxies struct {
Proxies []*gloov1.Proxy `json:"proxies,omitempty"`
}

func NewGatewayValidationHandler(ctx context.Context, validator validation.Validator, watchNamespaces []string, alwaysAccept bool, readGatewaysFromAllNamespaces bool, webhookNamespace string) *gatewayValidationWebhook {
return &gatewayValidationWebhook{ctx: ctx,
func NewGatewayValidationHandler(
ctx context.Context,
validator validation.Validator,
watchNamespaces []string,
alwaysAccept bool,
readGatewaysFromAllNamespaces bool,
webhookNamespace string,
kubeGatewayEnabled bool,
) *gatewayValidationWebhook {
return &gatewayValidationWebhook{
ctx: ctx,
validator: validator,
watchNamespaces: watchNamespaces,
alwaysAccept: alwaysAccept,
readGatewaysFromAllNamespaces: readGatewaysFromAllNamespaces,
webhookNamespace: webhookNamespace}
webhookNamespace: webhookNamespace,
kubeGatewayEnabled: kubeGatewayEnabled,
}
}

func (wh *gatewayValidationWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -441,6 +467,11 @@ func (wh *gatewayValidationWebhook) validateGvk(ctx context.Context, gvk schema.
var reports *validation.Reports
newResourceFunc := gloosnapshot.ApiGvkToHashableResource[gvk]

// check to see if we should perform validation for this GVK
if !wh.shouldValidateGvk(gvk) {
sam-heilbron marked this conversation as resolved.
Show resolved Hide resolved
return nil, nil
}

newResource := newResourceFunc()
oldResource := newResourceFunc()

Expand Down Expand Up @@ -475,6 +506,17 @@ func (wh *gatewayValidationWebhook) validateList(ctx context.Context, rawJson []
return reports, nil
}

func (wh *gatewayValidationWebhook) shouldValidateGvk(gvk schema.GroupVersionKind) bool {
if gvk == gwv1.RouteOptionGVK || gvk == gwv1.VirtualHostOptionGVK {
// only validate RouteOption and VirtualHostOption resources if K8s Gateway is enabled
return wh.kubeGatewayEnabled
}
sam-heilbron marked this conversation as resolved.
Show resolved Hide resolved

// no other special considerations at this point, so continue with validation
return true
}

// shouldValidateResource determines if a resource should be validated AND populates `resource` (and `oldResource` if applicable) from the objects[s] in the `admissionRequest`
func (wh *gatewayValidationWebhook) shouldValidateResource(ctx context.Context, admissionRequest *v1beta1.AdmissionRequest, resource, oldResource resources.HashableResource) (bool, error) {
logger := contextutils.LoggerFrom(ctx)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/rotisserie/eris"
"github.com/solo-io/gloo/projects/gateway/pkg/validation"
validation2 "github.com/solo-io/gloo/projects/gloo/pkg/api/grpc/validation"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/faultinjection"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/headers"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/static"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand All @@ -37,11 +39,11 @@ import (

var _ = Describe("ValidatingAdmissionWebhook", func() {

const errMsg = "didn't say the magic word"
var (
srv *httptest.Server
mv *mockValidator
wh *gatewayValidationWebhook
errMsg = "didn't say the magic word"
srv *httptest.Server
mv *mockValidator
wh *gatewayValidationWebhook
)

BeforeEach(func() {
Expand All @@ -52,7 +54,6 @@ var _ = Describe("ValidatingAdmissionWebhook", func() {
validator: mv,
}
srv = httptest.NewServer(wh)

})

setMockFunctions := func() {
Expand Down Expand Up @@ -110,6 +111,32 @@ var _ = Describe("ValidatingAdmissionWebhook", func() {

routeTable := &v1.RouteTable{Metadata: &core.Metadata{Namespace: "namespace", Name: "rt"}}

routeOption := &v1.RouteOption{
Metadata: &core.Metadata{
Name: "policy",
Namespace: "default",
},
Options: &gloov1.RouteOptions{
Faults: &faultinjection.RouteFaults{
Abort: &faultinjection.RouteAbort{
Percentage: 4.19,
HttpStatus: 500,
},
},
},
}
vHostOption := &v1.VirtualHostOption{
Metadata: &core.Metadata{
Name: "policy",
Namespace: "default",
},
Options: &gloov1.VirtualHostOptions{
HeaderManipulation: &headers.HeaderManipulation{
RequestHeadersToRemove: []string{"hello"},
},
},
}

DescribeTable("processes admission requests with auto-accept validator", func(crd crd.Crd, gvk schema.GroupVersionKind, op v1beta1.Operation, resourceOrRef interface{}) {
reviewRequest := makeReviewRequest(srv.URL, crd, gvk, op, resourceOrRef)
res, err := srv.Client().Do(reviewRequest)
Expand Down Expand Up @@ -431,6 +458,80 @@ var _ = Describe("ValidatingAdmissionWebhook", func() {
Expect(err).ToNot(HaveOccurred())
})
})

Describe("Kube Gateway API Policy Validation", func() {
When("kubeGateway disabled", func() {
DescribeTable(
"always accepts",
func(fail bool, crd crd.Crd, gvk schema.GroupVersionKind, op v1beta1.Operation, resourceOrRef interface{}) {
if fail {
setMockFunctions()
}
reviewRequest := makeReviewRequest(srv.URL, crd, gvk, op, resourceOrRef)
res, err := srv.Client().Do(reviewRequest)
Expect(err).NotTo(HaveOccurred())

review, err := parseReviewResponse(res)
Expect(err).NotTo(HaveOccurred())
Expect(review.Response).NotTo(BeNil())

Expect(review.Response.Allowed).To(BeTrue())
Expect(review.Proxies).To(BeEmpty())
},
Entry("RouteOption, CREATE, auto-accept = accepted", false, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Create, routeOption),
Entry("RouteOption, UPDATE, auto-accept = accepted", false, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Update, routeOption),
Entry("VirtualHostOption CREATE, auto-accept = accepted", false, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Create, vHostOption),
Entry("VirtualHostOption UPDATE, auto-accept = accepted", false, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Update, vHostOption),
Entry("RouteOption, CREATE, auto-fail = accepted", true, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Create, routeOption),
Entry("RouteOption, UPDATE, auto-fail = accepted", true, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Update, routeOption),
Entry("VirtualHostOption CREATE, auto-fail = accepted", true, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Create, vHostOption),
Entry("VirtualHostOption UPDATE, auto-fail = accepted", true, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Update, vHostOption),
)
})
When("kubeGateway enabled", func() {
JustBeforeEach(func() {
mv = &mockValidator{}
wh = &gatewayValidationWebhook{
webhookNamespace: "namespace",
ctx: context.TODO(),
validator: mv,
kubeGatewayEnabled: true,
}
srv = httptest.NewServer(wh)
})

DescribeTable(
"webhook processes admission requests",
func(allowed bool, crd crd.Crd, gvk schema.GroupVersionKind, op v1beta1.Operation, resourceOrRef interface{}) {
if !allowed {
setMockFunctions()
}
reviewRequest := makeReviewRequest(srv.URL, crd, gvk, op, resourceOrRef)
res, err := srv.Client().Do(reviewRequest)
Expect(err).NotTo(HaveOccurred())

review, err := parseReviewResponse(res)
Expect(err).NotTo(HaveOccurred())
Expect(review.Response).NotTo(BeNil())
if !allowed {
Expect(review.Response.Allowed).To(BeFalse())
} else {
Expect(review.Response.Allowed).To(BeTrue())
}
Expect(review.Proxies).To(BeEmpty())
},
Entry("RouteOption, CREATE, auto-accept = accepted", true, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Create, routeOption),
Entry("VirtualHostOption CREATE, auto-accept = accepted", true, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Create, vHostOption),
Entry("RouteOption, CREATE, auto-fail = fail", false, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Create, routeOption),
Entry("VirtualHostOption CREATE, auto-fail = fail", false, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Create, vHostOption),
// TODO(Law): add UPDATE tests here, need to handle oldObject similar to the status update tests
npolshakova marked this conversation as resolved.
Show resolved Hide resolved
// Entry("RouteOption, UPDATE, auto-accept = accepted", true, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Update, routeOption),
// Entry("VirtualHostOption UPDATE, auto-accept = accepted", true, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Update, vHostOption),
// Entry("RouteOption, UPDATE, auto-fail = fail", false, v1.RouteOptionCrd, v1.RouteOptionCrd.GroupVersionKind(), v1beta1.Update, routeOption),
// Entry("VirtualHostOption UPDATE, auto-fail = fail", false, v1.VirtualHostOptionCrd, v1.VirtualHostOptionCrd.GroupVersionKind(), v1beta1.Update, vHostOption),
)
})
})
})

func makeReviewRequest(url string, crd crd.Crd, gvk schema.GroupVersionKind, operation v1beta1.Operation, resource interface{}) *http.Request {
Expand Down
Loading
Loading