Skip to content

Commit 26cf119

Browse files
committed
feat(network): add percentage-based IP selection
Implements consistent hashing for selecting subsets of IPs based on percentage configuration in network disruptions. Enables deterministic IP selection across chaos pods. - Add SelectIPsByPercentage with SHA256-based hashing - Ensure consistent selection using seed parameter - Add comprehensive test coverage for edge cases - Add example demonstrating DNS resolver control Jira: CHAOSPLT-259
1 parent 10db19f commit 26cf119

18 files changed

+989
-68
lines changed

api/disruption_kind.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type DisruptionArgs struct {
3131
MetricsSink string
3232
DisruptionName string
3333
DisruptionNamespace string
34+
DisruptionUID string
3435
TargetName string
3536
TargetNodeName string
3637
ChaosNamespace string
@@ -63,6 +64,7 @@ func (d DisruptionArgs) CreateCmdArgs(args []string) []string {
6364
// log context args
6465
"--log-context-disruption-name", d.DisruptionName,
6566
"--log-context-disruption-namespace", d.DisruptionNamespace,
67+
"--log-context-disruption-uid", d.DisruptionUID,
6668
"--log-context-target-name", d.TargetName,
6769
"--log-context-target-node-name", d.TargetNodeName,
6870
)

api/v1beta1/network_disruption.go

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,19 @@ type NetworkDisruptionHostSpec struct {
107107
ConnState string `json:"connState,omitempty" chaos_validate:"omitempty,oneofci=new est"`
108108
// +kubebuilder:validation:Enum=pod;node;pod-fallback-node;node-fallback-pod;""
109109
DNSResolver string `json:"dnsResolver,omitempty" chaos_validate:"omitempty,oneofci=pod node pod-fallback-node node-fallback-pod"`
110+
// +kubebuilder:validation:Minimum=1
111+
// +kubebuilder:validation:Maximum=100
112+
Percentage *int `json:"percentage,omitempty" chaos_validate:"omitempty,gte=1,lte=100"`
110113
}
111114

112115
type NetworkDisruptionServiceSpec struct {
113116
Name string `json:"name"`
114117
Namespace string `json:"namespace"`
115118
// +optional
116119
Ports []NetworkDisruptionServicePortSpec `json:"ports,omitempty" chaos_validate:"omitempty,dive"`
120+
// +kubebuilder:validation:Minimum=1
121+
// +kubebuilder:validation:Maximum=100
122+
Percentage *int `json:"percentage,omitempty" chaos_validate:"omitempty,gte=1,lte=100"`
117123
}
118124

119125
type NetworkDisruptionServicePortSpec struct {
@@ -358,20 +364,20 @@ func (s *NetworkDisruptionSpec) GenerateArgs() []string {
358364

359365
// append hosts
360366
for _, host := range s.Hosts {
361-
if host.DNSResolver == "" {
362-
args = append(args, "--hosts", fmt.Sprintf("%s;%d;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState))
363-
} else {
364-
args = append(args, "--hosts", fmt.Sprintf("%s;%d;%s;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState, host.DNSResolver))
367+
hostStr := fmt.Sprintf("%s;%d;%s;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState, host.DNSResolver)
368+
if host.Percentage != nil {
369+
hostStr = fmt.Sprintf("%s;%d", hostStr, *host.Percentage)
365370
}
371+
args = append(args, "--hosts", hostStr)
366372
}
367373

368374
// append allowed hosts
369375
for _, host := range s.AllowedHosts {
370-
if host.DNSResolver == "" {
371-
args = append(args, "--allowed-hosts", fmt.Sprintf("%s;%d;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState))
372-
} else {
373-
args = append(args, "--allowed-hosts", fmt.Sprintf("%s;%d;%s;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState, host.DNSResolver))
376+
hostStr := fmt.Sprintf("%s;%d;%s;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState, host.DNSResolver)
377+
if host.Percentage != nil {
378+
hostStr = fmt.Sprintf("%s;%d", hostStr, *host.Percentage)
374379
}
380+
args = append(args, "--allowed-hosts", hostStr)
375381
}
376382

377383
// append services
@@ -381,7 +387,11 @@ func (s *NetworkDisruptionSpec) GenerateArgs() []string {
381387
ports += fmt.Sprintf(";%d-%s", port.Port, port.Name)
382388
}
383389

384-
args = append(args, "--services", fmt.Sprintf("%s;%s%s", service.Name, service.Namespace, ports))
390+
serviceStr := fmt.Sprintf("%s;%s%s", service.Name, service.Namespace, ports)
391+
if service.Percentage != nil {
392+
serviceStr = fmt.Sprintf("%s;%d", serviceStr, *service.Percentage)
393+
}
394+
args = append(args, "--services", serviceStr)
385395
}
386396

387397
if s.HTTP != nil {
@@ -607,7 +617,7 @@ func (s *NetworkDisruptionCloudSpec) Explain() []string {
607617
}
608618

609619
// NetworkDisruptionHostSpecFromString parses the given hosts to host specs
610-
// The expected format for hosts is <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>
620+
// The expected format for hosts is <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>;<percentage>
611621
func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHostSpec, error) {
612622
var err error
613623

@@ -620,9 +630,10 @@ func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHos
620630
flow := ""
621631
connState := ""
622632
dnsResolver := ""
633+
var percentage *int
623634

624-
// parse host with format <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>
625-
parsedHost := strings.SplitN(host, ";", 6)
635+
// parse host with format <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>;<percentage>
636+
parsedHost := strings.SplitN(host, ";", 7)
626637

627638
// cast port to int if specified
628639
if len(parsedHost) > 1 && parsedHost[1] != "" {
@@ -652,6 +663,15 @@ func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHos
652663
dnsResolver = parsedHost[5]
653664
}
654665

666+
// get percentage if specified
667+
if len(parsedHost) > 6 && parsedHost[6] != "" {
668+
pct, err := strconv.Atoi(parsedHost[6])
669+
if err != nil {
670+
return nil, fmt.Errorf("unexpected percentage parameter in %s: %w", host, err)
671+
}
672+
percentage = &pct
673+
}
674+
655675
// generate host spec
656676
parsedHosts = append(parsedHosts, NetworkDisruptionHostSpec{
657677
Host: parsedHost[0],
@@ -660,28 +680,44 @@ func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHos
660680
Flow: flow,
661681
ConnState: connState,
662682
DNSResolver: dnsResolver,
683+
Percentage: percentage,
663684
})
664685
}
665686

666687
return parsedHosts, nil
667688
}
668689

669690
// NetworkDisruptionServiceSpecFromString parses the given services to service specs
670-
// The expected format for services is <serviceName>;<serviceNamespace>
691+
// The expected format for services is <serviceName>;<serviceNamespace>;<port-value>-<port-name>;<port-value>-<port-name>...;<percentage>
692+
// The percentage field is optional and should be a plain integer at the end (no dash)
671693
func NetworkDisruptionServiceSpecFromString(services []string) ([]NetworkDisruptionServiceSpec, error) {
672694
parsedServices := []NetworkDisruptionServiceSpec{}
673695

674696
// parse given services
675697
for _, service := range services {
676-
// parse service with format <name>;<namespace>;<port-value>-<port-name>;<port-value>-<port-name>...
698+
// parse service with format <name>;<namespace>;<port-value>-<port-name>;<port-value>-<port-name>...;<percentage>
677699
parsedService := strings.Split(service, ";")
678700
if len(parsedService) < 2 {
679-
return nil, fmt.Errorf("service format is expected to follow '<name>;<namespace>;<port-value>-<port-name>;<port-value>-<port-name>', unexpected format detected: %s", service)
701+
return nil, fmt.Errorf("service format is expected to follow '<name>;<namespace>;<port-value>-<port-name>;<port-value>-<port-name>;<percentage>', unexpected format detected: %s", service)
680702
}
681703

682704
ports := []NetworkDisruptionServicePortSpec{}
705+
var percentage *int
706+
portFields := parsedService[2:]
707+
708+
// Check if the last field is a percentage (plain integer without dash)
709+
if len(portFields) > 0 {
710+
lastField := portFields[len(portFields)-1]
711+
if !strings.Contains(lastField, "-") {
712+
// This might be a percentage
713+
if pct, err := strconv.Atoi(lastField); err == nil {
714+
percentage = &pct
715+
portFields = portFields[:len(portFields)-1] // Remove percentage from port fields
716+
}
717+
}
718+
}
683719

684-
for _, unparsedPort := range parsedService[2:] {
720+
for _, unparsedPort := range portFields {
685721
// <port-value>-<port-name>
686722
portValue, portName, ok := strings.Cut(unparsedPort, "-")
687723
if !ok {
@@ -701,9 +737,10 @@ func NetworkDisruptionServiceSpecFromString(services []string) ([]NetworkDisrupt
701737

702738
// generate service spec
703739
parsedServices = append(parsedServices, NetworkDisruptionServiceSpec{
704-
Name: parsedService[0],
705-
Namespace: parsedService[1],
706-
Ports: ports,
740+
Name: parsedService[0],
741+
Namespace: parsedService[1],
742+
Ports: ports,
743+
Percentage: percentage,
707744
})
708745
}
709746

api/v1beta1/network_disruption_test.go

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -617,9 +617,9 @@ var _ = Describe("NetworkDisruptionSpec", func() {
617617
"--bandwidth-limit",
618618
"6",
619619
"--hosts",
620-
"lorem;8080;TCP;ingress;open",
620+
"lorem;8080;TCP;ingress;open;",
621621
"--allowed-hosts",
622-
"localhost;9090;UDP;egress;closed",
622+
"localhost;9090;UDP;egress;closed;",
623623
"--services",
624624
"name;namespace;9191-default",
625625
}
@@ -722,7 +722,7 @@ var _ = Describe("NetworkDisruptionSpec", func() {
722722
"name;namespace;9191-default",
723723
},
724724
),
725-
Entry("with DNSResolver empty (backward compatibility)",
725+
Entry("with DNSResolver empty (always includes trailing semicolon)",
726726
func() NetworkDisruptionSpec {
727727
networkDisruption := defaultNetworkDisruption.DeepCopy()
728728
networkDisruption.Hosts[0].DNSResolver = ""
@@ -745,12 +745,103 @@ var _ = Describe("NetworkDisruptionSpec", func() {
745745
"--bandwidth-limit",
746746
"6",
747747
"--hosts",
748-
"lorem;8080;TCP;ingress;open",
748+
"lorem;8080;TCP;ingress;open;",
749749
"--allowed-hosts",
750-
"localhost;9090;UDP;egress;closed",
750+
"localhost;9090;UDP;egress;closed;",
751751
"--services",
752752
"name;namespace;9191-default",
753753
},
754+
),
755+
Entry("with percentage set on hosts",
756+
func() NetworkDisruptionSpec {
757+
networkDisruption := defaultNetworkDisruption.DeepCopy()
758+
pct50 := 50
759+
networkDisruption.Hosts[0].Percentage = &pct50
760+
761+
return *networkDisruption
762+
}(),
763+
[]string{
764+
"network-disruption",
765+
"--corrupt",
766+
"3",
767+
"--drop",
768+
"1",
769+
"--duplicate",
770+
"2",
771+
"--delay",
772+
"4",
773+
"--delay-jitter",
774+
"5",
775+
"--bandwidth-limit",
776+
"6",
777+
"--hosts",
778+
"lorem;8080;TCP;ingress;open;;50",
779+
"--allowed-hosts",
780+
"localhost;9090;UDP;egress;closed;",
781+
"--services",
782+
"name;namespace;9191-default",
783+
},
784+
),
785+
Entry("with percentage and DNSResolver set on hosts",
786+
func() NetworkDisruptionSpec {
787+
networkDisruption := defaultNetworkDisruption.DeepCopy()
788+
pct30 := 30
789+
networkDisruption.Hosts[0].DNSResolver = "pod"
790+
networkDisruption.Hosts[0].Percentage = &pct30
791+
792+
return *networkDisruption
793+
}(),
794+
[]string{
795+
"network-disruption",
796+
"--corrupt",
797+
"3",
798+
"--drop",
799+
"1",
800+
"--duplicate",
801+
"2",
802+
"--delay",
803+
"4",
804+
"--delay-jitter",
805+
"5",
806+
"--bandwidth-limit",
807+
"6",
808+
"--hosts",
809+
"lorem;8080;TCP;ingress;open;pod;30",
810+
"--allowed-hosts",
811+
"localhost;9090;UDP;egress;closed;",
812+
"--services",
813+
"name;namespace;9191-default",
814+
},
815+
),
816+
Entry("with percentage set on services",
817+
func() NetworkDisruptionSpec {
818+
networkDisruption := defaultNetworkDisruption.DeepCopy()
819+
pct75 := 75
820+
networkDisruption.Services[0].Percentage = &pct75
821+
822+
return *networkDisruption
823+
}(),
824+
[]string{
825+
"network-disruption",
826+
"--corrupt",
827+
"3",
828+
"--drop",
829+
"1",
830+
"--duplicate",
831+
"2",
832+
"--delay",
833+
"4",
834+
"--delay-jitter",
835+
"5",
836+
"--bandwidth-limit",
837+
"6",
838+
"--hosts",
839+
"lorem;8080;TCP;ingress;open;",
840+
"--allowed-hosts",
841+
"localhost;9090;UDP;egress;closed;",
842+
"--services",
843+
"name;namespace;9191-default;75",
844+
},
754845
))
755846
})
756847
})

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chart/templates/generated/chaos.datadoghq.com_disruptioncrons.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ spec:
283283
type: string
284284
host:
285285
type: string
286+
percentage:
287+
maximum: 100
288+
minimum: 1
289+
type: integer
286290
port:
287291
maximum: 65535
288292
minimum: 0
@@ -431,6 +435,10 @@ spec:
431435
type: string
432436
host:
433437
type: string
438+
percentage:
439+
maximum: 100
440+
minimum: 1
441+
type: integer
434442
port:
435443
maximum: 65535
436444
minimum: 0
@@ -464,6 +472,10 @@ spec:
464472
type: string
465473
namespace:
466474
type: string
475+
percentage:
476+
maximum: 100
477+
minimum: 1
478+
type: integer
467479
ports:
468480
items:
469481
properties:

0 commit comments

Comments
 (0)