diff --git a/connectivity/builder/builder.go b/connectivity/builder/builder.go index a186a39317..da96a47533 100644 --- a/connectivity/builder/builder.go +++ b/connectivity/builder/builder.go @@ -236,6 +236,7 @@ func concurrentTests(connTests []*check.ConnectivityTest) error { podToK8sOnControlplane{}, podToControlplaneHostCidr{}, podToK8sOnControlplaneCidr{}, + localRedirectPolicy{}, } return injectTests(tests, connTests...) } diff --git a/connectivity/builder/local_redirect_policy.go b/connectivity/builder/local_redirect_policy.go new file mode 100644 index 0000000000..4b1bf04743 --- /dev/null +++ b/connectivity/builder/local_redirect_policy.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package builder + +import ( + "github.com/cilium/cilium-cli/connectivity/check" + "github.com/cilium/cilium-cli/connectivity/tests" + "github.com/cilium/cilium-cli/utils/features" +) + +type localRedirectPolicy struct{} + +func (t localRedirectPolicy) build(ct *check.ConnectivityTest, _ map[string]string) { + newTest("lrp", ct). + WithCiliumLocalRedirectPolicy(check.CiliumLocalRedirectPolicyParams{ + Name: "lrp-addr-matcher", + SkipRedirectFromBackend: false, + }). + WithCiliumLocalRedirectPolicy(check.CiliumLocalRedirectPolicyParams{ + Name: "lrp-addr-matcher-skip-redirect", + SkipRedirectFromBackend: true, + }). + WithFeatureRequirements(features.RequireEnabled(features.LocalRedirectPolicy)). + WithFeatureRequirements(features.RequireEnabled(features.KPRSocketLB)). + WithScenarios( + tests.LRP(false), + tests.LRP(true), + ). + WithExpectations(func(a *check.Action) (egress, ingress check.Result) { + if a.Name() == "local-redirect-policy-skip-redirect-from-backend" { + if a.Source().HasLabel("other", "lrp-backend") { + return check.ResultCurlHTTPError, check.ResultNone + } + return check.ResultOK, check.ResultNone + } + return check.ResultOK, check.ResultNone + }) +} diff --git a/connectivity/builder/manifests/local-redirect-policy.yaml b/connectivity/builder/manifests/local-redirect-policy.yaml new file mode 100644 index 0000000000..a8dcef5f55 --- /dev/null +++ b/connectivity/builder/manifests/local-redirect-policy.yaml @@ -0,0 +1,21 @@ +apiVersion: "cilium.io/v2" +kind: CiliumLocalRedirectPolicy +metadata: + name: # set by the check package in WithCiliumLocalRedirectPolicy() +spec: + redirectFrontend: + addressMatcher: + ip: "169.254.169.254" + toPorts: + - port: "8080" + protocol: TCP + redirectBackend: + localEndpointSelector: + matchLabels: + io.kubernetes.pod.namespace: cilium-test + kind: lrp + toPorts: + - port: "8080" + name: "tcp-8080"" + protocol: TCP + diff --git a/connectivity/check/context.go b/connectivity/check/context.go index df99c83130..76d5e22547 100644 --- a/connectivity/check/context.go +++ b/connectivity/check/context.go @@ -70,6 +70,7 @@ type ConnectivityTest struct { ingressService map[string]Service k8sService Service externalWorkloads map[string]ExternalWorkload + lrpBackendPods map[string]Pod hostNetNSPodsByNode map[string]Pod secondaryNetworkNodeIPv4 map[string]string // node name => secondary ip @@ -203,6 +204,7 @@ func NewConnectivityTest(client *k8s.Client, p Parameters, version string, logge echoExternalPods: make(map[string]Pod), clientPods: make(map[string]Pod), clientCPPods: make(map[string]Pod), + lrpBackendPods: map[string]Pod{}, perfClientPods: []Pod{}, perfServerPod: []Pod{}, PerfResults: []common.PerfSummary{}, @@ -1115,6 +1117,10 @@ func (ct *ConnectivityTest) EchoPods() map[string]Pod { return ct.echoPods } +func (ct *ConnectivityTest) LrpBackendPods() map[string]Pod { + return ct.lrpBackendPods +} + // EchoServices returns all the non headless services func (ct *ConnectivityTest) EchoServices() map[string]Service { svcs := map[string]Service{} diff --git a/connectivity/check/deployment.go b/connectivity/check/deployment.go index f68f6a020f..85c6b0ab19 100644 --- a/connectivity/check/deployment.go +++ b/connectivity/check/deployment.go @@ -53,6 +53,8 @@ const ( kindEchoExternalNodeName = "echo-external-node" kindClientName = "client" kindPerfName = "perf" + lrpBackendDeploymentName = "lrp-backend" + kindLrpName = "lrp" hostNetNSDeploymentName = "host-netns" hostNetNSDeploymentNameNonCilium = "host-netns-non-cilium" // runs on non-Cilium test nodes @@ -924,6 +926,38 @@ func (ct *ConnectivityTest) deploy(ctx context.Context) error { } } } + + if ct.Features[features.LocalRedirectPolicy].Enabled { + containerPort := 8080 + lrpBackendDeployment := newDeployment(deploymentParameters{ + Name: lrpBackendDeploymentName, + Kind: kindLrpName, + Image: ct.params.JSONMockImage, + NamedPort: "tcp-8080", + Port: containerPort, + Affinity: &corev1.Affinity{ + PodAffinity: &corev1.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "name", Operator: metav1.LabelSelectorOpIn, Values: []string{clientDeploymentName}}, + }, + }, + TopologyKey: corev1.LabelHostname, + }, + }, + }, + }, + ReadinessProbe: newLocalReadinessProbe(containerPort, "/"), + Labels: map[string]string{"other": "lrp-backend"}, + Annotations: ct.params.DeploymentAnnotations.Match(lrpBackendDeploymentName), + }) + _, err = ct.clients.src.CreateDeployment(ctx, ct.params.TestNamespace, lrpBackendDeployment, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("unable to create deployment %s: %s", lrpBackendDeployment, err) + } + } return nil } @@ -1172,6 +1206,23 @@ func (ct *ConnectivityTest) validateDeployment(ctx context.Context) error { return nil } + if ct.Features[features.LocalRedirectPolicy].Enabled { + lrpPods, err := ct.client.ListPods(ctx, ct.params.TestNamespace, metav1.ListOptions{LabelSelector: "kind=" + kindLrpName}) + if err != nil { + return fmt.Errorf("unable to list lrp pods: %w", err) + } + for _, lrpPod := range lrpPods.Items { + _, hasLabel := lrpPod.GetLabels()["lrp-backend"] + if hasLabel { + ct.lrpBackendPods[lrpPod.Name] = Pod{ + K8sClient: ct.client, + Pod: lrpPod.DeepCopy(), + } + } + } + return nil + } + clientPods, err := ct.client.ListPods(ctx, ct.params.TestNamespace, metav1.ListOptions{LabelSelector: "kind=" + kindClientName}) if err != nil { return fmt.Errorf("unable to list client pods: %s", err) diff --git a/connectivity/check/peer.go b/connectivity/check/peer.go index 5eb9ebff13..b33129bb64 100644 --- a/connectivity/check/peer.go +++ b/connectivity/check/peer.go @@ -9,9 +9,10 @@ import ( "net/url" "strconv" + corev1 "k8s.io/api/core/v1" + "github.com/cilium/cilium/api/v1/flow" ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" - corev1 "k8s.io/api/core/v1" "github.com/cilium/cilium-cli/k8s" "github.com/cilium/cilium-cli/utils/features" @@ -491,6 +492,60 @@ func (he httpEndpoint) FlowFilters() []*flow.FlowFilter { return nil } +type LRPFrontend struct { + name string + ip string + port string +} + +func NewLRPFrontend(frontend ciliumv2.RedirectFrontend) *LRPFrontend { + var lf LRPFrontend + if f := frontend.AddressMatcher; f != nil { + lf.ip = f.IP + lf.port = f.ToPorts[0].Port + + return &lf + } + + return nil +} + +func (l LRPFrontend) Name() string { + return l.name +} + +func (l LRPFrontend) Scheme() string { + return "" +} + +func (l LRPFrontend) Path() string { + return "" +} + +func (l LRPFrontend) Address(features.IPFamily) string { + return l.ip +} + +func (l LRPFrontend) Port() uint32 { + p, err := strconv.Atoi(l.port) + if err != nil { + return 0 + } + return uint32(p) +} + +func (l LRPFrontend) HasLabel(string, string) bool { + return false +} + +func (l LRPFrontend) Labels() map[string]string { + return nil +} + +func (l LRPFrontend) FlowFilters() []*flow.FlowFilter { + return nil +} + // EchoIPPod is a Kubernetes Pod that prints back the client IP, acting as a peer in a connectivity test. type EchoIPPod struct { Pod diff --git a/connectivity/check/policy.go b/connectivity/check/policy.go index c6b4c9e9a0..a05734ca52 100644 --- a/connectivity/check/policy.go +++ b/connectivity/check/policy.go @@ -13,9 +13,6 @@ import ( "sync" "time" - flowpb "github.com/cilium/cilium/api/v1/flow" - ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" - "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/scheme" networkingv1 "k8s.io/api/networking/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,6 +20,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" clientsetscheme "k8s.io/client-go/kubernetes/scheme" + flowpb "github.com/cilium/cilium/api/v1/flow" + ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/scheme" + "github.com/cilium/cilium-cli/defaults" "github.com/cilium/cilium-cli/k8s" ) @@ -447,6 +448,11 @@ func (t *Test) addCEGPs(cegps ...*ciliumv2.CiliumEgressGatewayPolicy) (err error return err } +func (t *Test) addCLRPs(clrps ...*ciliumv2.CiliumLocalRedirectPolicy) (err error) { + t.clrps, err = RegisterPolicy(t.clrps, clrps...) + return err +} + func sumMap(m map[string]int) int { sum := 0 for _, v := range m { @@ -722,3 +728,9 @@ func parseK8SPolicyYAML(policy string) (policies []*networkingv1.NetworkPolicy, func parseCiliumEgressGatewayPolicyYAML(policy string) (cegps []*ciliumv2.CiliumEgressGatewayPolicy, err error) { return ParsePolicyYAML[*ciliumv2.CiliumEgressGatewayPolicy](policy, scheme.Scheme) } + +// parseCiliumLocalRedirectPolicyYAML decodes policy yaml into a slice of +// CiliumLocalRedirectPolicies. +func parseCiliumLocalRedirectPolicyYAML(policy string) (clrp []*ciliumv2.CiliumLocalRedirectPolicy, err error) { + return ParsePolicyYAML[*ciliumv2.CiliumLocalRedirectPolicy](policy, scheme.Scheme) +} diff --git a/connectivity/check/test.go b/connectivity/check/test.go index 5bd9a16949..c539aa6106 100644 --- a/connectivity/check/test.go +++ b/connectivity/check/test.go @@ -14,10 +14,6 @@ import ( "time" "github.com/blang/semver/v4" - k8sConst "github.com/cilium/cilium/pkg/k8s/apis/cilium.io" - ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" - "github.com/cilium/cilium/pkg/policy/api" - "github.com/cilium/cilium/pkg/versioncheck" "github.com/cloudflare/cfssl/cli/genkey" "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/csr" @@ -29,6 +25,11 @@ import ( networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sConst "github.com/cilium/cilium/pkg/k8s/apis/cilium.io" + ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + "github.com/cilium/cilium/pkg/policy/api" + "github.com/cilium/cilium/pkg/versioncheck" + "github.com/cilium/cilium-cli/defaults" "github.com/cilium/cilium-cli/sysdump" "github.com/cilium/cilium-cli/utils/features" @@ -121,6 +122,9 @@ type Test struct { // Cilium Egress Gateway Policies active during this test. cegps map[string]*ciliumv2.CiliumEgressGatewayPolicy + // Cilium Local Redirect Policies active during this test. + clrps map[string]*ciliumv2.CiliumLocalRedirectPolicy + // Secrets that have to be present during the test. secrets map[string]*corev1.Secret @@ -537,6 +541,41 @@ func (t *Test) WithK8SPolicy(policy string) *Test { return t } +// CiliumLocalRedirectPolicyParams is used to configure a CiliumLocalRedirectPolicy template. +type CiliumLocalRedirectPolicyParams struct { + // Name controls the name of the policy + Name string + + // SkipRedirectFromBackend is the flag set in the policy spec. + SkipRedirectFromBackend bool +} + +func (t *Test) WithCiliumLocalRedirectPolicy(params CiliumLocalRedirectPolicyParams) *Test { + pl, err := parseCiliumLocalRedirectPolicyYAML(params.Name) + if err != nil { + t.Fatalf("Parsing local redirect policy YAML: %s", err) + } + + // Change the default test namespace as required. + for i := range pl { + pl[i].Namespace = t.ctx.params.TestNamespace + + // Set the policy name + pl[i].Name = params.Name + + // Set the flag from params. + pl[i].Spec.SkipRedirectFromBackend = params.SkipRedirectFromBackend + } + + if err := t.addCLRPs(pl...); err != nil { + t.Fatalf("Adding CLRPs to cilium local redirect policy context: %s", err) + } + + t.WithFeatureRequirements(features.RequireEnabled(features.LocalRedirectPolicy)) + + return t +} + type ExcludedCIDRsKind int const ( @@ -935,3 +974,7 @@ func (t *Test) CiliumClusterwideNetworkPolicies() map[string]*ciliumv2.CiliumClu func (t *Test) KubernetesNetworkPolicies() map[string]*networkingv1.NetworkPolicy { return t.knps } + +func (t *Test) CiliumLocalRedirectPolicies() map[string]*ciliumv2.CiliumLocalRedirectPolicy { + return t.clrps +} diff --git a/connectivity/tests/lrp.go b/connectivity/tests/lrp.go new file mode 100644 index 0000000000..1273453a65 --- /dev/null +++ b/connectivity/tests/lrp.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package tests + +import ( + "context" + "fmt" + + "github.com/cilium/cilium-cli/connectivity/check" + "github.com/cilium/cilium-cli/utils/features" +) + +// LRP runs test scenarios for local redirect policy. It tests local redirection +// connectivity from test source pods to LRP frontend. +// +// It tests connectivity with the configured skipRedirectFromBackend flag for: +// - client pods to LRP frontend +// - LRP backend pods to LRP frontend +func LRP(skipRedirectFromBackend bool) check.Scenario { + return lrp{skipRedirectFromBackend: skipRedirectFromBackend} +} + +type lrp struct { + skipRedirectFromBackend bool +} + +func (s lrp) Name() string { + if s.skipRedirectFromBackend { + return "local-redirect-policy-skip-redirect-from-backend" + } + return "local-redirect-policy" +} + +func (s lrp) Run(ctx context.Context, t *check.Test) { + ct := t.Context() + + // Tests client pods to LRP frontend connectivity. Traffic gets redirected + // to the LRP backends. + for _, pod := range t.Context().ClientPods() { + pod := pod + + for _, policy := range t.CiliumLocalRedirectPolicies() { + policy := policy + + i := 0 + lf := check.NewLRPFrontend(policy.Spec.RedirectFrontend) + t.NewAction(s, fmt.Sprintf("curl-%d", i), &pod, lf, features.IPFamilyV4).Run(func(a *check.Action) { + a.ExecInPod(ctx, ct.CurlCommand(lf, features.IPFamilyV4)) + i++ + }) + } + } + + // Tests LRP backend pods to LRP frontend connectivity. Traffic gets redirected + // based on the configured skipRedirectFromBackend flag. + for _, pod := range t.Context().LrpBackendPods() { + pod := pod + + for _, policy := range t.CiliumLocalRedirectPolicies() { + policy := policy + + i := 0 + lf := check.NewLRPFrontend(policy.Spec.RedirectFrontend) + t.NewAction(s, fmt.Sprintf("curl-%d", i), &pod, lf, features.IPFamilyV4).Run(func(a *check.Action) { + a.ExecInPod(ctx, ct.CurlCommand(lf, features.IPFamilyV4)) + + // TODO (aditi): Wrap this under policy.Spec.SkipRedirectFromBackend check + // a.ValidateFlows(ctx, pod, a.GetEgressRequirements(check.FlowParameters{ + // AltDstIP: lf.Address(features.IPFamilyV4), + // AltDstPort: lf.Port(), + // })) + i++ + }) + } + + } + +}