|
| 1 | +# OpenShift Node E2E Tests - Developer Guide |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This directory contains E2E tests for OpenShift node-related functionality using the Ginkgo test framework. When writing tests here, **ALWAYS use the utility functions in `node_utils.go`** instead of implementing your own. |
| 6 | + |
| 7 | +## Key Utility Functions Available |
| 8 | + |
| 9 | +**See `node_utils.go` for current function signatures.** The lists below describe exported (public) functions only. Lowercase helper functions are internal implementation details and not documented here. |
| 10 | + |
| 11 | +### Node Selection and Filtering |
| 12 | + |
| 13 | +- `GetNodesByLabel()` - Get nodes matching a label selector |
| 14 | +- `GetControlPlaneNodes()` - Get all control plane nodes (handles both master and control-plane labels) |
| 15 | +- `GetPureWorkerNodes()` - Filter out nodes that have both worker and control-plane roles (important for SNO clusters) |
| 16 | +- `GetCNVWorkerNodeName()` - Get a randomly selected CNV-enabled worker node |
| 17 | + |
| 18 | +### Executing Commands on Nodes |
| 19 | + |
| 20 | +- `ExecOnNodeWithChroot()` - Run command on a node using `oc debug` with `chroot /host` (most common) |
| 21 | +- `ExecOnNodeWithNsenter()` - Run command on a node using `nsenter` to access host namespaces (required for swap operations) |
| 22 | + |
| 23 | +**Important**: These functions handle the `oc debug` boilerplate for you. Never manually construct `oc debug node/...` commands. |
| 24 | + |
| 25 | +### Managing Kubelet Configuration |
| 26 | + |
| 27 | +- `GetKubeletConfigFromNode()` - Get kubelet configuration from a node via the configz API |
| 28 | +- `CreateDropInFile()` - Create a drop-in configuration file on a node |
| 29 | +- `RemoveDropInFile()` - Remove a drop-in configuration file from a node |
| 30 | +- `LoadConfigFromFile()` - Read kubelet configuration from a YAML file in testdata |
| 31 | +- `EnsureDropInDirectoryExists()` - Create drop-in directory on all worker nodes if it doesn't exist |
| 32 | + |
| 33 | +### Kubelet Lifecycle Management |
| 34 | + |
| 35 | +- `RestartKubeletOnNode()` - Restart kubelet service on a node (with automatic retry on transient network errors) |
| 36 | +- `WaitForNodeToBeReady()` - Wait for a node to reach Ready condition |
| 37 | +- `IsNodeInReadyState()` - Check if a node is currently in Ready condition |
| 38 | +- `CleanupDropInAndRestartKubelet()` - Remove drop-in file, restart kubelet, and wait for node Ready (cleanup pattern) |
| 39 | + |
| 40 | +### CNV (OpenShift Virtualization) Operations |
| 41 | + |
| 42 | +- `IsCNVInstalled()` - Check if CNV operator is installed in the cluster |
| 43 | +- `InstallCNVOperator()` - Install CNV operator (creates namespace, subscription, HyperConverged CR, labels nodes, waits for MCP) |
| 44 | +- `UninstallCNVOperator()` - Uninstall CNV operator and clean up all resources |
| 45 | +- `LabelWorkerNodesForCNV()` - Label all worker nodes with `kubevirt.io/schedulable=true` |
| 46 | +- `UnlabelWorkerNodesForCNV()` - Remove CNV scheduling labels from worker nodes |
| 47 | + |
| 48 | +### MachineConfigPool Operations |
| 49 | + |
| 50 | +- `WaitForMCP()` - Wait for a MachineConfigPool to finish updating (returns error if degraded) |
| 51 | +- `GetWorkerGeneratedKubeletMC()` - Get the highest numbered `worker-generated-kubelet` MachineConfig |
| 52 | + |
| 53 | +## Test Structure Best Practices |
| 54 | + |
| 55 | +**Note:** Code examples below are illustrative. Check `node_utils.go` for current function signatures. |
| 56 | + |
| 57 | +### Standard Test Pattern |
| 58 | + |
| 59 | +```go |
| 60 | +var _ = g.Describe("[sig-node][Feature:MyFeature] Description", func() { |
| 61 | + defer g.GinkgoRecover() |
| 62 | + |
| 63 | + var oc = exutil.NewCLI("test-name") |
| 64 | + |
| 65 | + g.BeforeAll(func(ctx context.Context) { |
| 66 | + // Setup that applies to all tests in this Describe block |
| 67 | + }) |
| 68 | + |
| 69 | + g.AfterAll(func(ctx context.Context) { |
| 70 | + // Cleanup after all tests |
| 71 | + }) |
| 72 | + |
| 73 | + g.It("should do something", func(ctx context.Context) { |
| 74 | + // Test implementation using context.Context |
| 75 | + nodes, err := GetNodesByLabel(ctx, oc, "node-role.kubernetes.io/worker") |
| 76 | + o.Expect(err).NotTo(o.HaveOccurred()) |
| 77 | + }) |
| 78 | +}) |
| 79 | +``` |
| 80 | + |
| 81 | +### Always Use Context |
| 82 | + |
| 83 | +Always pass `ctx` when the helper or test function signature requires it. Most utility functions in `node_utils.go` accept `context.Context` (check the function signature). Ginkgo test functions should always use the `ctx` parameter: |
| 84 | + |
| 85 | +```go |
| 86 | +// Good - test function receives and uses ctx |
| 87 | +g.It("test name", func(ctx context.Context) { |
| 88 | + nodes, err := GetNodesByLabel(ctx, oc, "label") // Helper needs ctx |
| 89 | +}) |
| 90 | + |
| 91 | +// Bad - missing context parameter |
| 92 | +g.It("test name", func() { |
| 93 | + // Cannot call context-aware helpers |
| 94 | +}) |
| 95 | +``` |
| 96 | + |
| 97 | +**Note:** Some helpers like `ExecOnNodeWithChroot()` and `CreateDropInFile()` do not require `ctx`. Check the function signature in `node_utils.go`. |
| 98 | + |
| 99 | +### Common Patterns |
| 100 | + |
| 101 | +**Note:** Examples below are illustrative. Check `node_utils.go` for current function signatures. |
| 102 | + |
| 103 | +**Finding a worker node:** |
| 104 | +```go |
| 105 | +allWorkerNodes, err := GetNodesByLabel(ctx, oc, "node-role.kubernetes.io/worker") |
| 106 | +o.Expect(err).NotTo(o.HaveOccurred()) |
| 107 | +o.Expect(len(allWorkerNodes)).Should(o.BeNumerically(">", 0)) |
| 108 | + |
| 109 | +// Filter out nodes that are also control plane (SNO handling) |
| 110 | +workerNodes := GetPureWorkerNodes(allWorkerNodes) |
| 111 | +o.Expect(len(workerNodes)).Should(o.BeNumerically(">", 0), "expected at least one pure worker node") |
| 112 | +nodeName := workerNodes[0].Name |
| 113 | +``` |
| 114 | + |
| 115 | +**Safely modifying kubelet config:** |
| 116 | +```go |
| 117 | +// In test setup |
| 118 | +dropInPath := "/etc/kubelet.conf.d/99-my-test.conf" |
| 119 | +configContent := LoadConfigFromFile(exutil.FixturePath("testdata", "node", "my-config.yaml")) |
| 120 | +err := CreateDropInFile(oc, nodeName, dropInPath, configContent) |
| 121 | +o.Expect(err).NotTo(o.HaveOccurred()) |
| 122 | + |
| 123 | +err = RestartKubeletOnNode(ctx, oc, nodeName) |
| 124 | +o.Expect(err).NotTo(o.HaveOccurred()) |
| 125 | + |
| 126 | +WaitForNodeToBeReady(ctx, oc, nodeName) |
| 127 | + |
| 128 | +// In cleanup (defer or AfterEach) |
| 129 | +CleanupDropInAndRestartKubelet(ctx, oc, nodeName, dropInPath) |
| 130 | +``` |
| 131 | + |
| 132 | +**Working with CNV:** |
| 133 | +```go |
| 134 | +var cnvInstalledByTest bool |
| 135 | + |
| 136 | +g.BeforeAll(func(ctx context.Context) { |
| 137 | + if !IsCNVInstalled(ctx, oc) { |
| 138 | + err := InstallCNVOperator(ctx, oc) |
| 139 | + if err != nil { |
| 140 | + e2eskipper.Skipf("Failed to install CNV: %v", err) |
| 141 | + } |
| 142 | + cnvInstalledByTest = true |
| 143 | + } |
| 144 | +}) |
| 145 | + |
| 146 | +g.AfterAll(func(ctx context.Context) { |
| 147 | + if cnvInstalledByTest { |
| 148 | + UninstallCNVOperator(ctx, oc) |
| 149 | + } |
| 150 | +}) |
| 151 | +``` |
| 152 | + |
| 153 | +## Common Mistakes to Avoid |
| 154 | + |
| 155 | +1. **Don't manually construct `oc debug` commands** - use `ExecOnNodeWithChroot()` or `ExecOnNodeWithNsenter()` |
| 156 | + |
| 157 | +2. **Don't forget to handle SNO clusters** - use `GetPureWorkerNodes()` to filter out nodes with dual roles |
| 158 | + |
| 159 | +3. **Don't skip context propagation** - always pass `ctx` to utility functions |
| 160 | + |
| 161 | +4. **Don't forget cleanup** - use `defer` or `g.AfterEach` with `CleanupDropInAndRestartKubelet()` |
| 162 | + |
| 163 | +5. **Don't ignore MCP rollouts** - after MachineConfig changes, use `WaitForMCP()` to ensure stability |
| 164 | + |
| 165 | +6. **Don't assume swap operations work with chroot** - use `ExecOnNodeWithNsenter()` for swap commands |
| 166 | + |
| 167 | +## Constants and GVRs Available |
| 168 | + |
| 169 | +The file defines commonly used constants: |
| 170 | +- `debugNamespace = "openshift-machine-config-operator"` |
| 171 | +- `cnvNamespace = "openshift-cnv"` |
| 172 | +- CNV-related resource names |
| 173 | + |
| 174 | +And GVRs for dynamic client operations: |
| 175 | +- `subscriptionGVR`, `operatorGroupGVR`, `hyperConvergedGVR`, `csvGVR`, `mcpGVR` |
| 176 | + |
| 177 | +## Example: Complete Test |
| 178 | + |
| 179 | +See `node_swap_cnv.go` for a complete example showing: |
| 180 | +- BeforeAll/AfterAll hooks |
| 181 | +- CNV installation/cleanup |
| 182 | +- Using multiple utility functions together |
| 183 | +- Proper error handling and skip conditions |
| 184 | +- Working with drop-in files and kubelet restarts |
| 185 | + |
| 186 | +## Getting Help |
| 187 | + |
| 188 | +- Read the function documentation in `node_utils.go` |
| 189 | +- Look at existing tests in this directory for patterns |
| 190 | +- Check testdata files in `testdata/node/` for config examples |
0 commit comments