Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

e2e, persistent-ip: Add primary-UDN tests #72

Merged
merged 7 commits into from
Oct 8, 2024

Conversation

RamLavi
Copy link
Contributor

@RamLavi RamLavi commented Oct 1, 2024

What this PR does / why we need it:

Since the same test portfolio is run for both secondary/primary UDN, we could either copy the entire tests for primary UDN (making a lot of duplicate code), OR try to join them. This PR is choosing the latter, joining both options (secondary and primary UDNs) to the same test suite.

The main change between the two is the way both tests retrieve the IPs from the VMI:
For secondary: retrieve the IPs from the VMI.Status
For primary UDN: retrieve the IPs from the pod's network-status annotation (this may change in future iterations).

This PR:

  • primes the e2e/persistentips_test.go for introduction of the primary-udn test cases.
    • generalizes the NAD creation using the role parameter.
    • generalizes the "getIP" function to work on both cases (secondary/primary UDN).
    • simplifies the NAD name to be static, to simplify case complexity.
    • generalizes the "generate" VMI function to work on both cases (secondary/primary UDN).
  • adds the primary-udn test cases.

With this change, we now cover ipam-extentions operation on VMI/VMs using primary-UDN.

Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Fixes #

Special notes for your reviewer:

Release note:

NONE

@kubevirt-bot kubevirt-bot added the dco-signoff: yes Indicates the PR's author has DCO signed all their commits. label Oct 1, 2024
This will enable the network-segmentation feature on OVNK, needed in
order to run VM workloads with primary UDN.

Signed-off-by: Ram Lavi <[email protected]>
@RamLavi
Copy link
Contributor Author

RamLavi commented Oct 1, 2024

@qinqon I eventually did not parametize the generateVM function, as it also needs the nad.Name which is not static (randomized)... so this is the only place where we use If(role ==...). I can live with that, tell me what you think though.

Copy link
Collaborator

@qinqon qinqon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just nits

@@ -40,6 +40,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

const networkInterfaceName = "multus"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is both the VMI interface/network logical name and the field at VMI status to look for so and is just for secondaries so let's name this
secondaryLogicalNetworkInterfaceName

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

vmi *kubevirtv1.VirtualMachineInstance
nad *nadv1.NetworkAttachmentDefinition
td testenv.TestData
getIPsFunc func(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this ipsFrom

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -312,3 +317,7 @@ func removeFinalizersPatch() ([]byte, error) {
}
return json.Marshal(patch)
}

