Skip to content

Commit 6f36370

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 1a19143 commit 6f36370

19 files changed

+1066
-54
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: 66 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,24 @@ 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+
369+
if host.Percentage != nil {
370+
hostStr = fmt.Sprintf("%s;%d", hostStr, *host.Percentage)
365371
}
372+
373+
args = append(args, "--hosts", hostStr)
366374
}
367375

368376
// append allowed hosts
369377
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))
378+
hostStr := fmt.Sprintf("%s;%d;%s;%s;%s;%s", host.Host, host.Port, host.Protocol, host.Flow, host.ConnState, host.DNSResolver)
379+
380+
if host.Percentage != nil {
381+
hostStr = fmt.Sprintf("%s;%d", hostStr, *host.Percentage)
374382
}
383+
384+
args = append(args, "--allowed-hosts", hostStr)
375385
}
376386

377387
// append services
@@ -381,7 +391,13 @@ func (s *NetworkDisruptionSpec) GenerateArgs() []string {
381391
ports += fmt.Sprintf(";%d-%s", port.Port, port.Name)
382392
}
383393

384-
args = append(args, "--services", fmt.Sprintf("%s;%s%s", service.Name, service.Namespace, ports))
394+
serviceStr := fmt.Sprintf("%s;%s%s", service.Name, service.Namespace, ports)
395+
396+
if service.Percentage != nil {
397+
serviceStr = fmt.Sprintf("%s;%d", serviceStr, *service.Percentage)
398+
}
399+
400+
args = append(args, "--services", serviceStr)
385401
}
386402

387403
if s.HTTP != nil {
@@ -607,7 +623,7 @@ func (s *NetworkDisruptionCloudSpec) Explain() []string {
607623
}
608624

609625
// NetworkDisruptionHostSpecFromString parses the given hosts to host specs
610-
// The expected format for hosts is <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>
626+
// The expected format for hosts is <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>;<percentage>
611627
func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHostSpec, error) {
612628
var err error
613629

@@ -621,8 +637,10 @@ func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHos
621637
connState := ""
622638
dnsResolver := ""
623639

624-
// parse host with format <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>
625-
parsedHost := strings.SplitN(host, ";", 6)
640+
var percentage *int
641+
642+
// parse host with format <host>;<port>;<protocol>;<flow>;<connState>;<dnsResolver>;<percentage>
643+
parsedHost := strings.SplitN(host, ";", 7)
626644

627645
// cast port to int if specified
628646
if len(parsedHost) > 1 && parsedHost[1] != "" {
@@ -652,6 +670,16 @@ func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHos
652670
dnsResolver = parsedHost[5]
653671
}
654672

673+
// get percentage if specified
674+
if len(parsedHost) > 6 && parsedHost[6] != "" {
675+
pct, err := strconv.Atoi(parsedHost[6])
676+
if err != nil {
677+
return nil, fmt.Errorf("unexpected percentage parameter in %s: %w", host, err)
678+
}
679+
680+
percentage = &pct
681+
}
682+
655683
// generate host spec
656684
parsedHosts = append(parsedHosts, NetworkDisruptionHostSpec{
657685
Host: parsedHost[0],
@@ -660,28 +688,46 @@ func NetworkDisruptionHostSpecFromString(hosts []string) ([]NetworkDisruptionHos
660688
Flow: flow,
661689
ConnState: connState,
662690
DNSResolver: dnsResolver,
691+
Percentage: percentage,
663692
})
664693
}
665694

666695
return parsedHosts, nil
667696
}
668697

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

674704
// parse given services
675705
for _, service := range services {
676-
// parse service with format <name>;<namespace>;<port-value>-<port-name>;<port-value>-<port-name>...
706+
// parse service with format <name>;<namespace>;<port-value>-<port-name>;<port-value>-<port-name>...;<percentage>
677707
parsedService := strings.Split(service, ";")
678708
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)
709+
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)
680710
}
681711

682712
ports := []NetworkDisruptionServicePortSpec{}
683713

684-
for _, unparsedPort := range parsedService[2:] {
714+
var percentage *int
715+
716+
portFields := parsedService[2:]
717+
718+
// Check if the last field is a percentage (plain integer without dash)
719+
if len(portFields) > 0 {
720+
lastField := portFields[len(portFields)-1]
721+
if !strings.Contains(lastField, "-") {
722+
// This might be a percentage
723+
if pct, err := strconv.Atoi(lastField); err == nil {
724+
percentage = &pct
725+
portFields = portFields[:len(portFields)-1] // Remove percentage from port fields
726+
}
727+
}
728+
}
729+
730+
for _, unparsedPort := range portFields {
685731
// <port-value>-<port-name>
686732
portValue, portName, ok := strings.Cut(unparsedPort, "-")
687733
if !ok {
@@ -701,9 +747,10 @@ func NetworkDisruptionServiceSpecFromString(services []string) ([]NetworkDisrupt
701747

702748
// generate service spec
703749
parsedServices = append(parsedServices, NetworkDisruptionServiceSpec{
704-
Name: parsedService[0],
705-
Namespace: parsedService[1],
706-
Ports: ports,
750+
Name: parsedService[0],
751+
Namespace: parsedService[1],
752+
Ports: ports,
753+
Percentage: percentage,
707754
})
708755
}
709756

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)