diff --git a/build/charts/antrea/crds/packetsampling.yaml b/build/charts/antrea/crds/packetsampling.yaml new file mode 100644 index 00000000000..d673dac5e7f --- /dev/null +++ b/build/charts/antrea/crds/packetsampling.yaml @@ -0,0 +1,180 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps diff --git a/build/charts/antrea/templates/agent/clusterrole.yaml b/build/charts/antrea/templates/agent/clusterrole.yaml index 05ca58c1ab6..60e174dedcf 100644 --- a/build/charts/antrea/templates/agent/clusterrole.yaml +++ b/build/charts/antrea/templates/agent/clusterrole.yaml @@ -154,6 +154,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/build/charts/antrea/templates/controller/clusterrole.yaml b/build/charts/antrea/templates/controller/clusterrole.yaml index f16ff1b517b..1a082cdcca6 100644 --- a/build/charts/antrea/templates/controller/clusterrole.yaml +++ b/build/charts/antrea/templates/controller/clusterrole.yaml @@ -216,6 +216,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml index 494c88f78c4..520f5a30932 100644 --- a/build/yamls/antrea-aks.yml +++ b/build/yamls/antrea-aks.yml @@ -4413,6 +4413,189 @@ spec: # Deprecated shortName and shall be removed in Antrea v1.14.0 - anp +--- +# Source: crds/packetsampling.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps + --- # Source: crds/supportbundlecollection.yaml apiVersion: apiextensions.k8s.io/v1 @@ -6218,6 +6401,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: @@ -6630,6 +6826,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/build/yamls/antrea-crds.yml b/build/yamls/antrea-crds.yml index a215b73db1e..cf52bfb2f79 100644 --- a/build/yamls/antrea-crds.yml +++ b/build/yamls/antrea-crds.yml @@ -4394,6 +4394,187 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: supportbundlecollections.crd.antrea.io spec: diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml index bf31133d0f5..5aa59c71bde 100644 --- a/build/yamls/antrea-eks.yml +++ b/build/yamls/antrea-eks.yml @@ -4413,6 +4413,189 @@ spec: # Deprecated shortName and shall be removed in Antrea v1.14.0 - anp +--- +# Source: crds/packetsampling.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps + --- # Source: crds/supportbundlecollection.yaml apiVersion: apiextensions.k8s.io/v1 @@ -6218,6 +6401,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: @@ -6630,6 +6826,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml index 8ef0f76ffe2..45a34187bdc 100644 --- a/build/yamls/antrea-gke.yml +++ b/build/yamls/antrea-gke.yml @@ -4413,6 +4413,189 @@ spec: # Deprecated shortName and shall be removed in Antrea v1.14.0 - anp +--- +# Source: crds/packetsampling.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps + --- # Source: crds/supportbundlecollection.yaml apiVersion: apiextensions.k8s.io/v1 @@ -6218,6 +6401,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: @@ -6630,6 +6826,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml index 8c644ea62ad..6d678c7a288 100644 --- a/build/yamls/antrea-ipsec.yml +++ b/build/yamls/antrea-ipsec.yml @@ -4413,6 +4413,189 @@ spec: # Deprecated shortName and shall be removed in Antrea v1.14.0 - anp +--- +# Source: crds/packetsampling.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps + --- # Source: crds/supportbundlecollection.yaml apiVersion: apiextensions.k8s.io/v1 @@ -6231,6 +6414,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: @@ -6643,6 +6839,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml index 4c297d211a1..65af54a9a0e 100644 --- a/build/yamls/antrea.yml +++ b/build/yamls/antrea.yml @@ -4413,6 +4413,189 @@ spec: # Deprecated shortName and shall be removed in Antrea v1.14.0 - anp +--- +# Source: crds/packetsampling.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packetsamplings.crd.antrea.io + labels: + app: antrea +spec: + group: crd.antrea.io + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - jsonPath: .status.phase + description: The phase of the Traceflow. + name: Phase + type: string + - jsonPath: .spec.source.pod + description: The name of the source Pod. + name: Source-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.pod + description: The name of the destination Pod. + name: Destination-Pod + type: string + priority: 10 + - jsonPath: .spec.destination.ip + description: The IP address of the destination. + name: Destination-IP + type: string + priority: 10 + - jsonPath: .spec.timeout + description: Timeout in seconds. + name: Timeout + type: integer + priority: 10 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + properties: + source: + type: object + properties: + pod: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + destination: + type: object + properties: + pod: + type: string + service: + type: string + namespace: + type: string + ip: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + packet: + type: object + properties: + ipHeader: + type: object + properties: + srcIP: + type: string + oneOf: + - format: ipv4 + - format: ipv6 + protocol: + type: integer + ttl: + type: integer + flags: + type: integer + ipv6Header: + type: object + properties: + srcIP: + type: string + format: ipv6 + nextHeader: + type: integer + hopLimit: + type: integer + transportHeader: + type: object + properties: + icmp: + type: object + properties: + id: + type: integer + sequence: + type: integer + udp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + tcp: + type: object + properties: + srcPort: + type: integer + dstPort: + type: integer + flags: + type: integer + timeout: + type: integer + type: + type: string + firstNSamplingConfig: + type: object + properties: + number: + type: integer + fileServer: + type: object + properties: + url: + type: string + authentication: + type: object + properties: + authType: + type: string + enum: [ "BearerToken", "APIKey", "BasicAuthentication" ] + authSecret: + type: object + properties: + name: + type: string + namespace: + type: string + status: + type: object + properties: + reason: + type: string + dataplaneTag: + type: integer + phase: + type: string + startTime: + type: string + numCapturedPackets: + type: integer + packetsPath: + type: string + + subresources: + status: {} + scope: Cluster + names: + plural: packetsamplings + singular: packetsampling + kind: PacketSampling + shortNames: + - ps + --- # Source: crds/supportbundlecollection.yaml apiVersion: apiextensions.k8s.io/v1 @@ -6218,6 +6401,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: @@ -6630,6 +6826,19 @@ rules: - patch - create - delete + - apiGroups: + - crd.antrea.io + resources: + - packetsamplings + - packetsamplings/status + verbs: + - get + - watch + - list + - update + - patch + - create + - delete - apiGroups: - crd.antrea.io resources: diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go index 6c30874d141..3c8aab8fa2c 100644 --- a/cmd/antrea-agent/agent.go +++ b/cmd/antrea-agent/agent.go @@ -41,6 +41,7 @@ import ( "antrea.io/antrea/pkg/agent/controller/ipseccertificate" "antrea.io/antrea/pkg/agent/controller/networkpolicy" "antrea.io/antrea/pkg/agent/controller/noderoute" + "antrea.io/antrea/pkg/agent/controller/packetsampling" "antrea.io/antrea/pkg/agent/controller/serviceexternalip" "antrea.io/antrea/pkg/agent/controller/traceflow" "antrea.io/antrea/pkg/agent/controller/trafficcontrol" @@ -111,6 +112,7 @@ func run(o *Options) error { informerFactory := informers.NewSharedInformerFactory(k8sClient, informerDefaultResync) crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, informerDefaultResync) traceflowInformer := crdInformerFactory.Crd().V1beta1().Traceflows() + packetSamplingInformer := crdInformerFactory.Crd().V1alpha1().PacketSamplings() egressInformer := crdInformerFactory.Crd().V1beta1().Egresses() externalIPPoolInformer := crdInformerFactory.Crd().V1beta1().ExternalIPPools() trafficControlInformer := crdInformerFactory.Crd().V1alpha2().TrafficControls() @@ -596,6 +598,22 @@ func run(o *Options) error { o.enableAntreaProxy) } + var packetSamplingController *packetsampling.Controller + if features.DefaultFeatureGate.Enabled(features.PacketSampling) { + packetSamplingController = packetsampling.NewPacketSamplingController( + k8sClient, + crdClient, + serviceInformer, + packetSamplingInformer, + ofClient, + ifaceStore, + networkConfig, + nodeConfig, + serviceCIDRNet, + o.enableAntreaProxy, + ) + } + // TODO: we should call this after installing flows for initial node routes // and initial NetworkPolicies so that no packets will be mishandled. if err := agentInitializer.FlowRestoreComplete(); err != nil { @@ -735,6 +753,10 @@ func run(o *Options) error { go traceflowController.Run(stopCh) } + if features.DefaultFeatureGate.Enabled(features.PacketSampling) { + go packetSamplingController.Run(stopCh) + } + if o.enableAntreaProxy { go proxier.GetProxyProvider().Run(stopCh) diff --git a/cmd/antrea-controller/controller.go b/cmd/antrea-controller/controller.go index bdd0b79ed5e..6b83417a308 100644 --- a/cmd/antrea-controller/controller.go +++ b/cmd/antrea-controller/controller.go @@ -59,6 +59,7 @@ import ( "antrea.io/antrea/pkg/controller/metrics" "antrea.io/antrea/pkg/controller/networkpolicy" "antrea.io/antrea/pkg/controller/networkpolicy/store" + "antrea.io/antrea/pkg/controller/packetsampling" "antrea.io/antrea/pkg/controller/querier" "antrea.io/antrea/pkg/controller/serviceexternalip" "antrea.io/antrea/pkg/controller/stats" @@ -144,6 +145,7 @@ func run(o *Options) error { annpInformer := crdInformerFactory.Crd().V1beta1().NetworkPolicies() tierInformer := crdInformerFactory.Crd().V1beta1().Tiers() tfInformer := crdInformerFactory.Crd().V1beta1().Traceflows() + psInformer := crdInformerFactory.Crd().V1alpha1().PacketSamplings() cgInformer := crdInformerFactory.Crd().V1beta1().ClusterGroups() grpInformer := crdInformerFactory.Crd().V1beta1().Groups() egressInformer := crdInformerFactory.Crd().V1beta1().Egresses() @@ -261,6 +263,11 @@ func run(o *Options) error { traceflowController = traceflow.NewTraceflowController(crdClient, podInformer, tfInformer) } + var packetSamplingController *packetsampling.Controller + if features.DefaultFeatureGate.Enabled(features.PacketSampling) { + packetSamplingController = packetsampling.NewPacketSamplingController(crdClient, podInformer, psInformer) + } + // statsAggregator takes stats summaries from antrea-agents, aggregates them, and serves the Stats APIs with the // aggregated data. For now it's only used for NetworkPolicy stats. var statsAggregator *stats.Aggregator @@ -304,6 +311,7 @@ func run(o *Options) error { statsAggregator, bundleCollectionController, traceflowController, + packetSamplingController, *o.config.EnablePrometheusMetrics, cipherSuites, cipher.TLSVersionMap[o.config.TLSMinVersion]) @@ -378,6 +386,10 @@ func run(o *Options) error { go traceflowController.Run(stopCh) } + if features.DefaultFeatureGate.Enabled(features.PacketSampling) { + go packetSamplingController.Run(stopCh) + } + if features.DefaultFeatureGate.Enabled(features.AntreaPolicy) { go networkPolicyStatusController.Run(stopCh) } @@ -497,6 +509,7 @@ func createAPIServerConfig(kubeconfig string, statsAggregator *stats.Aggregator, bundleCollectionStore *supportbundlecollection.Controller, traceflowController *traceflow.Controller, + packetSamplingController *packetsampling.Controller, enableMetrics bool, cipherSuites []uint16, tlsMinVersion uint16) (*apiserver.Config, error) { @@ -564,5 +577,6 @@ func createAPIServerConfig(kubeconfig string, npController, egressController, bundleCollectionStore, - traceflowController), nil + traceflowController, + packetSamplingController), nil } diff --git a/go.mod b/go.mod index 567022d8e22..290d606347a 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,8 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.3 github.com/google/btree v1.1.2 - github.com/google/uuid v1.5.0 + github.com/google/gopacket v1.1.19 + github.com/google/uuid v1.4.0 github.com/hashicorp/memberlist v0.5.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.3.0 github.com/k8snetworkplumbingwg/sriov-cni v2.1.0+incompatible diff --git a/go.sum b/go.sum index 58defc66092..46eab531ae6 100644 --- a/go.sum +++ b/go.sum @@ -377,6 +377,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -387,8 +389,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -861,6 +863,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/pkg/agent/controller/packetsampling/packetin.go b/pkg/agent/controller/packetsampling/packetin.go new file mode 100644 index 00000000000..c11aa827830 --- /dev/null +++ b/pkg/agent/controller/packetsampling/packetin.go @@ -0,0 +1,166 @@ +// Copyright 2024 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packetsampling + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/gopacket" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + + "antrea.io/libOpenflow/protocol" + "antrea.io/libOpenflow/util" + "antrea.io/ofnet/ofctrl" + + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" +) + +func (c *Controller) HandlePacketIn(pktIn *ofctrl.PacketIn) error { + klog.V(4).Infof("PacketIn for PacketSampling: %+v", pktIn.PacketIn) + if !c.packetSamplingSynced() { + return errors.New("PacketSampling controller is not started") + } + oldPS, samplingState, shouldSkip, err := c.parsePacketIn(pktIn) + if err != nil { + return fmt.Errorf("parsePacketIn error: %v", err) + } + if shouldSkip { + return nil + } + + // Retry when update CRD conflict which caused by multiple agents updating one CRD at same time. + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + ps, err := c.packetSamplingInformer.Lister().Get(oldPS.Name) + if err != nil { + return fmt.Errorf("get packetsampling failed: %w", err) + } + + if samplingState != nil { + shouldUpdate := samplingState.shouldSyncPackets && (samplingState.updateRateLimiter.Allow() || samplingState.numCapturedPackets == samplingState.maxNumCapturedPackets) + if !shouldUpdate { + return nil + } + } + + update := ps.DeepCopy() + if samplingState != nil { + update.Status.NumCapturedPackets = samplingState.numCapturedPackets + } + + _, err = c.crdClient.CrdV1alpha1().PacketSamplings().UpdateStatus(context.TODO(), update, v1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("update Traceflow failed: %w", err) + } + klog.InfoS("Updated packetsampling", "ps", klog.KObj(ps), "status", update.Status) + return nil + }) + if err != nil { + return fmt.Errorf("PacketSampling update error: %w", err) + } + + if samplingState != nil { + rawData := pktIn.Data.(*util.Buffer).Bytes() + + ci := gopacket.CaptureInfo{ + Timestamp: time.Now(), + CaptureLength: len(rawData), + Length: len(rawData), + } + err = samplingState.pcapngWriter.WritePacket(ci, rawData) + if err != nil { + return fmt.Errorf("couldn't write packet: %w", err) + } + + if samplingState.numCapturedPackets == oldPS.Spec.FirstNSamplingConfig.Number && samplingState.shouldSyncPackets { + return c.uploadPacketsFile(oldPS) + } + } + return nil +} + +// parsePacketIn parses the packet-in message and returns +// 1. the sampling state of the PacketSampling (on sampling mode), +func (c *Controller) parsePacketIn(pktIn *ofctrl.PacketIn) (_ *crdv1alpha1.PacketSampling, _ *packetSamplingState, shouldSkip bool, _ error) { + + // Get data plane tag. + // Directly read data plane tag from packet. + var err error + var tag uint8 + + etherData := new(protocol.Ethernet) + if err := etherData.UnmarshalBinary(pktIn.Data.(*util.Buffer).Bytes()); err != nil { + return nil, nil, false, fmt.Errorf("failed to parse Ethernet packet from packet-in message: %v", err) + } + + samplingState := packetSamplingState{} + + if etherData.Ethertype == protocol.IPv4_MSG { + ipPacket, ok := etherData.Data.(*protocol.IPv4) + if !ok { + return nil, nil, false, fmt.Errorf("invalid packetsampling ipv4 packet") + + } + tag = ipPacket.DSCP + } else if etherData.Ethertype == protocol.IPv6_MSG { + ipv6Packet, ok := etherData.Data.(*protocol.IPv6) + if !ok { + return nil, nil, false, fmt.Errorf("invalid packetsampling ipv6 packet") + } + tag = ipv6Packet.TrafficClass >> 2 + } else { + return nil, nil, false, fmt.Errorf("unsupported traceflow packet Ethertype: %d", etherData.Ethertype) + } + + c.runningPacketSamplingsMutex.Lock() + psState, exists := c.runningPacketSamplings[tag] + if exists { + if psState.numCapturedPackets == psState.maxNumCapturedPackets { + c.runningPacketSamplingsMutex.Unlock() + return nil, nil, true, nil + } + psState.numCapturedPackets++ + if psState.numCapturedPackets == psState.maxNumCapturedPackets { + c.ofClient.UninstallPacketSamplingFlows(tag) + } + samplingState = *psState + + } + c.runningPacketSamplingsMutex.Unlock() + if !exists { + return nil, nil, false, fmt.Errorf("PacketSampling for dataplane tag %d not found in cache", tag) + } + + ps, err := c.packetSamplingLister.Get(psState.name) + if err != nil { + return nil, nil, false, fmt.Errorf("failed to get PacketSampling %s CRD: %v", psState.name, err) + } + + return ps, &samplingState, false, nil + +} + +func (c *Controller) uploadPacketsFile(ps *crdv1alpha1.PacketSampling) error { + name := uidToPath(string(ps.UID)) + file, err := defaultFS.Open(name) + if err != nil { + return err + } + return c.uploadPackets(ps, file) +} diff --git a/pkg/agent/controller/packetsampling/packetsampling_controller.go b/pkg/agent/controller/packetsampling/packetsampling_controller.go new file mode 100644 index 00000000000..bba2bf44a81 --- /dev/null +++ b/pkg/agent/controller/packetsampling/packetsampling_controller.go @@ -0,0 +1,763 @@ +package packetsampling + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/url" + "os" + "path" + "runtime" + "sync" + "time" + + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcapgo" + "github.com/pkg/sftp" + "github.com/spf13/afero" + "golang.org/x/crypto/ssh" + "golang.org/x/time/rate" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + coreinformers "k8s.io/client-go/informers/core/v1" + clientset "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "antrea.io/libOpenflow/protocol" + + "antrea.io/antrea/pkg/agent/config" + "antrea.io/antrea/pkg/agent/interfacestore" + "antrea.io/antrea/pkg/agent/openflow" + "antrea.io/antrea/pkg/agent/util" + "antrea.io/antrea/pkg/apis/controlplane" + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + clientsetversioned "antrea.io/antrea/pkg/client/clientset/versioned" + crdinformers "antrea.io/antrea/pkg/client/informers/externalversions/crd/v1alpha1" + crdlisters "antrea.io/antrea/pkg/client/listers/crd/v1alpha1" + binding "antrea.io/antrea/pkg/ovs/openflow" +) + +type StorageProtocolType string + +const ( + sftpProtocol StorageProtocolType = "sftp" + + uploadToFileServerTries = 5 + uploadToFileServerRetryDelay = 5 * time.Second + + secretKeyWithAPIKey = "apikey" + secretKeyWithBearerToken = "token" + secretKeyWithUsername = "username" + secretKeyWithPassword = "password" +) + +const ( + controllerName = "AntreaAgentPacketSamplingController" + resyncPeriod time.Duration = 0 + + minRetryDelay = 5 * time.Second + maxRetryDelay = 300 * time.Second + + defaultWorkers = 4 +) + +const ( + samplingStatusUpdatePeriod = 10 * time.Second + packetDirectoryUnix = "/tmp/packetsampling/packets" + packetDirectoryWindows = "C:\\packetsampling\\packets" +) + +var ( + packetDirectory = getPacketDirectory() + defaultFS = afero.NewOsFs() +) + +func getPacketDirectory() string { + if runtime.GOOS == "windows" { + return packetDirectoryWindows + } else { + return packetDirectoryUnix + } +} + +type packetSamplingState struct { + name string + tag uint8 + shouldSyncPackets bool + numCapturedPackets int32 + maxNumCapturedPackets int32 + updateRateLimiter *rate.Limiter + uid string + pcapngFile *os.File + pcapngWriter *pcapgo.NgWriter + receiverOnly bool + isSender bool +} + +type Controller struct { + kubeClient clientset.Interface + crdClient clientsetversioned.Interface + serviceLister corelisters.ServiceLister + serviceListerSynced cache.InformerSynced + packetSamplingInformer crdinformers.PacketSamplingInformer + packetSamplingLister crdlisters.PacketSamplingLister + packetSamplingSynced cache.InformerSynced + + ofClient openflow.Client + interfaceStore interfacestore.InterfaceStore + networkConfig *config.NetworkConfig + nodeConfig *config.NodeConfig + serviceCIDR *net.IPNet + queue workqueue.RateLimitingInterface + + runningPacketSamplingsMutex sync.RWMutex + runningPacketSamplings map[uint8]*packetSamplingState + + enableAntreaProxy bool + + sftpUploader uploader +} + +func NewPacketSamplingController( + kubeClient clientset.Interface, + crdClient clientsetversioned.Interface, + serviceInformer coreinformers.ServiceInformer, + packetSamplingInformer crdinformers.PacketSamplingInformer, + client openflow.Client, + interfaceStore interfacestore.InterfaceStore, + networkConfig *config.NetworkConfig, + nodeConfig *config.NodeConfig, + serviceCIDR *net.IPNet, + enableAntreaProxy bool, +) *Controller { + c := &Controller{ + kubeClient: kubeClient, + crdClient: crdClient, + packetSamplingInformer: packetSamplingInformer, + packetSamplingLister: packetSamplingInformer.Lister(), + packetSamplingSynced: packetSamplingInformer.Informer().HasSynced, + ofClient: client, + interfaceStore: interfaceStore, + networkConfig: networkConfig, + nodeConfig: nodeConfig, + serviceCIDR: serviceCIDR, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "packetsampling"), + runningPacketSamplings: make(map[uint8]*packetSamplingState), + sftpUploader: &sftpUploader{}, + enableAntreaProxy: enableAntreaProxy, + } + + packetSamplingInformer.Informer().AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ + AddFunc: c.addPacketSampling, + UpdateFunc: c.updatePacketSampling, + DeleteFunc: c.deletePacketSampling, + }, resyncPeriod) + + c.ofClient.RegisterPacketInHandler(uint8(openflow.PacketInCategoryPS), c) + + if c.enableAntreaProxy { + c.serviceLister = serviceInformer.Lister() + c.serviceListerSynced = serviceInformer.Informer().HasSynced + } + return c + +} + +func (c *Controller) enqueuePacketSampling(ps *crdv1alpha1.PacketSampling) { + c.queue.Add(ps.Name) +} + +// Run will create defaultWorkers workers (go routines) which will process the PacketSampling events from the +// workqueue. +func (c *Controller) Run(stopCh <-chan struct{}) { + defer c.queue.ShutDown() + + klog.Infof("Starting %s", controllerName) + defer klog.Infof("Shutting down %s", controllerName) + + cacheSynced := []cache.InformerSynced{c.packetSamplingSynced} + if c.enableAntreaProxy { + cacheSynced = append(cacheSynced, c.serviceListerSynced) + } + if !cache.WaitForNamedCacheSync(controllerName, stopCh, cacheSynced...) { + return + } + + err := os.MkdirAll(packetDirectory, 0755) + if err != nil { + klog.ErrorS(err, "Couldn't create directory for storing sampling packets") + return + } + + for i := 0; i < defaultWorkers; i++ { + go wait.Until(c.worker, time.Second, stopCh) + } + <-stopCh +} + +func (c *Controller) addPacketSampling(obj interface{}) { + ps := obj.(*crdv1alpha1.PacketSampling) + klog.Infof("Processing PacketSampling %s ADD event", ps.Name) + c.enqueuePacketSampling(ps) +} + +func (c *Controller) updatePacketSampling(_, obj interface{}) { + ps := obj.(*crdv1alpha1.PacketSampling) + klog.Infof("Processing PacketSampling %s UPDATE EVENT", ps.Name) + c.enqueuePacketSampling(ps) +} + +func (c *Controller) deletePacketSampling(obj interface{}) { + ps := obj.(*crdv1alpha1.PacketSampling) + klog.Infof("Processing PacketSampling %s DELETE event", ps.Name) + + err := deletePcapngFile(string(ps.UID)) + if err != nil { + klog.ErrorS(err, "Couldn't delete pcapng file") + + } + c.enqueuePacketSampling(ps) + +} + +func deletePcapngFile(uid string) error { + return os.Remove(uidToPath(uid)) +} + +func uidToPath(uid string) string { + return path.Join(packetDirectory, uid+".pcapng") +} + +func (c *Controller) worker() { + for c.processPacketSamplingItem() { + + } +} + +func (c *Controller) processPacketSamplingItem() bool { + obj, quit := c.queue.Get() + if quit { + return false + } + + defer c.queue.Done(obj) + + if key, ok := obj.(string); !ok { + c.queue.Forget(obj) + klog.Errorf("Expected string in work queue but got %#v", obj) + return true + } else if err := c.syncPacketSampling(key); err == nil { + c.queue.Forget(key) + } else { + klog.Errorf("Error syncing PacketSampling %s, existing. Error: %v", key, err) + } + return true +} + +func (c *Controller) validatePacketSampling(ps *crdv1alpha1.PacketSampling) error { + if ps.Spec.Destination.Service != "" && !c.enableAntreaProxy { + return errors.New("using Service destination requires AntreaPolicy feature gate") + } + + if ps.Spec.Destination.IP != "" { + destIP := net.ParseIP(ps.Spec.Destination.IP) + if destIP == nil { + return fmt.Errorf("destination IP %s is not valid", ps.Spec.Destination.IP) + } + if !c.enableAntreaProxy && c.serviceCIDR.Contains(destIP) { + return errors.New("using ClusterIP destination requires AntreaPolicy feature gate") + } + + } + return nil +} + +func (c *Controller) errorPacketSamplingCRD(ps *crdv1alpha1.PacketSampling, reason string) (*crdv1alpha1.PacketSampling, error) { + ps.Status.Phase = crdv1alpha1.PacketSamplingFailed + + type PacketSampling struct { + Status crdv1alpha1.PacketSamplingStatus `json:"status,omitempty"` + } + + patchData := PacketSampling{ + Status: crdv1alpha1.PacketSamplingStatus{Phase: ps.Status.Phase, Reason: reason}, + } + payloads, _ := json.Marshal(patchData) + return c.crdClient.CrdV1alpha1().PacketSamplings().Patch(context.TODO(), ps.Name, types.MergePatchType, payloads, metav1.PatchOptions{}, "status") + +} + +func (c *Controller) cleanupPacketSampling(psName string) { + psState := c.deletePacketSamplingState(psName) + if psState != nil { + err := c.ofClient.UninstallPacketSamplingFlows(psState.tag) + if err != nil { + klog.Errorf("Error cleaning up flows for PacketSampling %s: %v", psName, err) + } + + err = psState.pcapngWriter.Flush() + if err != nil { + klog.Errorf("Error flushing pcapng file for PacketSampling %s: %v", psName, err) + } + err = psState.pcapngFile.Close() + if err != nil { + klog.Errorf("Error closing pcapng file for PacketSampling %s: %v", psName, err) + } + } +} + +func (c *Controller) deletePacketSamplingState(psName string) *packetSamplingState { + c.runningPacketSamplingsMutex.Lock() + defer c.runningPacketSamplingsMutex.Unlock() + + for tag, state := range c.runningPacketSamplings { + if state.name == psName { + delete(c.runningPacketSamplings, tag) + return state + } + } + return nil +} + +func (c *Controller) startPacketSampling(ps *crdv1alpha1.PacketSampling) error { + err := c.validatePacketSampling(ps) + + func() { + if err != nil { + c.cleanupPacketSampling(ps.Name) + c.errorPacketSamplingCRD(ps, fmt.Sprintf("Node: %s, error:%+v", c.nodeConfig.Name, err)) + + } + }() + + if err != nil { + return err + } + + receiverOnly := false + senderOnly := true + var pod, ns string + if ps.Spec.Source.Pod != "" { + pod = ps.Spec.Source.Pod + ns = ps.Spec.Source.Namespace + if ps.Spec.Destination.Pod != "" { + senderOnly = false + } + } else { + receiverOnly = true + pod = ps.Spec.Destination.Pod + ns = ps.Spec.Destination.Namespace + } + + podInterfaces := c.interfaceStore.GetContainerInterfacesByPod(pod, ns) + isSender := !receiverOnly && len(podInterfaces) > 0 + + var matchPacket *binding.Packet + var ofPort uint32 + + if len(podInterfaces) > 0 { + packet, err := c.preparePacket(ps, podInterfaces[0], receiverOnly) + if err != nil { + return err + } + ofPort = uint32(podInterfaces[0].OFPort) + matchPacket = packet + klog.V(2).Infof("PacketSampling packet: %v", *packet) + } + + c.runningPacketSamplingsMutex.Lock() + psState := packetSamplingState{ + name: ps.Name, tag: uint8(ps.Status.DataplaneTag), + receiverOnly: receiverOnly, isSender: isSender, + maxNumCapturedPackets: ps.Spec.FirstNSamplingConfig.Number, + } + + exists, err := fileExists(string(ps.UID)) + if err != nil { + return fmt.Errorf("couldn't check if the file exists: %w", err) + + } + if exists { + return fmt.Errorf("packet file already exists. this may be due to an unexpected termination") + } + + file, err := createPcapngFile(string(ps.UID)) + if err != nil { + return fmt.Errorf("couldn't craete pcapng file: %w", err) + } + + writer, err := pcapgo.NewNgWriter(file, layers.LinkTypeEthernet) + if err != nil { + return fmt.Errorf("couldn't init pcap writer: %w", err) + } + + if ps.Spec.Destination.Pod != "" { + pod = ps.Spec.Destination.Pod + ns = ps.Spec.Destination.Namespace + } else { + pod = ps.Spec.Source.Pod + ns = ps.Spec.Source.Namespace + } + podInterfaces = c.interfaceStore.GetContainerInterfacesByPod(pod, ns) + + shouldSyncPackets := len(podInterfaces) > 0 + klog.Infof("fk-1: %v, %s,%s", shouldSyncPackets, ns, pod) + psState.shouldSyncPackets = shouldSyncPackets + psState.uid = string(ps.UID) + psState.pcapngFile = file + psState.pcapngWriter = writer + + if psState.shouldSyncPackets { + psState.updateRateLimiter = rate.NewLimiter(rate.Every(samplingStatusUpdatePeriod), 1) + } + + c.runningPacketSamplings[psState.tag] = &psState + c.runningPacketSamplingsMutex.Unlock() + + klog.V(2).Infof("installing flow entries to packetsampling %s", ps.Name) + timeout := ps.Spec.Timeout + if timeout == 0 { + timeout = crdv1alpha1.DefaultPacketSamplingTimeout + } + fmt.Println(senderOnly) + err = c.ofClient.InstallPacketSamplingFlows(psState.tag, senderOnly, receiverOnly, matchPacket, ofPort, timeout) + return err + +} + +func createPcapngFile(uid string) (*os.File, error) { + return os.Create(uidToPath(uid)) +} + +func fileExists(uid string) (bool, error) { + _, err := os.Stat(uidToPath(uid)) + if err == nil { + return true, nil + } else { + if os.IsNotExist(err) { + return false, nil + } else { + return false, err + } + } +} + +func (c *Controller) preparePacket(ps *crdv1alpha1.PacketSampling, intf *interfacestore.InterfaceConfig, receiverOnly bool) (*binding.Packet, error) { + isICMP := false + packet := new(binding.Packet) + packet.IsIPv6 = ps.Spec.Packet.IPv6Header != nil + + if receiverOnly { + if ps.Spec.Source.IP != "" { + packet.SourceIP = net.ParseIP(ps.Spec.Source.IP) + isIPv6 := packet.SourceIP.To4() == nil + if isIPv6 != packet.IsIPv6 { + return nil, errors.New("source IP does not match the IP header family") + } + } + packet.DestinationMAC = intf.MAC + } else if ps.Spec.Destination.IP != "" { + packet.DestinationIP = net.ParseIP(ps.Spec.Destination.IP) + if packet.DestinationIP == nil { + return nil, errors.New("destination IP is not valid") + } + isIPv6 := packet.DestinationIP.To4() == nil + if isIPv6 != packet.IsIPv6 { + return nil, errors.New("destination IP does not match the IP header family") + } + } else if ps.Spec.Destination.Pod != "" { + dstPodInterfaces := c.interfaceStore.GetContainerInterfacesByPod(ps.Spec.Destination.Pod, ps.Spec.Destination.Namespace) + + if len(dstPodInterfaces) > 0 { + if packet.IsIPv6 { + packet.DestinationIP = dstPodInterfaces[0].GetIPv6Addr() + } else { + packet.DestinationIP = dstPodInterfaces[0].GetIPv4Addr() + } + } else { + dstPod, err := c.kubeClient.CoreV1().Pods(ps.Spec.Destination.Namespace).Get(context.TODO(), ps.Spec.Destination.Pod, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get the destination pod %s/%s: %v", ps.Spec.Destination.Namespace, ps.Spec.Destination.Pod, err) + } + podIPs := make([]net.IP, len(dstPod.Status.PodIPs)) + for i, ip := range dstPod.Status.PodIPs { + podIPs[i] = net.ParseIP(ip.IP) + } + if packet.IsIPv6 { + packet.DestinationIP, _ = util.GetIPWithFamily(podIPs, util.FamilyIPv6) + } else { + packet.DestinationIP = util.GetIPv4Addr(podIPs) + } + } + if packet.DestinationIP == nil { + if packet.IsIPv6 { + return nil, errors.New("destination Pod does not have an IPv6 address") + } + return nil, errors.New("destination Pod does not have an IPv4 address") + } + } else if ps.Spec.Destination.Service != "" { + dstSvc, err := c.serviceLister.Services(ps.Spec.Destination.Namespace).Get(ps.Spec.Destination.Service) + + if err != nil { + return nil, fmt.Errorf("failed to get the destination service %s/%s: %v", ps.Spec.Destination.Namespace, ps.Spec.Destination.Service, err) + } + if dstSvc.Spec.ClusterIP == "" { + return nil, errors.New("destination Service does not have an ClusterIP") + } + + packet.DestinationIP = net.ParseIP(dstSvc.Spec.ClusterIP) + if !packet.IsIPv6 { + packet.DestinationIP = packet.DestinationIP.To4() + if packet.DestinationIP == nil { + return nil, errors.New("destination Service does not have an IPv4 address") + } + } else if packet.DestinationIP.To4() != nil { + return nil, errors.New("destination Service does not have an IPv6 address") + } + } else { + return nil, errors.New("destination is not specified") + } + + if ps.Spec.Packet.IPv6Header != nil { + if ps.Spec.Packet.IPv6Header.NextHeader != nil { + packet.IPProto = uint8(*ps.Spec.Packet.IPv6Header.NextHeader) + } + } else if ps.Spec.Packet.IPHeader.Protocol != 0 { + packet.IPProto = uint8(ps.Spec.Packet.IPHeader.Protocol) + } + + if ps.Spec.Packet.TransportHeader.TCP != nil { + packet.IPProto = protocol.Type_TCP + packet.SourcePort = uint16(ps.Spec.Packet.TransportHeader.TCP.SrcPort) + packet.DestinationPort = uint16(ps.Spec.Packet.TransportHeader.TCP.DstPort) + if ps.Spec.Packet.TransportHeader.TCP.Flags != 0 { + packet.TCPFlags = uint8(ps.Spec.Packet.TransportHeader.TCP.Flags) + } + } else if ps.Spec.Packet.TransportHeader.UDP != nil { + packet.IPProto = protocol.Type_UDP + packet.SourcePort = uint16(ps.Spec.Packet.TransportHeader.UDP.SrcPort) + packet.DestinationPort = uint16(ps.Spec.Packet.TransportHeader.UDP.DstPort) + } else if ps.Spec.Packet.TransportHeader.ICMP != nil { + isICMP = true + } + + if packet.IPProto == protocol.Type_ICMP || packet.IPProto == protocol.Type_IPv6ICMP { + isICMP = true + } + + if isICMP { + if packet.IsIPv6 { + packet.IPProto = protocol.Type_IPv6ICMP + + } else { + packet.IPProto = protocol.Type_ICMP + } + } + + return packet, nil +} + +func (c *Controller) syncPacketSampling(psName string) error { + startTime := time.Now() + defer func() { + klog.V(4).Infof("Finished syncing PacketSampling for %s. (%v)", psName, time.Since(startTime)) + }() + + ps, err := c.packetSamplingLister.Get(psName) + if err != nil { + if apierrors.IsNotFound(err) { + c.cleanupPacketSampling(psName) + return nil + } + return err + } + + switch ps.Status.Phase { + case crdv1alpha1.PacketSamplingRunning: + if ps.Status.DataplaneTag != 0 { + start := false + c.runningPacketSamplingsMutex.Lock() + if _, ok := c.runningPacketSamplings[uint8(ps.Status.DataplaneTag)]; !ok { + start = true + } + c.runningPacketSamplingsMutex.Unlock() + if start { + err = c.startPacketSampling(ps) + } + } else { + klog.Warningf("Invalid data plane tag %d for packet %s", ps.Status.DataplaneTag, ps.Name) + } + default: + c.cleanupPacketSampling(psName) + } + return err + +} + +type uploader interface { + upload(addr string, path string, config *ssh.ClientConfig, tarGzFile io.Reader) error +} + +type sftpUploader struct { +} + +func (uploader *sftpUploader) upload(address string, path string, config *ssh.ClientConfig, packetFile io.Reader) error { + conn, err := ssh.Dial("tcp", address, config) + if err != nil { + return fmt.Errorf("error when connecting to fs server: %w", err) + } + sftpClient, err := sftp.NewClient(conn) + if err != nil { + return fmt.Errorf("error when setting up sftp client: %w", err) + } + defer func() { + if err := sftpClient.Close(); err != nil { + klog.ErrorS(err, "Error when closing sftp client") + } + }() + targetFile, err := sftpClient.Create(path) + if err != nil { + return fmt.Errorf("error when creating target file on remote: %v", err) + } + defer func() { + if err := targetFile.Close(); err != nil { + klog.ErrorS(err, "Error when closing target file on remote") + } + }() + if written, err := io.Copy(targetFile, packetFile); err != nil { + return fmt.Errorf("error when copying target file: %v, written: %d", err, written) + } + klog.InfoS("Successfully upload file to path", "filePath", path) + return nil +} + +func (c *Controller) getUploaderByProtocol(protocol StorageProtocolType) (uploader, error) { + if protocol == sftpProtocol { + return c.sftpUploader, nil + } + return nil, fmt.Errorf("unsupported protocol %s", protocol) +} + +func parseUploadUrl(uploadUrl string) (*url.URL, error) { + parsedURL, err := url.Parse(uploadUrl) + if err != nil { + parsedURL, err = url.Parse("sftp://" + uploadUrl) + if err != nil { + return nil, err + } + } + if parsedURL.Scheme != "sftp" { + return nil, fmt.Errorf("not sftp protocol") + } + return parsedURL, nil +} + +// parseBundleAuth returns the authentication from the Secret provided in BundleServerAuthConfiguration. +// The authentication is stored in the Secret Data with a key decided by the AuthType, and encoded using base64. +func (c *Controller) parseBundleAuth(authentication crdv1alpha1.BundleServerAuthConfiguration) (*controlplane.BundleServerAuthConfiguration, error) { + secretReference := authentication.AuthSecret + if secretReference == nil { + return nil, fmt.Errorf("authentication is not specified") + } + secret, err := c.kubeClient.CoreV1().Secrets(secretReference.Namespace).Get(context.TODO(), secretReference.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("unable to get Secret with name %s in Namespace %s: %v", secretReference.Name, secretReference.Namespace, err) + } + parseAuthValue := func(secretData map[string][]byte, key string) (string, error) { + authValue, found := secret.Data[key] + if !found { + return "", fmt.Errorf("not found authentication in Secret %s/%s with key %s", secretReference.Namespace, secretReference.Name, key) + } + return bytes.NewBuffer(authValue).String(), nil + } + switch authentication.AuthType { + case crdv1alpha1.APIKey: + value, err := parseAuthValue(secret.Data, secretKeyWithAPIKey) + if err != nil { + return nil, err + } + return &controlplane.BundleServerAuthConfiguration{ + APIKey: value, + }, nil + case crdv1alpha1.BearerToken: + value, err := parseAuthValue(secret.Data, secretKeyWithBearerToken) + if err != nil { + return nil, err + } + return &controlplane.BundleServerAuthConfiguration{ + BearerToken: value, + }, nil + case crdv1alpha1.BasicAuthentication: + username, err := parseAuthValue(secret.Data, secretKeyWithUsername) + if err != nil { + return nil, err + } + password, err := parseAuthValue(secret.Data, secretKeyWithPassword) + if err != nil { + return nil, err + } + return &controlplane.BundleServerAuthConfiguration{ + BasicAuthentication: &controlplane.BasicAuthentication{ + Username: username, + Password: password, + }, + }, nil + } + return nil, fmt.Errorf("unsupported authentication type %s", authentication.AuthType) +} + +func (c *Controller) uploadToFileServer(up uploader, uid string, parsedURL *url.URL, serverAuth *controlplane.BundleServerAuthConfiguration, packetFile io.Reader) error { + joinedPath := path.Join(parsedURL.Path, c.nodeConfig.Name+"_"+uid+".pcapng") + cfg := &ssh.ClientConfig{ + User: serverAuth.BasicAuthentication.Username, + Auth: []ssh.AuthMethod{ssh.Password(serverAuth.BasicAuthentication.Password)}, + // #nosec G106: skip host key check here and users can specify their own checks if needed + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: time.Second, + } + return up.upload(parsedURL.Host, joinedPath, cfg, packetFile) +} + +func (c *Controller) uploadPackets(ps *crdv1alpha1.PacketSampling, outputFile afero.File) error { + klog.V(2).InfoS("Uploading captured packets for packetsampling", "name", ps.Name) + uploader, err := c.getUploaderByProtocol(sftpProtocol) + if err != nil { + return fmt.Errorf("failed to upload support bundle while getting uploader: %v", err) + } + if _, err := outputFile.Seek(0, 0); err != nil { + return fmt.Errorf("failed to upload sampled packets to file server while setting offset: %v", err) + } + // fileServer.URL should be like: 10.92.23.154:22/path or sftp://10.92.23.154:22/path + parsedURL, err := parseUploadUrl(ps.Spec.FileServer.URL) + if err != nil { + return fmt.Errorf("failed to upload packets while parsing upload URL: %v", err) + } + triesLeft := uploadToFileServerTries + var uploadErr error + authentication, err := c.parseBundleAuth(ps.Spec.Authentication) + if err != nil { + klog.ErrorS(err, "Failed to get authentication defined in the PacketSampling CR", "name", ps.Name, "authentication", ps.Spec.Authentication) + return err + } + for triesLeft > 0 { + if uploadErr = c.uploadToFileServer(uploader, string(ps.UID), parsedURL, authentication, outputFile); uploadErr == nil { + return nil + } + triesLeft-- + if triesLeft == 0 { + return fmt.Errorf("failed to upload support bundle after %d attempts", uploadToFileServerTries) + } + klog.InfoS("Failed to upload sampled packets", "UploadError", uploadErr, "TriesLeft", triesLeft) + time.Sleep(uploadToFileServerRetryDelay) + } + return nil +} diff --git a/pkg/agent/controller/packetsampling/packetsampling_controller_test.go b/pkg/agent/controller/packetsampling/packetsampling_controller_test.go new file mode 100644 index 00000000000..bfb17445e29 --- /dev/null +++ b/pkg/agent/controller/packetsampling/packetsampling_controller_test.go @@ -0,0 +1,263 @@ +// Copyright 2024 Antrea Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packetsampling + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/util/workqueue" + + "antrea.io/libOpenflow/protocol" + + "antrea.io/antrea/pkg/agent/config" + "antrea.io/antrea/pkg/agent/interfacestore" + openflowtest "antrea.io/antrea/pkg/agent/openflow/testing" + "antrea.io/antrea/pkg/agent/util" + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + fakeversioned "antrea.io/antrea/pkg/client/clientset/versioned/fake" + crdinformers "antrea.io/antrea/pkg/client/informers/externalversions" + binding "antrea.io/antrea/pkg/ovs/openflow" + "antrea.io/antrea/pkg/util/k8s" +) + +var ( + pod1IPv4 = "192.168.10.10" + pod2IPv4 = "192.168.11.10" + // dstIPv4 = "192.168.99.99" + pod1MAC, _ = net.ParseMAC("aa:bb:cc:dd:ee:0f") + pod2MAC, _ = net.ParseMAC("aa:bb:cc:dd:ee:00") + ofPortPod1 = uint32(1) + ofPortPod2 = uint32(2) + // protocolICMPv6 = int32(58) + + pod1 = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "default", + }, + Status: v1.PodStatus{ + PodIP: pod1IPv4, + }, + } + pod2 = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: "default", + }, + Status: v1.PodStatus{ + PodIP: pod2IPv4, + }, + } + pod3 = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-3", + Namespace: "default", + }, + } +) + +type fakePacketSamplingController struct { + *Controller + kubeClient kubernetes.Interface + mockController *gomock.Controller + mockOFClient *openflowtest.MockClient + crdClient *fakeversioned.Clientset + crdInformerFactory crdinformers.SharedInformerFactory +} + +func newFakePacketSamplingController(t *testing.T, initObjects []runtime.Object, networkConfig *config.NetworkConfig, nodeConfig *config.NodeConfig) *fakePacketSamplingController { + controller := gomock.NewController(t) + kubeClient := fake.NewSimpleClientset(&pod1, &pod2, &pod3) + mockOFClient := openflowtest.NewMockClient(controller) + crdClient := fakeversioned.NewSimpleClientset(initObjects...) + crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, 0) + packetSamplingInformer := crdInformerFactory.Crd().V1alpha1().PacketSamplings() + + ifaceStore := interfacestore.NewInterfaceStore() + addPodInterface(ifaceStore, pod1.Namespace, pod1.Name, pod1IPv4, pod1MAC.String(), int32(ofPortPod1)) + addPodInterface(ifaceStore, pod2.Namespace, pod2.Name, pod2IPv4, pod2MAC.String(), int32(ofPortPod2)) + + _, serviceCIDRNet, _ := net.ParseCIDR("10.96.0.0/12") + + tfController := &Controller{ + kubeClient: kubeClient, + crdClient: crdClient, + packetSamplingInformer: packetSamplingInformer, + packetSamplingLister: packetSamplingInformer.Lister(), + packetSamplingSynced: packetSamplingInformer.Informer().HasSynced, + ofClient: mockOFClient, + interfaceStore: ifaceStore, + networkConfig: networkConfig, + nodeConfig: nodeConfig, + serviceCIDR: serviceCIDRNet, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "PacketSampling"), + runningPacketSamplings: make(map[uint8]*packetSamplingState), + } + + return &fakePacketSamplingController{ + Controller: tfController, + kubeClient: kubeClient, + mockController: controller, + mockOFClient: mockOFClient, + crdClient: crdClient, + crdInformerFactory: crdInformerFactory, + } +} + +func addPodInterface(ifaceStore interfacestore.InterfaceStore, podNamespace, podName, podIP, podMac string, ofPort int32) { + containerName := k8s.NamespacedName(podNamespace, podName) + ifIPs := []net.IP{net.ParseIP(podIP)} + mac, _ := net.ParseMAC(podMac) + ifaceStore.AddInterface(&interfacestore.InterfaceConfig{ + IPs: ifIPs, + MAC: mac, + InterfaceName: util.GenerateContainerInterfaceName(podName, podNamespace, containerName), + ContainerInterfaceConfig: &interfacestore.ContainerInterfaceConfig{PodName: podName, PodNamespace: podNamespace, ContainerID: containerName}, + OVSPortConfig: &interfacestore.OVSPortConfig{OFPort: ofPort}, + }) +} + +func TestErrPacketSamplingCRD(t *testing.T) { + ps := &crdv1alpha1.PacketSampling{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ps", + UID: "uid", + }, + Spec: crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: pod1.Namespace, + Pod: pod1.Name, + }, + Destination: crdv1alpha1.Destination{ + Namespace: pod2.Namespace, + Pod: pod2.Name, + }, + }, + Status: crdv1alpha1.PacketSamplingStatus{ + Phase: crdv1alpha1.PacketSamplingRunning, + DataplaneTag: 1, + }, + } + expectedPS := ps + reason := "failed" + expectedPS.Status.Phase = crdv1alpha1.PacketSamplingFailed + expectedPS.Status.Reason = reason + + psc := newFakePacketSamplingController(t, []runtime.Object{ps}, nil, nil) + + gotPS, err := psc.errorPacketSamplingCRD(ps, reason) + require.NoError(t, err) + assert.Equal(t, expectedPS, gotPS) +} + +func TestPreparePacket(t *testing.T) { + pss := []struct { + name string + ps *crdv1alpha1.PacketSampling + intf *interfacestore.InterfaceConfig + receiverOnly bool + expectedPacket *binding.Packet + expectedErr string + }{ + { + name: "invalid destination IPv4", + ps: &crdv1alpha1.PacketSampling{ + ObjectMeta: metav1.ObjectMeta{Name: "ps1", UID: "uid1"}, + Spec: crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: pod1.Namespace, + Pod: pod1.Name, + }, + Destination: crdv1alpha1.Destination{ + IP: "1.1.1.300", + }, + }, + }, + expectedErr: "destination IP is not valid", + }, + { + name: "empty destination", + ps: &crdv1alpha1.PacketSampling{ + ObjectMeta: metav1.ObjectMeta{Name: "ps2", UID: "uid2"}, + Spec: crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: pod1.Namespace, + Pod: pod1.Name, + }, + }, + }, + expectedErr: "destination is not specified", + }, + { + name: "tcp packet", + ps: &crdv1alpha1.PacketSampling{ + ObjectMeta: metav1.ObjectMeta{Name: "ps3", UID: "uid3"}, + Spec: crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: pod1.Namespace, + Pod: pod1.Name, + }, + Destination: crdv1alpha1.Destination{ + Namespace: pod2.Namespace, + Pod: pod2.Name, + }, + Packet: crdv1alpha1.Packet{ + TransportHeader: crdv1alpha1.TransportHeader{ + TCP: &crdv1alpha1.TCPHeader{ + SrcPort: 80, + DstPort: 81, + Flags: 11, + }, + }, + }, + }, + }, + expectedPacket: &binding.Packet{ + DestinationIP: net.ParseIP(pod2IPv4), + IPProto: protocol.Type_TCP, + SourcePort: 80, + DestinationPort: 81, + TCPFlags: 11, + }, + }, + } + for _, ps := range pss { + t.Run(ps.name, func(t *testing.T) { + tfc := newFakePacketSamplingController(t, []runtime.Object{ps.ps}, nil, nil) + podInterfaces := tfc.interfaceStore.GetContainerInterfacesByPod(pod1.Name, pod1.Namespace) + if ps.intf != nil { + podInterfaces[0] = ps.intf + } + + pkt, err := tfc.preparePacket(ps.ps, podInterfaces[0], ps.receiverOnly) + if ps.expectedErr == "" { + require.NoError(t, err) + assert.Equal(t, ps.expectedPacket, pkt) + } else { + assert.ErrorContains(t, err, ps.expectedErr) + assert.Nil(t, pkt) + } + }) + } +} diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go index 209e1049c4e..9b8f9134e1a 100644 --- a/pkg/agent/openflow/client.go +++ b/pkg/agent/openflow/client.go @@ -19,6 +19,8 @@ import ( "math/rand" "net" + "antrea.io/antrea/pkg/agent/metrics" + "antrea.io/libOpenflow/openflow15" "antrea.io/libOpenflow/protocol" ofutil "antrea.io/libOpenflow/util" @@ -26,7 +28,6 @@ import ( "k8s.io/klog/v2" "antrea.io/antrea/pkg/agent/config" - "antrea.io/antrea/pkg/agent/metrics" "antrea.io/antrea/pkg/agent/openflow/cookie" "antrea.io/antrea/pkg/agent/types" "antrea.io/antrea/pkg/agent/util" @@ -234,9 +235,13 @@ type Client interface { // InstallTraceflowFlows installs flows for a Traceflow request. InstallTraceflowFlows(dataplaneTag uint8, liveTraffic, droppedOnly, receiverOnly bool, packet *binding.Packet, ofPort uint32, timeoutSeconds uint16) error + InstallPacketSamplingFlows(dataplaneTag uint8, senderOnly bool, receiverOnly bool, packet *binding.Packet, ofPort uint32, timeoutSeconds uint16) error + // UninstallTraceflowFlows uninstalls flows for a Traceflow request. UninstallTraceflowFlows(dataplaneTag uint8) error + UninstallPacketSamplingFlows(dataplaneTag uint8) error + // GetPolicyInfoFromConjunction returns the following policy information for the provided conjunction ID: // NetworkPolicy reference, OF priority, rule name, label // The boolean return value indicates whether the policy information was found. @@ -927,6 +932,7 @@ func (c *client) generatePipelines() { c.enableTrafficControl) c.activatedFeatures = append(c.activatedFeatures, c.featurePodConnectivity) c.traceableFeatures = append(c.traceableFeatures, c.featurePodConnectivity) + c.sampleFeatures = append(c.sampleFeatures, c.featurePodConnectivity) c.featureService = newFeatureService(c.cookieAllocator, c.nodeIPChecker, @@ -942,6 +948,7 @@ func (c *client) generatePipelines() { c.connectUplinkToBridge) c.activatedFeatures = append(c.activatedFeatures, c.featureService) c.traceableFeatures = append(c.traceableFeatures, c.featureService) + c.sampleFeatures = append(c.sampleFeatures, c.featureService) } if c.nodeType == config.ExternalNode { @@ -989,6 +996,9 @@ func (c *client) generatePipelines() { c.featureTraceflow = newFeatureTraceflow() c.activatedFeatures = append(c.activatedFeatures, c.featureTraceflow) + c.featurePacketSampling = newFeaturePacketSampling() + c.activatedFeatures = append(c.activatedFeatures, c.featurePacketSampling) + // Pipelines to generate. pipelineIDs := []binding.PipelineID{pipelineRoot, pipelineIP} if c.networkConfig.IPv4Enabled { @@ -1233,6 +1243,24 @@ func (c *client) SendTraceflowPacket(dataplaneTag uint8, packet *binding.Packet, return c.bridge.SendPacketOut(packetOutObj) } +func (c *client) InstallPacketSamplingFlows(dataplaneTag uint8, senderOnly, receiverOnly bool, packet *binding.Packet, ofPort uint32, timeoutSeconds uint16) error { + cacheKey := fmt.Sprintf("%x", dataplaneTag) + var flows []binding.Flow + + for _, f := range c.sampleFeatures { + flows = append(flows, f.flowsToSample(dataplaneTag, + c.ovsMetersAreSupported, + senderOnly, + receiverOnly, + packet, + ofPort, + timeoutSeconds)...) + + } + return c.addFlows(c.featurePacketSampling.cachedFlows, cacheKey, flows) + +} + func (c *client) InstallTraceflowFlows(dataplaneTag uint8, liveTraffic, droppedOnly, receiverOnly bool, packet *binding.Packet, ofPort uint32, timeoutSeconds uint16) error { cacheKey := fmt.Sprintf("%x", dataplaneTag) var flows []binding.Flow @@ -1251,7 +1279,12 @@ func (c *client) InstallTraceflowFlows(dataplaneTag uint8, liveTraffic, droppedO func (c *client) UninstallTraceflowFlows(dataplaneTag uint8) error { cacheKey := fmt.Sprintf("%x", dataplaneTag) - return c.deleteFlows(c.featureTraceflow.cachedFlows, cacheKey) + return c.deleteFlows(c.featurePacketSampling.cachedFlows, cacheKey) +} + +func (c *client) UninstallPacketSamplingFlows(dataplaneTag uint8) error { + cacheKey := fmt.Sprintf("%x", dataplaneTag) + return c.deleteFlows(c.featurePacketSampling.cachedFlows, cacheKey) } // setBasePacketOutBuilder sets base IP properties of a packetOutBuilder which can have more packet data added. diff --git a/pkg/agent/openflow/cookie/allocator.go b/pkg/agent/openflow/cookie/allocator.go index 3aef3db4c84..4ec3dc01f4e 100644 --- a/pkg/agent/openflow/cookie/allocator.go +++ b/pkg/agent/openflow/cookie/allocator.go @@ -39,6 +39,7 @@ const ( Multicluster Traceflow ExternalNodeConnectivity + PacketSampling ) func (c Category) String() string { @@ -61,6 +62,8 @@ func (c Category) String() string { return "Traceflow" case ExternalNodeConnectivity: return "ExternalNodeConnectivity" + case PacketSampling: + return "PacketSampling" default: return "Invalid" } diff --git a/pkg/agent/openflow/framework.go b/pkg/agent/openflow/framework.go index 26320404a48..d6afaa96658 100644 --- a/pkg/agent/openflow/framework.go +++ b/pkg/agent/openflow/framework.go @@ -309,6 +309,10 @@ func (f *featureTraceflow) getRequiredTables() []*Table { return nil } +func (f *featurePacketSampling) getRequiredTables() []*Table { + return nil +} + func (f *featureExternalNodeConnectivity) getRequiredTables() []*Table { return []*Table{ ConntrackTable, @@ -336,3 +340,13 @@ type traceableFeature interface { ofPort uint32, timeoutSeconds uint16) []binding.Flow } + +type sampleFeature interface { + flowsToSample(dataplaneTag uint8, + ovsMetersAreSupported, + senderOnly bool, + receiverOnly bool, + packet *binding.Packet, + ofPort uint32, + timeoutSeconds uint16) []binding.Flow +} diff --git a/pkg/agent/openflow/packetin.go b/pkg/agent/openflow/packetin.go index e4189760722..11350dd8195 100644 --- a/pkg/agent/openflow/packetin.go +++ b/pkg/agent/openflow/packetin.go @@ -53,6 +53,8 @@ const ( // PacketInCategorySvcReject is used to process the Service packets not matching any // Endpoints within packetIn message. PacketInCategorySvcReject + // PacketSampling is used for packetIn messages related to sampling. + PacketInCategoryPS // PacketIn operations below are used to decide which operation(s) should be // executed by a handler. It(they) should be loaded in the second byte of the diff --git a/pkg/agent/openflow/packetsampling.go b/pkg/agent/openflow/packetsampling.go new file mode 100644 index 00000000000..a3988e1e9f8 --- /dev/null +++ b/pkg/agent/openflow/packetsampling.go @@ -0,0 +1,55 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openflow + +import ( + "antrea.io/libOpenflow/openflow15" + + binding "antrea.io/antrea/pkg/ovs/openflow" +) + +type featurePacketSampling struct { + cachedFlows *flowCategoryCache +} + +func (f *featurePacketSampling) getFeatureName() string { + return "PacketSampling" +} + +func newFeaturePacketSampling() *featurePacketSampling { + return &featurePacketSampling{ + cachedFlows: newFlowCategoryCache(), + } +} + +func (f *featurePacketSampling) initFlows() []*openflow15.FlowMod { + return []*openflow15.FlowMod{} +} + +func (f *featurePacketSampling) replayFlows() []*openflow15.FlowMod { + return []*openflow15.FlowMod{} +} + +func (f *featurePacketSampling) initGroups() []binding.OFEntry { + return nil +} + +func (f *featurePacketSampling) replayGroups() []binding.OFEntry { + return nil +} + +func (f *featurePacketSampling) replayMeters() []binding.OFEntry { + return nil +} diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go index fd38572aad0..4cf4f51e529 100644 --- a/pkg/agent/openflow/pipeline.go +++ b/pkg/agent/openflow/pipeline.go @@ -451,6 +451,9 @@ type client struct { featureTraceflow *featureTraceflow traceableFeatures []traceableFeature + featurePacketSampling *featurePacketSampling + sampleFeatures []sampleFeature + pipelines map[binding.PipelineID]binding.Pipeline // ofEntryOperations is a wrapper interface for operating multiple OpenFlow entries with action AddAll / ModifyAll / DeleteAll. @@ -963,6 +966,234 @@ func (f *featureService) snatConntrackFlows() []binding.Flow { return flows } +func matchTransportHeader(packet *binding.Packet, flowBuilder binding.FlowBuilder) binding.FlowBuilder { + // Match transport header + switch packet.IPProto { + case protocol.Type_ICMP: + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolICMP) + case protocol.Type_IPv6ICMP: + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolICMPv6) + case protocol.Type_TCP: + if packet.IsIPv6 { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolTCPv6) + } else { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolTCP) + } + case protocol.Type_UDP: + if packet.IsIPv6 { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolUDPv6) + } else { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolUDP) + } + default: + flowBuilder = flowBuilder.MatchIPProtocolValue(packet.IsIPv6, packet.IPProto) + } + return flowBuilder +} + +func (f *featurePodConnectivity) flowsToSample(dataplaneTag uint8, + ovsMetersAreSupported, + senderOnly bool, + receiverOnly bool, + packet *binding.Packet, + ofPort uint32, + timeout uint16) []binding.Flow { + cookieID := f.cookieAllocator.Request(cookie.PacketSampling).Raw() + var flows []binding.Flow + + if packet == nil { + for _, ipProtocol := range f.ipProtocols { + flows = append(flows, + ConntrackStateTable.ofTable.BuildFlow(priorityLow+1). + Cookie(cookieID). + MatchProtocol(ipProtocol). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().GotoStage(stagePreRouting). + Done(), + ConntrackStateTable.ofTable.BuildFlow(priorityLow+2). + Cookie(cookieID). + MatchProtocol(ipProtocol). + MatchCTStateTrk(true). + MatchCTStateRpl(true). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().Drop(). + Done(), + ) + } + } else { + var flowBuilder binding.FlowBuilder + if !receiverOnly { + flowBuilder = ConntrackStateTable.ofTable.BuildFlow(priorityHigh). + Cookie(cookieID). + MatchInPort(ofPort). + //MatchCTStateNew(true). + MatchCTStateTrk(true). + Action().LoadIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().GotoStage(stagePreRouting) + if !senderOnly { + if packet.DestinationIP != nil { + flowBuilder = flowBuilder.MatchDstIP(packet.DestinationIP) + } + } + } else { + flowBuilder = L2ForwardingCalcTable.ofTable.BuildFlow(priorityHigh). + Cookie(cookieID). + //MatchCTStateNew(true). + MatchCTStateTrk(true). + MatchDstMAC(packet.DestinationMAC). + Action().LoadToRegField(TargetOFPortField, ofPort). + Action().LoadRegMark(OutputToOFPortRegMark). + Action().LoadIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().GotoStage(stageIngressSecurity) + if packet.SourceIP != nil { + flowBuilder = flowBuilder.MatchSrcIP(packet.SourceIP) + } + } + + // for sender only case, capture the tracked packets. + if senderOnly { + for _, ipProtocol := range f.ipProtocols { + tmpFlowBuilder := ConntrackStateTable.ofTable.BuildFlow(priorityHigh). + Cookie(cookieID). + MatchInPort(ofPort). + MatchProtocol(ipProtocol). + MatchCTMark(ServiceCTMark). + MatchCTStateNew(false). + MatchCTStateTrk(true). + Action().LoadRegMark(RewriteMACRegMark). + Action().LoadIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().GotoStage(stageEgressSecurity) + tmpFlowBuilder = matchTransportHeader(packet, tmpFlowBuilder) + flows = append(flows, tmpFlowBuilder.Done()) + } + } + + // Match transport header + switch packet.IPProto { + case protocol.Type_ICMP: + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolICMP) + case protocol.Type_IPv6ICMP: + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolICMPv6) + case protocol.Type_TCP: + if packet.IsIPv6 { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolTCPv6) + } else { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolTCP) + } + case protocol.Type_UDP: + if packet.IsIPv6 { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolUDPv6) + } else { + flowBuilder = flowBuilder.MatchProtocol(binding.ProtocolUDP) + } + default: + flowBuilder = flowBuilder.MatchIPProtocolValue(packet.IsIPv6, packet.IPProto) + } + if packet.IPProto == protocol.Type_TCP || packet.IPProto == protocol.Type_UDP { + if packet.DestinationPort != 0 { + flowBuilder = flowBuilder.MatchDstPort(packet.DestinationPort, nil) + } + if packet.SourcePort != 0 { + flowBuilder = flowBuilder.MatchSrcPort(packet.SourcePort, nil) + } + } + flows = append(flows, flowBuilder.Done()) + + } + // Clear the loaded DSCP bits before output. + ifLiveTraffic := func(fb binding.FlowBuilder) binding.FlowBuilder { + return fb.Action().LoadIPDSCP(0). + Action().OutputToRegField(TargetOFPortField) + + } + + // Do not send to controller if captures only dropped packet. + ifDroppedOnly := func(fb binding.FlowBuilder) binding.FlowBuilder { + if ovsMetersAreSupported { + fb = fb.Action().Meter(PacketInMeterIDTF) + } + fb = fb.Action().SendToController([]byte{uint8(PacketInCategoryPS)}, false) + + return fb + } + + // This generates Traceflow specific flows that outputs traceflow non-hairpin packets to OVS port and Antrea Agent after + // L2 forwarding calculation. + for _, ipProtocol := range f.ipProtocols { + if f.networkConfig.TrafficEncapMode.SupportsEncap() { + // SendToController and Output if output port is tunnel port. + fb := OutputTable.ofTable.BuildFlow(priorityNormal+3). + Cookie(cookieID). + MatchRegFieldWithValue(TargetOFPortField, f.tunnelPort). + MatchProtocol(ipProtocol). + MatchRegMark(OutputToOFPortRegMark). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().OutputToRegField(TargetOFPortField) + fb = ifDroppedOnly(fb) + flows = append(flows, fb.Done()) + // For injected packets, only SendToController if output port is local gateway. In encapMode, a Traceflow + // packet going out of the gateway port (i.e. exiting the overlay) essentially means that the Traceflow + // request is complete. + fb = OutputTable.ofTable.BuildFlow(priorityNormal+2). + Cookie(cookieID). + MatchRegFieldWithValue(TargetOFPortField, f.gatewayPort). + MatchProtocol(ipProtocol). + MatchRegMark(OutputToOFPortRegMark). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout) + fb = ifDroppedOnly(fb) + fb = ifLiveTraffic(fb) + flows = append(flows, fb.Done()) + } else { + // SendToController and Output if output port is local gateway. Unlike in encapMode, inter-Node Pod-to-Pod + // traffic is expected to go out of the gateway port on the way to its destination. + fb := OutputTable.ofTable.BuildFlow(priorityNormal+2). + Cookie(cookieID). + MatchRegFieldWithValue(TargetOFPortField, f.gatewayPort). + MatchProtocol(ipProtocol). + MatchRegMark(OutputToOFPortRegMark). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout). + Action().OutputToRegField(TargetOFPortField) + fb = ifDroppedOnly(fb) + flows = append(flows, fb.Done()) + } + // Only SendToController if output port is local gateway and destination IP is gateway. + gatewayIP := f.gatewayIPs[ipProtocol] + if gatewayIP != nil { + fb := OutputTable.ofTable.BuildFlow(priorityNormal+3). + Cookie(cookieID). + MatchRegFieldWithValue(TargetOFPortField, f.gatewayPort). + MatchProtocol(ipProtocol). + MatchDstIP(gatewayIP). + MatchRegMark(OutputToOFPortRegMark). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout) + fb = ifDroppedOnly(fb) + fb = ifLiveTraffic(fb) + flows = append(flows, fb.Done()) + } + // Only SendToController if output port is Pod port. + fb := OutputTable.ofTable.BuildFlow(priorityNormal + 2). + Cookie(cookieID). + MatchProtocol(ipProtocol). + MatchRegMark(OutputToOFPortRegMark). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout) + fb = ifDroppedOnly(fb) + fb = ifLiveTraffic(fb) + flows = append(flows, fb.Done()) + } + + return flows +} + // TODO: Use DuplicateToBuilder or integrate this function into original one to avoid unexpected difference. // flowsToTrace generates Traceflow specific flows in the connectionTrackStateTable or L2ForwardingCalcTable for featurePodConnectivity. // When packet is not provided, the flows bypass the drop flow in conntrackStateFlow to avoid unexpected drop of the @@ -1154,6 +1385,53 @@ func (f *featurePodConnectivity) flowsToTrace(dataplaneTag uint8, return flows } +// flowsToTrace is used to generate flows for Traceflow in featureService. +func (f *featureService) flowsToSample(dataplaneTag uint8, + ovsMetersAreSupported, + senderOnly bool, + receiverOnly bool, + packet *binding.Packet, + ofPort uint32, + timeout uint16) []binding.Flow { + cookieID := f.cookieAllocator.Request(cookie.PacketSampling).Raw() + var flows []binding.Flow + + // Clear the loaded DSCP bits before output. + ifLiveTraffic := func(fb binding.FlowBuilder) binding.FlowBuilder { + return fb.Action().LoadIPDSCP(0). + Action().OutputToRegField(TargetOFPortField) + + } + + ifDroppedOnly := func(fb binding.FlowBuilder) binding.FlowBuilder { + if ovsMetersAreSupported { + fb = fb.Action().Meter(PacketInMeterIDTF) + } + fb = fb.Action().SendToController([]byte{uint8(PacketInCategoryPS)}, false) + + return fb + } + + // This generates Traceflow specific flows that outputs hairpin traceflow packets to OVS port and Antrea Agent after + // L2forwarding calculation. + for _, ipProtocol := range f.ipProtocols { + if f.enableProxy { + // Only SendToController for hairpin traffic. + // This flow must have higher priority than the one installed by l2ForwardOutputHairpinServiceFlow. + fb := OutputTable.ofTable.BuildFlow(priorityHigh + 2). + Cookie(cookieID). + MatchProtocol(ipProtocol). + MatchCTMark(HairpinCTMark). + MatchIPDSCP(dataplaneTag). + SetHardTimeout(timeout) + fb = ifDroppedOnly(fb) + fb = ifLiveTraffic(fb) + flows = append(flows, fb.Done()) + } + } + return flows +} + // flowsToTrace is used to generate flows for Traceflow in featureService. func (f *featureService) flowsToTrace(dataplaneTag uint8, ovsMetersAreSupported, diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go index e778cae3203..5527c4d3423 100644 --- a/pkg/agent/openflow/testing/mock_openflow.go +++ b/pkg/agent/openflow/testing/mock_openflow.go @@ -419,6 +419,20 @@ func (mr *MockClientMockRecorder) InstallNodeFlows(arg0, arg1, arg2, arg3, arg4 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallNodeFlows", reflect.TypeOf((*MockClient)(nil).InstallNodeFlows), arg0, arg1, arg2, arg3, arg4) } +// InstallPacketSamplingFlows mocks base method. +func (m *MockClient) InstallPacketSamplingFlows(arg0 byte, arg1, arg2 bool, arg3 *openflow.Packet, arg4 uint32, arg5 uint16) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstallPacketSamplingFlows", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(error) + return ret0 +} + +// InstallPacketSamplingFlows indicates an expected call of InstallPacketSamplingFlows. +func (mr *MockClientMockRecorder) InstallPacketSamplingFlows(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPacketSamplingFlows", reflect.TypeOf((*MockClient)(nil).InstallPacketSamplingFlows), arg0, arg1, arg2, arg3, arg4, arg5) +} + // InstallPodFlows mocks base method. func (m *MockClient) InstallPodFlows(arg0 string, arg1 []net.IP, arg2 net.HardwareAddr, arg3 uint32, arg4 uint16, arg5 *uint32) error { m.ctrl.T.Helper() @@ -959,6 +973,20 @@ func (mr *MockClientMockRecorder) UninstallNodeFlows(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallNodeFlows", reflect.TypeOf((*MockClient)(nil).UninstallNodeFlows), arg0) } +// UninstallPacketSamplingFlows mocks base method. +func (m *MockClient) UninstallPacketSamplingFlows(arg0 byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UninstallPacketSamplingFlows", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UninstallPacketSamplingFlows indicates an expected call of UninstallPacketSamplingFlows. +func (mr *MockClientMockRecorder) UninstallPacketSamplingFlows(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallPacketSamplingFlows", reflect.TypeOf((*MockClient)(nil).UninstallPacketSamplingFlows), arg0) +} + // UninstallPodFlows mocks base method. func (m *MockClient) UninstallPodFlows(arg0 string) error { m.ctrl.T.Helper() diff --git a/pkg/apis/crd/v1alpha1/register.go b/pkg/apis/crd/v1alpha1/register.go index a9492ab5ac0..fc9924fa7c9 100644 --- a/pkg/apis/crd/v1alpha1/register.go +++ b/pkg/apis/crd/v1alpha1/register.go @@ -61,6 +61,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ExternalNodeList{}, &SupportBundleCollection{}, &SupportBundleCollectionList{}, + &PacketSampling{}, + &PacketSamplingList{}, ) metav1.AddToGroupVersion( diff --git a/pkg/apis/crd/v1alpha1/types.go b/pkg/apis/crd/v1alpha1/types.go index 8682ddeb09b..364a2373b46 100644 --- a/pkg/apis/crd/v1alpha1/types.go +++ b/pkg/apis/crd/v1alpha1/types.go @@ -935,3 +935,64 @@ type TLSProtocol struct { // SNI (Server Name Indication) indicates the server domain name in the TLS/SSL hello message. SNI string `json:"sni,omitempty"` } + +type PacketSamplingType string + +const ( + FirstNSampling PacketSamplingType = "FirstNSampling" +) + +type FirstNSamplingConfig struct { + Number int32 `json:"number,omitempty"` +} + +const DefaultPacketSamplingTimeout uint16 = 60 + +type PacketSamplingPhase string + +const ( + PacketSamplingRunning PacketSamplingPhase = "Running" + PacketSamplingSucceeded PacketSamplingPhase = "Succeeded" + PacketSamplingFailed PacketSamplingPhase = "Failed" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type PacketSamplingList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + Items []PacketSampling `json:"items"` +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type PacketSampling struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec PacketSamplingSpec `json:"spec,omitempty"` + Status PacketSamplingStatus `json:"status,omitempty"` +} + +type PacketSamplingSpec struct { + Timeout uint16 `json:"timeout,omitempty"` + Type PacketSamplingType `json:"type,omitempty"` + FirstNSamplingConfig *FirstNSamplingConfig `json:"firstNSamplingConfig,omitempty"` + Source Source `json:"source,omitempty"` + Destination Destination `json:"destination,omitempty"` + Packet Packet `json:"packet,omitempty"` + FileServer BundleFileServer `json:"fileServer,omitempty"` + Authentication BundleServerAuthConfiguration `json:"authentication,omitempty"` +} + +type PacketSamplingStatus struct { + Phase PacketSamplingPhase `json:"phase,omitempty"` + Reason string `json:"reason,omitempty"` + NumCapturedPackets int32 `json:"numCapturedPackets,omitempty"` + PacketsPath string `json:"packetsPath,omitempty"` + StartTime *metav1.Time `json:"startTime,omitempty"` + DataplaneTag int8 `json:"dataplaneTag,omitempty"` +} diff --git a/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go index f80a99a2c64..493708adcb4 100644 --- a/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go @@ -353,6 +353,22 @@ func (in *ExternalNodeSpec) DeepCopy() *ExternalNodeSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FirstNSamplingConfig) DeepCopyInto(out *FirstNSamplingConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstNSamplingConfig. +func (in *FirstNSamplingConfig) DeepCopy() *FirstNSamplingConfig { + if in == nil { + return nil + } + out := new(FirstNSamplingConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPProtocol) DeepCopyInto(out *HTTPProtocol) { *out = *in @@ -864,6 +880,113 @@ func (in *Packet) DeepCopy() *Packet { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PacketSampling) DeepCopyInto(out *PacketSampling) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PacketSampling. +func (in *PacketSampling) DeepCopy() *PacketSampling { + if in == nil { + return nil + } + out := new(PacketSampling) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PacketSampling) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PacketSamplingList) DeepCopyInto(out *PacketSamplingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PacketSampling, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PacketSamplingList. +func (in *PacketSamplingList) DeepCopy() *PacketSamplingList { + if in == nil { + return nil + } + out := new(PacketSamplingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PacketSamplingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PacketSamplingSpec) DeepCopyInto(out *PacketSamplingSpec) { + *out = *in + if in.FirstNSamplingConfig != nil { + in, out := &in.FirstNSamplingConfig, &out.FirstNSamplingConfig + *out = new(FirstNSamplingConfig) + **out = **in + } + out.Source = in.Source + out.Destination = in.Destination + in.Packet.DeepCopyInto(&out.Packet) + out.FileServer = in.FileServer + in.Authentication.DeepCopyInto(&out.Authentication) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PacketSamplingSpec. +func (in *PacketSamplingSpec) DeepCopy() *PacketSamplingSpec { + if in == nil { + return nil + } + out := new(PacketSamplingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PacketSamplingStatus) DeepCopyInto(out *PacketSamplingStatus) { + *out = *in + if in.StartTime != nil { + in, out := &in.StartTime, &out.StartTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PacketSamplingStatus. +func (in *PacketSamplingStatus) DeepCopy() *PacketSamplingStatus { + if in == nil { + return nil + } + out := new(PacketSamplingStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PeerNamespaces) DeepCopyInto(out *PeerNamespaces) { *out = *in diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f93a69aef96..57dabc03c3f 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -18,6 +18,8 @@ import ( "context" "time" + "antrea.io/antrea/pkg/controller/packetsampling" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -119,6 +121,7 @@ type ExtraConfig struct { networkPolicyStatusController *controllernetworkpolicy.StatusController bundleCollectionController *controllerbundlecollection.Controller traceflowController *traceflow.Controller + packetSamplingController *packetsampling.Controller } // Config defines the config for Antrea apiserver. @@ -162,7 +165,8 @@ func NewConfig( npController *controllernetworkpolicy.NetworkPolicyController, egressController *egress.EgressController, bundleCollectionController *controllerbundlecollection.Controller, - traceflowController *traceflow.Controller) *Config { + traceflowController *traceflow.Controller, + packetSamplingController *packetsampling.Controller) *Config { return &Config{ genericConfig: genericConfig, extraConfig: ExtraConfig{ @@ -183,6 +187,7 @@ func NewConfig( egressController: egressController, bundleCollectionController: bundleCollectionController, traceflowController: traceflowController, + packetSamplingController: packetSamplingController, }, } } @@ -345,6 +350,11 @@ func installHandlers(c *ExtraConfig, s *genericapiserver.GenericAPIServer) { if features.DefaultFeatureGate.Enabled(features.Traceflow) { s.Handler.NonGoRestfulMux.HandleFunc("/validate/traceflow", webhook.HandlerForValidateFunc(c.traceflowController.Validate)) } + + if features.DefaultFeatureGate.Enabled(features.PacketSampling) { + s.Handler.NonGoRestfulMux.HandleFunc("/validate/packetsampling", webhook.HandlerForValidateFunc(c.packetSamplingController.Validate)) + } + } func DefaultCAConfig() *certificate.CAConfig { diff --git a/pkg/apiserver/handlers/featuregates/handler_test.go b/pkg/apiserver/handlers/featuregates/handler_test.go index e7a3d9c1f76..812c6c6b5f1 100644 --- a/pkg/apiserver/handlers/featuregates/handler_test.go +++ b/pkg/apiserver/handlers/featuregates/handler_test.go @@ -67,6 +67,7 @@ func Test_getGatesResponse(t *testing.T) { {Component: "agent", Name: "Multicluster", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "NetworkPolicyStats", Status: "Enabled", Version: "BETA"}, {Component: "agent", Name: "NodePortLocal", Status: "Enabled", Version: "GA"}, + {Component: "agent", Name: "PacketSampling", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "SecondaryNetwork", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "ServiceExternalIP", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "SupportBundleCollection", Status: "Disabled", Version: "ALPHA"}, @@ -198,6 +199,7 @@ func Test_getControllerGatesResponse(t *testing.T) { {Component: "controller", Name: "Multicluster", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "NetworkPolicyStats", Status: "Enabled", Version: "BETA"}, {Component: "controller", Name: "NodeIPAM", Status: "Enabled", Version: "BETA"}, + {Component: "controller", Name: "PacketSampling", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "ServiceExternalIP", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "SupportBundleCollection", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "Traceflow", Status: "Enabled", Version: "BETA"}, diff --git a/pkg/client/clientset/versioned/typed/crd/v1alpha1/crd_client.go b/pkg/client/clientset/versioned/typed/crd/v1alpha1/crd_client.go index 140c2de5f47..d0d743b938b 100644 --- a/pkg/client/clientset/versioned/typed/crd/v1alpha1/crd_client.go +++ b/pkg/client/clientset/versioned/typed/crd/v1alpha1/crd_client.go @@ -1,4 +1,4 @@ -// Copyright 2022 Antrea Authors +// Copyright 2023 Antrea Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ type CrdV1alpha1Interface interface { ClusterNetworkPoliciesGetter ExternalNodesGetter NetworkPoliciesGetter + PacketSamplingsGetter SupportBundleCollectionsGetter TiersGetter TraceflowsGetter @@ -51,6 +52,10 @@ func (c *CrdV1alpha1Client) NetworkPolicies(namespace string) NetworkPolicyInter return newNetworkPolicies(c, namespace) } +func (c *CrdV1alpha1Client) PacketSamplings() PacketSamplingInterface { + return newPacketSamplings(c) +} + func (c *CrdV1alpha1Client) SupportBundleCollections() SupportBundleCollectionInterface { return newSupportBundleCollections(c) } diff --git a/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_crd_client.go b/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_crd_client.go index a90f91178bc..c9bbe091330 100644 --- a/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_crd_client.go +++ b/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_crd_client.go @@ -1,4 +1,4 @@ -// Copyright 2022 Antrea Authors +// Copyright 2023 Antrea Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -38,6 +38,10 @@ func (c *FakeCrdV1alpha1) NetworkPolicies(namespace string) v1alpha1.NetworkPoli return &FakeNetworkPolicies{c, namespace} } +func (c *FakeCrdV1alpha1) PacketSamplings() v1alpha1.PacketSamplingInterface { + return &FakePacketSamplings{c} +} + func (c *FakeCrdV1alpha1) SupportBundleCollections() v1alpha1.SupportBundleCollectionInterface { return &FakeSupportBundleCollections{c} } diff --git a/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_packetsampling.go b/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_packetsampling.go new file mode 100644 index 00000000000..a073faaa9fe --- /dev/null +++ b/pkg/client/clientset/versioned/typed/crd/v1alpha1/fake/fake_packetsampling.go @@ -0,0 +1,131 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakePacketSamplings implements PacketSamplingInterface +type FakePacketSamplings struct { + Fake *FakeCrdV1alpha1 +} + +var packetsamplingsResource = schema.GroupVersionResource{Group: "crd.antrea.io", Version: "v1alpha1", Resource: "packetsamplings"} + +var packetsamplingsKind = schema.GroupVersionKind{Group: "crd.antrea.io", Version: "v1alpha1", Kind: "PacketSampling"} + +// Get takes name of the packetSampling, and returns the corresponding packetSampling object, and an error if there is any. +func (c *FakePacketSamplings) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.PacketSampling, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(packetsamplingsResource, name), &v1alpha1.PacketSampling{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.PacketSampling), err +} + +// List takes label and field selectors, and returns the list of PacketSamplings that match those selectors. +func (c *FakePacketSamplings) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.PacketSamplingList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(packetsamplingsResource, packetsamplingsKind, opts), &v1alpha1.PacketSamplingList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.PacketSamplingList{ListMeta: obj.(*v1alpha1.PacketSamplingList).ListMeta} + for _, item := range obj.(*v1alpha1.PacketSamplingList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested packetSamplings. +func (c *FakePacketSamplings) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(packetsamplingsResource, opts)) +} + +// Create takes the representation of a packetSampling and creates it. Returns the server's representation of the packetSampling, and an error, if there is any. +func (c *FakePacketSamplings) Create(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.CreateOptions) (result *v1alpha1.PacketSampling, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(packetsamplingsResource, packetSampling), &v1alpha1.PacketSampling{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.PacketSampling), err +} + +// Update takes the representation of a packetSampling and updates it. Returns the server's representation of the packetSampling, and an error, if there is any. +func (c *FakePacketSamplings) Update(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.UpdateOptions) (result *v1alpha1.PacketSampling, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(packetsamplingsResource, packetSampling), &v1alpha1.PacketSampling{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.PacketSampling), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakePacketSamplings) UpdateStatus(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.UpdateOptions) (*v1alpha1.PacketSampling, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(packetsamplingsResource, "status", packetSampling), &v1alpha1.PacketSampling{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.PacketSampling), err +} + +// Delete takes name of the packetSampling and deletes it. Returns an error if one occurs. +func (c *FakePacketSamplings) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(packetsamplingsResource, name, opts), &v1alpha1.PacketSampling{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakePacketSamplings) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(packetsamplingsResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.PacketSamplingList{}) + return err +} + +// Patch applies the patch and returns the patched packetSampling. +func (c *FakePacketSamplings) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.PacketSampling, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(packetsamplingsResource, name, pt, data, subresources...), &v1alpha1.PacketSampling{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.PacketSampling), err +} diff --git a/pkg/client/clientset/versioned/typed/crd/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/crd/v1alpha1/generated_expansion.go index d4d390824f7..4569fbeaaf6 100644 --- a/pkg/client/clientset/versioned/typed/crd/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/crd/v1alpha1/generated_expansion.go @@ -1,4 +1,4 @@ -// Copyright 2022 Antrea Authors +// Copyright 2023 Antrea Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ type ExternalNodeExpansion interface{} type NetworkPolicyExpansion interface{} +type PacketSamplingExpansion interface{} + type SupportBundleCollectionExpansion interface{} type TierExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/crd/v1alpha1/packetsampling.go b/pkg/client/clientset/versioned/typed/crd/v1alpha1/packetsampling.go new file mode 100644 index 00000000000..5eb8ca4de60 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/crd/v1alpha1/packetsampling.go @@ -0,0 +1,182 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + scheme "antrea.io/antrea/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// PacketSamplingsGetter has a method to return a PacketSamplingInterface. +// A group's client should implement this interface. +type PacketSamplingsGetter interface { + PacketSamplings() PacketSamplingInterface +} + +// PacketSamplingInterface has methods to work with PacketSampling resources. +type PacketSamplingInterface interface { + Create(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.CreateOptions) (*v1alpha1.PacketSampling, error) + Update(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.UpdateOptions) (*v1alpha1.PacketSampling, error) + UpdateStatus(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.UpdateOptions) (*v1alpha1.PacketSampling, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.PacketSampling, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.PacketSamplingList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.PacketSampling, err error) + PacketSamplingExpansion +} + +// packetSamplings implements PacketSamplingInterface +type packetSamplings struct { + client rest.Interface +} + +// newPacketSamplings returns a PacketSamplings +func newPacketSamplings(c *CrdV1alpha1Client) *packetSamplings { + return &packetSamplings{ + client: c.RESTClient(), + } +} + +// Get takes name of the packetSampling, and returns the corresponding packetSampling object, and an error if there is any. +func (c *packetSamplings) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.PacketSampling, err error) { + result = &v1alpha1.PacketSampling{} + err = c.client.Get(). + Resource("packetsamplings"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of PacketSamplings that match those selectors. +func (c *packetSamplings) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.PacketSamplingList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.PacketSamplingList{} + err = c.client.Get(). + Resource("packetsamplings"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested packetSamplings. +func (c *packetSamplings) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("packetsamplings"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a packetSampling and creates it. Returns the server's representation of the packetSampling, and an error, if there is any. +func (c *packetSamplings) Create(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.CreateOptions) (result *v1alpha1.PacketSampling, err error) { + result = &v1alpha1.PacketSampling{} + err = c.client.Post(). + Resource("packetsamplings"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(packetSampling). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a packetSampling and updates it. Returns the server's representation of the packetSampling, and an error, if there is any. +func (c *packetSamplings) Update(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.UpdateOptions) (result *v1alpha1.PacketSampling, err error) { + result = &v1alpha1.PacketSampling{} + err = c.client.Put(). + Resource("packetsamplings"). + Name(packetSampling.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(packetSampling). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *packetSamplings) UpdateStatus(ctx context.Context, packetSampling *v1alpha1.PacketSampling, opts v1.UpdateOptions) (result *v1alpha1.PacketSampling, err error) { + result = &v1alpha1.PacketSampling{} + err = c.client.Put(). + Resource("packetsamplings"). + Name(packetSampling.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(packetSampling). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the packetSampling and deletes it. Returns an error if one occurs. +func (c *packetSamplings) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("packetsamplings"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *packetSamplings) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("packetsamplings"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched packetSampling. +func (c *packetSamplings) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.PacketSampling, err error) { + result = &v1alpha1.PacketSampling{} + err = c.client.Patch(pt). + Resource("packetsamplings"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/crd/v1alpha1/interface.go b/pkg/client/informers/externalversions/crd/v1alpha1/interface.go index 5f34c67e3e2..78e3db6f7e5 100644 --- a/pkg/client/informers/externalversions/crd/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/crd/v1alpha1/interface.go @@ -1,4 +1,4 @@ -// Copyright 2022 Antrea Authors +// Copyright 2023 Antrea Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,6 +28,8 @@ type Interface interface { ExternalNodes() ExternalNodeInformer // NetworkPolicies returns a NetworkPolicyInformer. NetworkPolicies() NetworkPolicyInformer + // PacketSamplings returns a PacketSamplingInformer. + PacketSamplings() PacketSamplingInformer // SupportBundleCollections returns a SupportBundleCollectionInformer. SupportBundleCollections() SupportBundleCollectionInformer // Tiers returns a TierInformer. @@ -62,6 +64,11 @@ func (v *version) NetworkPolicies() NetworkPolicyInformer { return &networkPolicyInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// PacketSamplings returns a PacketSamplingInformer. +func (v *version) PacketSamplings() PacketSamplingInformer { + return &packetSamplingInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // SupportBundleCollections returns a SupportBundleCollectionInformer. func (v *version) SupportBundleCollections() SupportBundleCollectionInformer { return &supportBundleCollectionInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/informers/externalversions/crd/v1alpha1/packetsampling.go b/pkg/client/informers/externalversions/crd/v1alpha1/packetsampling.go new file mode 100644 index 00000000000..24f4e6c2975 --- /dev/null +++ b/pkg/client/informers/externalversions/crd/v1alpha1/packetsampling.go @@ -0,0 +1,87 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + versioned "antrea.io/antrea/pkg/client/clientset/versioned" + internalinterfaces "antrea.io/antrea/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "antrea.io/antrea/pkg/client/listers/crd/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// PacketSamplingInformer provides access to a shared informer and lister for +// PacketSamplings. +type PacketSamplingInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.PacketSamplingLister +} + +type packetSamplingInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewPacketSamplingInformer constructs a new informer for PacketSampling type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPacketSamplingInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredPacketSamplingInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredPacketSamplingInformer constructs a new informer for PacketSampling type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredPacketSamplingInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.CrdV1alpha1().PacketSamplings().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.CrdV1alpha1().PacketSamplings().Watch(context.TODO(), options) + }, + }, + &crdv1alpha1.PacketSampling{}, + resyncPeriod, + indexers, + ) +} + +func (f *packetSamplingInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredPacketSamplingInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *packetSamplingInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&crdv1alpha1.PacketSampling{}, f.defaultInformer) +} + +func (f *packetSamplingInformer) Lister() v1alpha1.PacketSamplingLister { + return v1alpha1.NewPacketSamplingLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index b5e7ce3c762..6cd4504fd06 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -60,6 +60,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Crd().V1alpha1().ExternalNodes().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("networkpolicies"): return &genericInformer{resource: resource.GroupResource(), informer: f.Crd().V1alpha1().NetworkPolicies().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("packetsamplings"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Crd().V1alpha1().PacketSamplings().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("supportbundlecollections"): return &genericInformer{resource: resource.GroupResource(), informer: f.Crd().V1alpha1().SupportBundleCollections().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("tiers"): diff --git a/pkg/client/listers/crd/v1alpha1/expansion_generated.go b/pkg/client/listers/crd/v1alpha1/expansion_generated.go index 99f199fd8a1..004e8cf7768 100644 --- a/pkg/client/listers/crd/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/crd/v1alpha1/expansion_generated.go @@ -1,4 +1,4 @@ -// Copyright 2022 Antrea Authors +// Copyright 2023 Antrea Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -36,6 +36,10 @@ type NetworkPolicyListerExpansion interface{} // NetworkPolicyNamespaceLister. type NetworkPolicyNamespaceListerExpansion interface{} +// PacketSamplingListerExpansion allows custom methods to be added to +// PacketSamplingLister. +type PacketSamplingListerExpansion interface{} + // SupportBundleCollectionListerExpansion allows custom methods to be added to // SupportBundleCollectionLister. type SupportBundleCollectionListerExpansion interface{} diff --git a/pkg/client/listers/crd/v1alpha1/packetsampling.go b/pkg/client/listers/crd/v1alpha1/packetsampling.go new file mode 100644 index 00000000000..6c2d7abf8a0 --- /dev/null +++ b/pkg/client/listers/crd/v1alpha1/packetsampling.go @@ -0,0 +1,66 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// PacketSamplingLister helps list PacketSamplings. +// All objects returned here must be treated as read-only. +type PacketSamplingLister interface { + // List lists all PacketSamplings in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.PacketSampling, err error) + // Get retrieves the PacketSampling from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.PacketSampling, error) + PacketSamplingListerExpansion +} + +// packetSamplingLister implements the PacketSamplingLister interface. +type packetSamplingLister struct { + indexer cache.Indexer +} + +// NewPacketSamplingLister returns a new PacketSamplingLister. +func NewPacketSamplingLister(indexer cache.Indexer) PacketSamplingLister { + return &packetSamplingLister{indexer: indexer} +} + +// List lists all PacketSamplings in the indexer. +func (s *packetSamplingLister) List(selector labels.Selector) (ret []*v1alpha1.PacketSampling, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.PacketSampling)) + }) + return ret, err +} + +// Get retrieves the PacketSampling from the index for a given name. +func (s *packetSamplingLister) Get(name string) (*v1alpha1.PacketSampling, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("packetsampling"), name) + } + return obj.(*v1alpha1.PacketSampling), nil +} diff --git a/pkg/controller/packetsampling/controller.go b/pkg/controller/packetsampling/controller.go new file mode 100644 index 00000000000..e9bcf02f884 --- /dev/null +++ b/pkg/controller/packetsampling/controller.go @@ -0,0 +1,349 @@ +package packetsampling + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/wait" + coreinformers "k8s.io/client-go/informers/core/v1" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + "antrea.io/antrea/pkg/client/clientset/versioned" + crdinformers "antrea.io/antrea/pkg/client/informers/externalversions/crd/v1alpha1" + crdlisters "antrea.io/antrea/pkg/client/listers/crd/v1alpha1" +) + +const ( + controllerName = "PacketSamplingController" + + // set resyncPeriod to 0 to disable resyncing + resyncPeriod time.Duration = 0 + + // Default number of workers processing packetsampling request. + defaultWorkers = 4 + + // reason for timeout + samplingTimeoutReason = "PacketSampling timeout" + + // How long to wait before retrying the processing of a traceflow. + minRetryDelay = 5 * time.Second + maxRetryDelay = 300 * time.Second + + tagStep uint8 = 0b100 + minTagNum uint8 = 0b1*tagStep + 0b11 + maxTagNum uint8 = 0b1110*tagStep + 0b11 + + defaultTimeoutDuration = time.Second * time.Duration(crdv1alpha1.DefaultPacketSamplingTimeout) +) + +var ( + timeoutCheckInterval = 10 * time.Second +) + +type Controller struct { + client versioned.Interface + podInformer coreinformers.PodInformer + podLister corelisters.PodLister + packetSamplingInformer crdinformers.PacketSamplingInformer + packetSamplingLister crdlisters.PacketSamplingLister + packetSamplingListerSynced cache.InformerSynced + + queue workqueue.RateLimitingInterface + + runningPacketSamplingsMutex sync.Mutex + runningPacketSamplings map[uint8]string +} + +func NewPacketSamplingController(client versioned.Interface, podInformer coreinformers.PodInformer, packetSamplingInformer crdinformers.PacketSamplingInformer) *Controller { + c := &Controller{ + client: client, + podInformer: podInformer, + podLister: podInformer.Lister(), + packetSamplingInformer: packetSamplingInformer, + packetSamplingLister: packetSamplingInformer.Lister(), + packetSamplingListerSynced: packetSamplingInformer.Informer().HasSynced, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "packetsampling"), + runningPacketSamplings: make(map[uint8]string), + } + + // add handlers + packetSamplingInformer.Informer().AddEventHandlerWithResyncPeriod( + cache.ResourceEventHandlerFuncs{ + AddFunc: c.addPacketSampling, + UpdateFunc: c.updatePacketSampling, + DeleteFunc: c.deletePacketSampling, + }, resyncPeriod, + ) + return c +} + +func (c *Controller) addPacketSampling(obj interface{}) { + ps := obj.(*crdv1alpha1.PacketSampling) + klog.V(2).Infof("Adding PacketSampling %s", ps.Name) + c.enqueuePacketSampling(ps) +} + +func (c *Controller) updatePacketSampling(_, cur interface{}) { + ps := cur.(*crdv1alpha1.PacketSampling) + klog.V(2).Infof("Updating PacketSampling %s", ps.Name) + c.enqueuePacketSampling(ps) +} + +func (c *Controller) deletePacketSampling(obj interface{}) { + ps := obj.(*crdv1alpha1.PacketSampling) + klog.V(2).Infof("Deleting PacketSampling %s", ps.Name) + c.deallocateTagForPS(ps) +} + +func (c *Controller) enqueuePacketSampling(ps *crdv1alpha1.PacketSampling) { + c.queue.Add(ps.Name) +} + +func (c *Controller) Run(stopCh <-chan struct{}) { + defer c.queue.ShutDown() + klog.Infof("Starting %s", controllerName) + defer klog.Infof("Shutting down %s", controllerName) + + if !cache.WaitForNamedCacheSync(controllerName, stopCh, c.packetSamplingListerSynced) { + return + } + + pss, err := c.packetSamplingLister.List(labels.Everything()) + if err != nil { + klog.Errorf("Failed to list all PacketSamplings: %v", err) + } + + for _, ps := range pss { + if ps.Status.Phase == crdv1alpha1.PacketSamplingRunning { + if err := c.occupyTag(ps); err != nil { + klog.Errorf("load packetsampling data plane tag failed: %+v+, %v", ps, err) + } + } + } + + go func() { + wait.Until(c.checkPacketSamplingTimeout, timeoutCheckInterval, stopCh) + }() + + for i := 0; i < defaultWorkers; i++ { + go wait.Until(c.worker, time.Second, stopCh) + } + <-stopCh + +} + +func (c *Controller) checkPacketSamplingTimeout() { + c.runningPacketSamplingsMutex.Lock() + ss := make([]string, 0, len(c.runningPacketSamplings)) + for _, psName := range c.runningPacketSamplings { + ss = append(ss, psName) + } + c.runningPacketSamplingsMutex.Unlock() + + for _, psName := range ss { + // Re-post all running PacketSampling requests to the work queue to + // be processed and checked for timeout. + c.queue.Add(psName) + } +} + +func (c *Controller) worker() { + for c.processPacketSamplingItem() { + + } +} + +func (c *Controller) startPacketSampling(ps *crdv1alpha1.PacketSampling) error { + tag, err := c.allocateTag(ps.Name) + if err != nil { + return err + } + if tag == 0 { + return nil + } + + err = c.updatePacketSamplingStatus(ps, crdv1alpha1.PacketSamplingRunning, "", tag) + if err != nil { + c.deallocateTag(ps.Name, tag) + } + + return err +} + +func (c *Controller) updatePacketSamplingStatus(ps *crdv1alpha1.PacketSampling, phase crdv1alpha1.PacketSamplingPhase, reason string, dataPlaneTag uint8) error { + update := ps.DeepCopy() + update.Status.Phase = phase + if phase == crdv1alpha1.PacketSamplingRunning && update.Status.StartTime == nil { + t := metav1.Now() + update.Status.StartTime = &t + } + update.Status.DataplaneTag = int8(dataPlaneTag) + if reason != "" { + update.Status.Reason = reason + } + + _, err := c.client.CrdV1alpha1().PacketSamplings().UpdateStatus(context.TODO(), update, metav1.UpdateOptions{}) + return err +} + +// Allocates a tag. If the PacketSampling request has been allocated with a tag +// already, 0 is returned. If number of existing PacketSampling requests reaches +// the upper limit, an error is returned. +func (c *Controller) allocateTag(name string) (uint8, error) { + c.runningPacketSamplingsMutex.Lock() + defer c.runningPacketSamplingsMutex.Unlock() + + for _, n := range c.runningPacketSamplings { + if n == name { + // The packetsampling request has been processed already. + return 0, nil + } + } + for i := minTagNum; i <= maxTagNum; i += tagStep { + if _, ok := c.runningPacketSamplings[i]; !ok { + c.runningPacketSamplings[i] = name + return i, nil + } + } + return 0, fmt.Errorf("number of on-going packetsampling operations already reached the upper limit: %d", maxTagNum) +} + +func (c *Controller) processPacketSamplingItem() bool { + obj, quit := c.queue.Get() + if quit { + return false + } + + defer c.queue.Done(obj) + + key, ok := obj.(string) + if !ok { + c.queue.Forget(obj) + klog.Errorf("Expected string in work queue but got %#v", obj) + return true + } + + err := c.syncPacketSampling(key) + + if err != nil { + klog.Errorf("error sync packetSampling %s, existing,error: %v", key, err) + c.queue.AddRateLimited(key) + } else { + c.queue.Forget(key) + } + + return true +} + +func (c *Controller) deallocateTagForPS(ps *crdv1alpha1.PacketSampling) { + if ps.Status.DataplaneTag != 0 { + c.deallocateTag(ps.Name, uint8(ps.Status.DataplaneTag)) + } +} + +func (c *Controller) deallocateTag(name string, tag uint8) { + c.runningPacketSamplingsMutex.Lock() + defer c.runningPacketSamplingsMutex.Unlock() + + if existing, ok := c.runningPacketSamplings[tag]; ok { + if name == existing { + delete(c.runningPacketSamplings, tag) + } + } +} + +func (c *Controller) occupyTag(ps *crdv1alpha1.PacketSampling) error { + tag := uint8(ps.Status.DataplaneTag) + + if tag < minTagNum || tag > maxTagNum { + return errors.New("this packetsamping crd's data plane tag is out of range") + } + c.runningPacketSamplingsMutex.Lock() + defer c.runningPacketSamplingsMutex.Unlock() + + if exist, ok := c.runningPacketSamplings[tag]; ok { + if ps.Name == exist { + return nil + } + return errors.New("this packetsamping crd's data plane tag is already occupied") + } + c.runningPacketSamplings[tag] = ps.Name + return nil + +} + +func (c *Controller) syncPacketSampling(name string) error { + startTime := time.Now() + defer func() { + klog.V(4).Infof("Finished sync for packetsampling %s.(%v)", name, time.Since(startTime)) + }() + + ps, err := c.packetSamplingLister.Get(name) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + switch ps.Status.Phase { + case "": + err = c.startPacketSampling(ps) + case crdv1alpha1.PacketSamplingRunning: + err = c.checkPacketSamplingStatus(ps) + case crdv1alpha1.PacketSamplingFailed: + c.deallocateTagForPS(ps) + } + return err +} + +// checkPacketSamplingStatus is only called for PacketSamplings in the Running phase +func (c *Controller) checkPacketSamplingStatus(ps *crdv1alpha1.PacketSampling) error { + if checkPacketSamplingSucceeded(ps) { + c.deallocateTagForPS(ps) + return c.updatePacketSamplingStatus(ps, crdv1alpha1.PacketSamplingSucceeded, "", 0) + } + + if checkPacketSamplingTimeout(ps) { + c.deallocateTagForPS(ps) + return c.updatePacketSamplingStatus(ps, crdv1alpha1.PacketSamplingFailed, samplingTimeoutReason, 0) + } + + return nil +} + +func checkPacketSamplingSucceeded(ps *crdv1alpha1.PacketSampling) bool { + succeeded := false + if ps.Spec.Type == crdv1alpha1.FirstNSampling && ps.Status.NumCapturedPackets == ps.Spec.FirstNSamplingConfig.Number { + succeeded = true + } + return succeeded +} + +func checkPacketSamplingTimeout(ps *crdv1alpha1.PacketSampling) bool { + var timeout time.Duration + if ps.Spec.Timeout != 0 { + timeout = time.Duration(ps.Spec.Timeout) * time.Second + } else { + timeout = defaultTimeoutDuration + } + var startTime time.Time + if ps.Status.StartTime != nil { + startTime = ps.Status.StartTime.Time + } else { + // a fallback that should not be needed in general since we are in the Running phase + // when upgrading Antrea from a previous version, the field would be empty + klog.V(2).InfoS("StartTime field in PacketSampling Status should not be empty", "Traceflow", klog.KObj(ps)) + startTime = ps.CreationTimestamp.Time + } + return startTime.Add(timeout).Before(time.Now()) +} diff --git a/pkg/controller/packetsampling/controller_test.go b/pkg/controller/packetsampling/controller_test.go new file mode 100644 index 00000000000..f38f14f1b1f --- /dev/null +++ b/pkg/controller/packetsampling/controller_test.go @@ -0,0 +1,197 @@ +// Copyright 2024 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packetsampling + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/informers" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" + + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + "antrea.io/antrea/pkg/client/clientset/versioned" + fakeversioned "antrea.io/antrea/pkg/client/clientset/versioned/fake" + crdinformers "antrea.io/antrea/pkg/client/informers/externalversions" +) + +var alwaysReady = func() bool { return true } + +const informerDefaultResync time.Duration = 30 * time.Second + +type packetSamplingController struct { + *Controller + kubeClient clientset.Interface + client versioned.Interface + informerFactory informers.SharedInformerFactory + crdInformerFactory crdinformers.SharedInformerFactory +} + +func newController(k8sObjects ...runtime.Object) *packetSamplingController { + client := fake.NewSimpleClientset(k8sObjects...) + crdClient := newCRDClientset() + informerFactory := informers.NewSharedInformerFactory(client, informerDefaultResync) + crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, informerDefaultResync) + controller := NewPacketSamplingController(crdClient, + informerFactory.Core().V1().Pods(), + crdInformerFactory.Crd().V1alpha1().PacketSamplings()) + controller.packetSamplingListerSynced = alwaysReady + return &packetSamplingController{ + controller, + client, + crdClient, + informerFactory, + crdInformerFactory, + } +} + +func (psc *packetSamplingController) waitForPodInNamespace(ns string, name string, timeout time.Duration) (*corev1.Pod, error) { + var pod *corev1.Pod + var err error + if err = wait.Poll(100*time.Millisecond, timeout, func() (bool, error) { + // Make sure dummy Pod is synced by informer + pod, err = psc.podLister.Pods(ns).Get(name) + if err != nil { + return false, nil + } + return true, nil + }); err != nil { + return nil, err + } + return pod, nil +} + +func (psc *packetSamplingController) waitForPacketSampling(name string, phase crdv1alpha1.PacketSamplingPhase, timeout time.Duration) (*crdv1alpha1.PacketSampling, error) { + var ps *crdv1alpha1.PacketSampling + var err error + if err = wait.Poll(100*time.Millisecond, timeout, func() (bool, error) { + ps, err = psc.client.CrdV1alpha1().PacketSamplings().Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil || ps.Status.Phase != phase { + return false, nil + } + return true, nil + }); err != nil { + return nil, err + } + return ps, nil +} + +func TestPacketSampling(t *testing.T) { + // Check timeout more frequently. + timeoutCheckInterval = time.Second + + psc := newController() + stopCh := make(chan struct{}) + psc.informerFactory.Start(stopCh) + psc.crdInformerFactory.Start(stopCh) + // Must wait for cache sync, otherwise resource creation events will be missing if the resources are created + // in-between list and watch call of an informer. This is because fake clientset doesn't support watching with + // resourceVersion. A watcher of fake clientset only gets events that happen after the watcher is created. + psc.informerFactory.WaitForCacheSync(stopCh) + psc.crdInformerFactory.WaitForCacheSync(stopCh) + go psc.Run(stopCh) + + numRunningPacketSamplings := func() int { + psc.runningPacketSamplingsMutex.Lock() + defer psc.runningPacketSamplingsMutex.Unlock() + return len(psc.runningPacketSamplings) + } + + ps1 := crdv1alpha1.PacketSampling{ + ObjectMeta: metav1.ObjectMeta{Name: "ps1", UID: "uid1"}, + Spec: crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{Namespace: "ns1", Pod: "pod1"}, + Destination: crdv1alpha1.Destination{Namespace: "ns2", Pod: "pod2"}, + Timeout: 2, // 2 seconds timeout + Type: crdv1alpha1.FirstNSampling, + FirstNSamplingConfig: &crdv1alpha1.FirstNSamplingConfig{ + Number: 5, + }, + }, + } + + t.Run("normalPacketSampling", func(t *testing.T) { + pod1 := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns1", + }, + } + + psc.kubeClient.CoreV1().Pods("ns1").Create(context.TODO(), &pod1, metav1.CreateOptions{}) + createdPod, _ := psc.waitForPodInNamespace("ns1", "pod1", time.Second) + require.NotNil(t, createdPod) + psc.client.CrdV1alpha1().PacketSamplings().Create(context.TODO(), &ps1, metav1.CreateOptions{}) + res, _ := psc.waitForPacketSampling("ps1", crdv1alpha1.PacketSamplingRunning, time.Second) + require.NotNil(t, res) + // DataplaneTag should be allocated by Controller. + assert.True(t, res.Status.DataplaneTag > 0) + assert.Equal(t, numRunningPacketSamplings(), 1) + + // Test Controller handling of successful PacketSampling. + res.Status.NumCapturedPackets = 5 + psc.client.CrdV1alpha1().PacketSamplings().Update(context.TODO(), res, metav1.UpdateOptions{}) + res, err := psc.waitForPacketSampling("ps1", crdv1alpha1.PacketSamplingSucceeded, time.Second) + if err != nil { + t.Fatal(err) + } + assert.NotNil(t, res) + // DataplaneTag should be deallocated by Controller. + assert.True(t, res.Status.DataplaneTag == 0) + assert.Equal(t, numRunningPacketSamplings(), 0) + psc.client.CrdV1alpha1().PacketSamplings().Delete(context.TODO(), "ps1", metav1.DeleteOptions{}) + }) + + t.Run("timeoutPacketSampling", func(t *testing.T) { + startTime := time.Now() + psc.client.CrdV1alpha1().PacketSamplings().Create(context.TODO(), &ps1, metav1.CreateOptions{}) + res, _ := psc.waitForPacketSampling("ps1", crdv1alpha1.PacketSamplingRunning, time.Second) + assert.NotNil(t, res) + res, _ = psc.waitForPacketSampling("ps1", crdv1alpha1.PacketSamplingFailed, defaultTimeoutDuration*2) + assert.NotNil(t, res) + assert.True(t, time.Now().Sub(startTime) >= time.Second*time.Duration(ps1.Spec.Timeout)) + assert.Equal(t, res.Status.Reason, samplingTimeoutReason) + assert.True(t, res.Status.DataplaneTag == 0) + assert.Equal(t, numRunningPacketSamplings(), 0) + }) + + close(stopCh) +} + +func newCRDClientset() *fakeversioned.Clientset { + client := fakeversioned.NewSimpleClientset() + + client.PrependReactor("create", "packetsamplings", k8stesting.ReactionFunc(func(action k8stesting.Action) (bool, runtime.Object, error) { + ps := action.(k8stesting.CreateAction).GetObject().(*crdv1alpha1.PacketSampling) + + // Fake client does not set CreationTimestamp. + if ps.ObjectMeta.CreationTimestamp == (metav1.Time{}) { + ps.ObjectMeta.CreationTimestamp.Time = time.Now() + } + + return false, ps, nil + })) + + return client +} diff --git a/pkg/controller/packetsampling/validate.go b/pkg/controller/packetsampling/validate.go new file mode 100644 index 00000000000..7bc5f418122 --- /dev/null +++ b/pkg/controller/packetsampling/validate.go @@ -0,0 +1,81 @@ +// Copyright 2024 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packetsampling + +import ( + "encoding/json" + "fmt" + + admv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" +) + +func (c *Controller) Validate(review *admv1.AdmissionReview) *admv1.AdmissionResponse { + newResponse := func(allowed bool, deniedReason string) *admv1.AdmissionResponse { + resp := &admv1.AdmissionResponse{ + UID: review.Request.UID, + Allowed: allowed, + } + if !allowed { + resp.Result = &metav1.Status{ + Message: deniedReason, + } + } + return resp + } + + klog.V(2).InfoS("Validating PacketSampling", "request", review.Request) + + var newObj crdv1alpha1.PacketSampling + if review.Request.Object.Raw != nil { + if err := json.Unmarshal(review.Request.Object.Raw, &newObj); err != nil { + klog.ErrorS(err, "Error de-serializing current Traceflow") + return newResponse(false, err.Error()) + } + } + + switch review.Request.Operation { + case admv1.Create: + klog.V(2).InfoS("Validating CREATE request for PacketSampling", "name", newObj.Name) + allowed, deniedReason := c.validate(&newObj) + return newResponse(allowed, deniedReason) + case admv1.Update: + klog.V(2).InfoS("Validating UPDATE request for PacketSampling", "name", newObj.Name) + allowed, deniedReason := c.validate(&newObj) + return newResponse(allowed, deniedReason) + default: + err := fmt.Errorf("invalid request operation %s for Traceflow", review.Request.Operation) + klog.ErrorS(err, "Failed to validate PacketSampling", "name", newObj.Name) + return newResponse(false, err.Error()) + } +} + +func (c *Controller) validate(ps *crdv1alpha1.PacketSampling) (allowed bool, deniedReason string) { + if ps.Spec.Source.Pod == "" && ps.Spec.Destination.Pod == "" { + return false, fmt.Sprintf("PacketSampling %s has neither source nor destination Pod specified", ps.Name) + } + + if ps.Spec.Type != crdv1alpha1.FirstNSampling { + return false, fmt.Sprintf("PacketSampling %s has invalid type %s (supported are [%s])", ps.Name, ps.Spec.Type, crdv1alpha1.FirstNSampling) + } + + if ps.Spec.FirstNSamplingConfig == nil { + return false, fmt.Sprintf("PacketSampling %s has no FirstNSamplingConfig", ps.Name) + } + return true, "" +} diff --git a/pkg/controller/packetsampling/validate_test.go b/pkg/controller/packetsampling/validate_test.go new file mode 100644 index 00000000000..b782764336f --- /dev/null +++ b/pkg/controller/packetsampling/validate_test.go @@ -0,0 +1,134 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packetsampling + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + admv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" +) + +func TestControllerValidate(t *testing.T) { + tests := []struct { + name string + + // input + oldSpec *crdv1alpha1.PacketSamplingSpec + newSpec *crdv1alpha1.PacketSamplingSpec + + // expected output + allowed bool + deniedReason string + }{ + { + name: "Traceflow should have either source or destination Pod assigned", + newSpec: &crdv1alpha1.PacketSamplingSpec{}, + deniedReason: "PacketSampling ps has neither source nor destination Pod specified", + }, + { + name: "Must assign sampling type", + newSpec: &crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: "test-ns", + Pod: "test-pod", + }, + }, + deniedReason: "PacketSampling ps has invalid type (supported are [FirstNSampling])", + }, + { + name: "FistNSampling config not set", + newSpec: &crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: "test-ns", + Pod: "test-pod", + }, + Type: crdv1alpha1.FirstNSampling, + }, + deniedReason: "PacketSampling ps has no FirstNSamplingConfig", + }, + { + name: "Valid request", + newSpec: &crdv1alpha1.PacketSamplingSpec{ + Source: crdv1alpha1.Source{ + Namespace: "test-ns", + Pod: "test-pod", + }, + Type: crdv1alpha1.FirstNSampling, + FirstNSamplingConfig: &crdv1alpha1.FirstNSamplingConfig{ + Number: 4, + }, + }, + allowed: true, + }, + } + for _, ps := range tests { + t.Run(ps.name, func(t *testing.T) { + stopCh := make(chan struct{}) + defer close(stopCh) + + controller := newController() + controller.informerFactory.Start(stopCh) + controller.crdInformerFactory.Start(stopCh) + // Must wait for cache sync, otherwise resource creation events will be missing if the resources are created + // in-between list and watch call of an informer. This is because fake clientset doesn't support watching with + // resourceVersion. A watcher of fake clientset only gets events that happen after the watcher is created. + controller.informerFactory.WaitForCacheSync(stopCh) + controller.crdInformerFactory.WaitForCacheSync(stopCh) + go controller.Run(stopCh) + + var request *admv1.AdmissionRequest + if ps.oldSpec != nil && ps.newSpec != nil { + request = &admv1.AdmissionRequest{ + Operation: admv1.Update, + OldObject: toRawExtension(ps.oldSpec), + Object: toRawExtension(ps.newSpec), + } + } else if ps.newSpec != nil { + request = &admv1.AdmissionRequest{ + Operation: admv1.Create, + Object: toRawExtension(ps.newSpec), + } + } + review := &admv1.AdmissionReview{ + Request: request, + } + + expectedResponse := &admv1.AdmissionResponse{ + Allowed: ps.allowed, + } + if !ps.allowed { + expectedResponse.Result = &metav1.Status{ + Message: ps.deniedReason, + } + } + + response := controller.Validate(review) + assert.Equal(t, expectedResponse, response) + }) + } +} + +func toRawExtension(spec *crdv1alpha1.PacketSamplingSpec) runtime.RawExtension { + ps := &crdv1alpha1.PacketSampling{Spec: *spec} + ps.Name = "ps" + raw, _ := json.Marshal(ps) + return runtime.RawExtension{Raw: raw} +} diff --git a/pkg/features/antrea_features.go b/pkg/features/antrea_features.go index e2b5cb801c8..fb965f1c4ec 100644 --- a/pkg/features/antrea_features.go +++ b/pkg/features/antrea_features.go @@ -67,6 +67,10 @@ const ( // Allows to trace path from a generated packet. Traceflow featuregate.Feature = "Traceflow" + // alpha: v1.5 + // Allows to capture sampling packtes for a flow. + PacketSampling featuregate.Feature = "PacketSampling" + // alpha: v0.9 // Flow exporter exports IPFIX flow records of Antrea flows seen in conntrack module. FlowExporter featuregate.Feature = "FlowExporter" @@ -167,6 +171,7 @@ var ( TopologyAwareHints: {Default: true, PreRelease: featuregate.Beta}, CleanupStaleUDPSvcConntrack: {Default: false, PreRelease: featuregate.Alpha}, Traceflow: {Default: true, PreRelease: featuregate.Beta}, + PacketSampling: {Default: false, PreRelease: featuregate.Alpha}, AntreaIPAM: {Default: false, PreRelease: featuregate.Alpha}, FlowExporter: {Default: false, PreRelease: featuregate.Alpha}, NetworkPolicyStats: {Default: true, PreRelease: featuregate.Beta}, @@ -209,6 +214,7 @@ var ( SupportBundleCollection, TopologyAwareHints, Traceflow, + PacketSampling, TrafficControl, EgressTrafficShaping, ) @@ -229,6 +235,7 @@ var ( ServiceExternalIP, SupportBundleCollection, Traceflow, + PacketSampling, ) // UnsupportedFeaturesOnWindows records the features not supported on @@ -255,6 +262,7 @@ var ( LoadBalancerModeDSR: {}, CleanupStaleUDPSvcConntrack: {}, EgressTrafficShaping: {}, + PacketSampling: {}, } // supportedFeaturesOnExternalNode records the features supported on an external // Node. Antrea Agent checks the enabled features if it is running on an