func getIPsFromVMIStatus(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just for secondaries since it passes the secondary network interface name let's call this secondaryNetworkVMIStatusIPs

so we have

ipsFrom: secondaryNetworkVMIStatusIPs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


type testParams struct {
role string
getIPsFunc func(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rename this to ipsFrom

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DONE

Copy link
Collaborator

@maiqueb maiqueb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you.

@@ -3,7 +3,7 @@
set -xe
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

KIND_ARGS="${KIND_ARGS:--ic -ikv -i6 -mne}"
KIND_ARGS="${KIND_ARGS:--ic -ikv -i6 -mne -nse}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want this on all lanes, or we want to add an additional lane, and test it there ?

I'm inclined to say we want the latter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm.. regardless of the question whether we want to run the tests in a parallel lane, I don't think we should only enable it when we want to test it.
It's a feature that is planned to be enabled by default, so I don't see a good reason why not to add it for all the cluster lanes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i also dont see why to add another lane tbh, we should not over test

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's roll with it.


Expect(vmi.Status.Interfaces).NotTo(BeEmpty())
Expect(vmi.Status.Interfaces[0].IPs).NotTo(BeEmpty())
Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPs(params.getIPsFunc, Not(BeEmpty())))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly fail to understand how is this simpler than what we had before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not simpler. It's adding another dimension in order to accommodate the primary UDN tests..

@@ -112,8 +128,7 @@ var _ = Describe("Persistent IPs", func() {
WithTimeout(5 * time.Minute).
Should(testenv.ContainConditionVMIReady())

Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPsAtInterfaceByName(networkInterfaceName, ConsistOf(vmiIPsBeforeMigration)))

Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPs(params.getIPsFunc, ConsistOf(vmiIPsBeforeMigration)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at this now, I think the condition was wrong before, and you're taking it a step further in the wrong direction.

I would expect to see something like:

Expect(testenv.VMIPs(vmi)()).To(ConsistOf(vmiIPsBeforeMigration))

meaning, IMHO the IP extraction should go in the expect body, rather than in a transformation applied in the matcher.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason this convoluted way of checking (original code):

Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPs(getIPsFunc, ConsistOf(vmiIPsBeforeMigration)))

is done so that we won't have to re-get the VMI instance.
Changing it to what you want needs to look like this:

Expect(testenv.Client.Get(context.Background(), client.ObjectKeyFromObject(vmi), vmi)).To(Succeed())
Expect(testenv.VMIPs(vmi)()).To(ConsistOf(vmiIPsBeforeMigration))

otherwise the VMI object in the test is not refreshed with the post-migration entity.

would you like this PR to include this refactorment? I kinda feel it should have a separate PR (which is why I didn't refactor it here) - but I don't mind doing it on this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I hate how this looks now, but this can be addressed in a follow up.

Just saying: this looks a lot more complex than it should be.

Hiding re-fetching the VM behind a function so I can save an eventually, is also not good IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Will tackle in follow up PR


Expect(vmi.Status.Interfaces).NotTo(BeEmpty())
Expect(vmi.Status.Interfaces[0].IPs).NotTo(BeEmpty())
IPs, err := params.getIPsFunc(vmi)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ips, not IPs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -311,3 +339,7 @@ func removeFinalizersPatch() ([]byte, error) {
}
return json.Marshal(patch)
}

func getIPsFromVMIStatus(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this function. All it does in wrap another function, and IIUC it can't error.

Hence, why do you have an error in the func's signature ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the same test portfolio is run for both secondary/primary UDN, we could either copy the entire tests for primary UDN (making a lot of duplicate code), OR try to join them. The main change is the way both tests retrieve the IPs from the VMI:
For secondary: retrieve the IPs from the VMI.Status
For primary UDN: retrieve the IPs from the pod's network-status annotation.

So right now you're right - it's seems like a unneeded generalization of the function.
But what we eventually need is a general ipsFrom function, that will satisfy both options.

BTW once the IPs could be retrieved from the VMI.status also for primary UDNs then we could remove this complexity.
I will adjust the commit message to better explain this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't get it.

Why does the function's signature return an error ?

Will a follow up commit change this to throw an error ? If so, why don't you update the function's signature to return an error when it is needed ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK, I'll introduce the error return param in the relevant commit.

Copy link
Contributor Author

@RamLavi RamLavi Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DONE. It's now in a separate commit. PTAL

Comment on lines 65 to 70
err := Client.List(context.Background(), pods,
client.InNamespace(namespace),
client.MatchingLabels(labelSelector),
client.MatchingFields(fieldSelector))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's be consistent; either everything in one line, or one item per line.

Suggested change
err := Client.List(context.Background(), pods,
client.InNamespace(namespace),
client.MatchingLabels(labelSelector),
client.MatchingFields(fieldSelector))
err := Client.List(
context.Background(),
pods,
client.InNamespace(namespace),
client.MatchingLabels(labelSelector),
client.MatchingFields(fieldSelector),
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DONE

}

if len(pods.Items) == 0 {
return nil, fmt.Errorf("failed to lookup pod")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe consider printing the selectors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Comment on lines 90 to 93
if node := vmi.Status.NodeName; node != "" {
const nodeName = "spec.nodeName"
fieldSelectors[nodeName] = node
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed ? Trying to wrap my head around how could we reach this sorry state, and I just can't find it. Minus KubeVirt bugs of course.

Copy link
Contributor Author

@RamLavi RamLavi Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was necessary because it's migration and we can two running pods, just on different nodes, but since we're also using the vmi label selector using the VMI.UID, then I guess it's redundant.
I don't know of any kubevirt bug it is meant to cover, so removing.

return netStatus, nil
}

func getDefaultNetworkStatus(vmi *kubevirtv1.VirtualMachineInstance) (*nadv1.NetworkStatus, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean cluster default network here ? In primary UDN we can have have a different primary network than the cluster default network.

Also, please drop the get.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the network status with the default: true in it.
Removed the get

return nil, fmt.Errorf("primary IPs not found")
}

func GetIPsFromNetworkStatusAnnotation(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func GetIPsFromNetworkStatusAnnotation(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {
func IPsFromNetworkStatusAnnotation(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DONE

@RamLavi
Copy link
Contributor Author

RamLavi commented Oct 7, 2024

Change: Address @qinqon + @maiqueb reviews

Copy link
Collaborator

@qinqon qinqon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits on naming.

Comment on lines 127 to 134
func IPsFromNetworkStatusAnnotation(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {
defNetworkStatus, err := defaultNetworkStatus(vmi)
if err != nil {
return nil, err
}

return defNetworkStatus.IPs, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this function to the test, and call it defaultNetworkStatusAnnotationIPs so the test reads

ipsFrom: defaultNetworkStatusAnnotationIPs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qinqon
Copy link
Collaborator

qinqon commented Oct 7, 2024

@RamLavi I would like to remove the other "if role == secondary" from the test by exposing the VMI build functios to the test struct, I know we say we were not going to do so, but it do not feel right, what do you think ?

@RamLavi
Copy link
Contributor Author

RamLavi commented Oct 7, 2024

@RamLavi I would like to remove the other "if role == secondary" from the test by exposing the VMI build functios to the test struct, I know we say we were not going to do so, but it do not feel right, what do you think ?

I'm game, I'll create a NewVMI function with options..

Currently GenerateLayer2WithSubnetNAD is creating NADs for secondary
roles (which is the default).
Add role input param to the function in order to support primary role
that will be used in future commits.

Signed-off-by: Ram Lavi <[email protected]>
@RamLavi
Copy link
Contributor Author

RamLavi commented Oct 7, 2024

@RamLavi I would like to remove the other "if role == secondary" from the test by exposing the VMI build functios to the test struct, I know we say we were not going to do so, but it do not feel right, what do you think ?

DONE

Copy link
Collaborator

@qinqon qinqon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nit I forgot.

test/e2e/persistentips_test.go Show resolved Hide resolved
@qinqon
Copy link
Collaborator

qinqon commented Oct 8, 2024

/lgtm

@kubevirt-bot kubevirt-bot added the lgtm Indicates that a PR is ready to be merged. label Oct 8, 2024
Copy link
Collaborator

@maiqueb maiqueb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the commit split. It makes it easier to review.

We're nearly there.

We can tackle the matchers in a follow-up PR.

@@ -3,7 +3,7 @@
set -xe
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

KIND_ARGS="${KIND_ARGS:--ic -ikv -i6 -mne}"
KIND_ARGS="${KIND_ARGS:--ic -ikv -i6 -mne -nse}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's roll with it.

test/e2e/persistentips_test.go Outdated Show resolved Hide resolved

Expect(vmi.Status.Interfaces).NotTo(BeEmpty())
Expect(vmi.Status.Interfaces[0].IPs).NotTo(BeEmpty())
Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPs(ipsFrom, Not(BeEmpty())))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this be the same as

Expect(...).NotTo(testenv.MatchIPs(ipsFrom, BeEmpty()))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, but both options seem equally convoluted. Let's tackle it in a follow up PR as you suggest

test/e2e/persistentips_test.go Outdated Show resolved Hide resolved
@@ -311,3 +339,7 @@ func removeFinalizersPatch() ([]byte, error) {
}
return json.Marshal(patch)
}

func getIPsFromVMIStatus(vmi *kubevirtv1.VirtualMachineInstance) ([]string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't get it.

Why does the function's signature return an error ?

Will a follow up commit change this to throw an error ? If so, why don't you update the function's signature to return an error when it is needed ?

@@ -112,8 +128,7 @@ var _ = Describe("Persistent IPs", func() {
WithTimeout(5 * time.Minute).
Should(testenv.ContainConditionVMIReady())

Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPsAtInterfaceByName(networkInterfaceName, ConsistOf(vmiIPsBeforeMigration)))

Expect(testenv.ThisVMI(vmi)()).Should(testenv.MatchIPs(params.getIPsFunc, ConsistOf(vmiIPsBeforeMigration)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I hate how this looks now, but this can be addressed in a follow up.

Just saying: this looks a lot more complex than it should be.

Hiding re-fetching the VM behind a function so I can save an eventually, is also not good IMHO.

@kubevirt-bot kubevirt-bot removed the lgtm Indicates that a PR is ready to be merged. label Oct 8, 2024
In the current tests (where we only run persistent ip tests for
secondary interfaces), the ips of the VMI is extracted via vmi.status.
This is done both directly and via the matchers in testenv library.

In order for the test suite to support running the same tests on primary
UDN VMs, this function needs to be generalized in the following ways:
1. it should align all the IP retrievals to use this new function (right
now the IPs are retrieved both directly accessing vmi.status and via a
function).
2. it should move the function retrieving the IPs to a first-class
functions variable, so that the way IPS are retrieved can be
generalized.
3. it should only have the common parameters needed to get the IPs
(which is the VMI object).

Moving get the IP extraction to a general function, so that it could be
consumed uniformly. Doing this will allow to more easily retrieve the IP
in other cases such as primary UDN, where the IP is currently retrieved
in another way. This will be added in future commits.

Signed-off-by: Ram Lavi <[email protected]>
In the current tests (where we only run persistent ip tests for
secondary interfaces), the VMI is created using the
GenerateAlpineWithMultusVMI function.

In order for the test suite to support running the same tests on primary
UDN VMIs, refactoring the function to use VMI factory function.

Moreover, the NAD Name is changed to be static, as it allows it to be
consumed before runtime, as a test parameter for both the NAD and VMI
creation. This shouldn't interfere with the tests operations as the NADs
are created in different namespaces.

Signed-off-by: Ram Lavi <[email protected]>
Expand the current test to run in a DescribeTableSubtree
context. This is in preparation of adding more interface types in future
commits.
This commit does not add tests, nor change their operation.

Signed-off-by: Ram Lavi <[email protected]>
@kubevirt-bot kubevirt-bot added dco-signoff: no Indicates the PR's author has not DCO signed all their commits. and removed dco-signoff: yes Indicates the PR's author has DCO signed all their commits. labels Oct 8, 2024
@RamLavi
Copy link
Contributor Author

RamLavi commented Oct 8, 2024

Change: Fixed reviews. @maiqueb PTAL

Introducing the functions needed in order to run the current test suite
for primary UDN interfaces:
- defaultNetworkStatusAnnotationIPs - as the getIP function for primary
UDN.
- vmiWithPasst - as the vmi Generating function for primary UDN.
These will be introduced as parameter functions when primary-UDN Entry
is introduced in future commit.

Moreover, since the function getting the IPs for primary-UDN case needs
to return error, this is added to the general function signature.

Signed-off-by: Ram Lavi <[email protected]>
Add a new test subtree entry for primary UDN interfaces, checking
persistentIPs on workload with primary UDN.

Signed-off-by: Ram Lavi <[email protected]>
@kubevirt-bot kubevirt-bot added dco-signoff: yes Indicates the PR's author has DCO signed all their commits. and removed dco-signoff: no Indicates the PR's author has not DCO signed all their commits. labels Oct 8, 2024
@RamLavi
Copy link
Contributor Author

RamLavi commented Oct 8, 2024

Change: fixed dco issue

Copy link
Collaborator

@maiqueb maiqueb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm
/approve
/hold

@qinqon please remove the hold if you're OK with this PR.

Thanks @RamLavi for adding this test.

@kubevirt-bot kubevirt-bot added lgtm Indicates that a PR is ready to be merged. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. labels Oct 8, 2024
@kubevirt-bot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: maiqueb

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubevirt-bot kubevirt-bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Oct 8, 2024
@qinqon
Copy link
Collaborator

qinqon commented Oct 8, 2024

/hold cancel

@kubevirt-bot kubevirt-bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 8, 2024
@kubevirt-bot kubevirt-bot merged commit 925bee6 into kubevirt:main Oct 8, 2024
4 checks passed
Comment on lines +385 to +388
version: 2
ethernets:
eth0:
dhcp4: true`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw this is not required as far as i remember for our machines right ?
going to try and drop it during the effort to convert to managedTap

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has DCO signed all their commits. lgtm Indicates that a PR is ready to be merged. size/L
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants