-
Notifications
You must be signed in to change notification settings - Fork 29
Add Longhorn UI/API access validation test suite #520
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
689b520
f31878b
87378ae
800d490
999c820
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/rancher/shepherd/clients/rancher" | ||
| "github.com/rancher/shepherd/extensions/defaults" | ||
| "github.com/rancher/shepherd/pkg/namegenerator" | ||
| "github.com/rancher/tests/actions/charts" | ||
| "github.com/rancher/tests/interoperability/longhorn" | ||
| kwait "k8s.io/apimachinery/pkg/util/wait" | ||
| ) | ||
|
|
||
| const ( | ||
| longhornNodeType = "longhorn.io.node" | ||
| longhornSettingType = "longhorn.io.setting" | ||
| longhornVolumeType = "longhorn.io.volume" | ||
| ) | ||
|
|
||
| // LonghornClient represents a client for interacting with Longhorn resources via Rancher API | ||
| type LonghornClient struct { | ||
| Client *rancher.Client | ||
| ClusterID string | ||
| ServiceURL string | ||
| } | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // NewLonghornClient creates a new Longhorn client that uses Rancher Steve API | ||
| func NewLonghornClient(client *rancher.Client, clusterID, serviceURL string) (*LonghornClient, error) { | ||
| longhornClient := &LonghornClient{ | ||
| Client: client, | ||
| ClusterID: clusterID, | ||
| ServiceURL: serviceURL, | ||
| } | ||
|
|
||
| return longhornClient, nil | ||
| } | ||
|
|
||
| // getReplicaCount determines an appropriate replica count for a Longhorn volume | ||
| // based on the number of available Longhorn nodes. It caps the replica count | ||
| // at 3 to preserve the previous default behavior on larger clusters, while | ||
| // ensuring it does not exceed the number of nodes on smaller clusters. | ||
| func getReplicaCount(t *testing.T, lc *LonghornClient) (int, error) { | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return 0, fmt.Errorf("failed to get downstream client for replica count: %w", err) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideia: We could adopt |
||
| } | ||
|
|
||
| longhornNodes, err := steveClient.SteveType(longhornNodeType).NamespacedSteveClient(charts.LonghornNamespace).List(nil) | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return 0, fmt.Errorf("failed to list Longhorn nodes: %w", err) | ||
| } | ||
|
|
||
| nodeCount := len(longhornNodes.Data) | ||
| if nodeCount <= 0 { | ||
| t.Logf("No Longhorn nodes found; defaulting replica count to 1") | ||
| return 1, nil | ||
| } | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Do not exceed the number of nodes, and cap at 3 to match previous behavior. | ||
| if nodeCount >= 3 { | ||
| return 3, nil | ||
| } | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return nodeCount, nil | ||
| } | ||
|
|
||
| // CreateVolume creates a new Longhorn volume via the Rancher Steve API | ||
| func CreateVolume(t *testing.T, lc *LonghornClient) (string, error) { | ||
|
||
| volumeName := namegenerator.AppendRandomString("test-lh-vol") | ||
|
|
||
| replicaCount, err := getReplicaCount(t, lc) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to get downstream client: %w", err) | ||
| } | ||
|
|
||
| // Create volume spec | ||
| volumeSpec := map[string]interface{}{ | ||
| "type": longhornVolumeType, | ||
| "metadata": map[string]interface{}{ | ||
| "name": volumeName, | ||
| "namespace": charts.LonghornNamespace, | ||
| }, | ||
| "spec": map[string]interface{}{ | ||
| "numberOfReplicas": replicaCount, | ||
| "size": "1073741824", // 1Gi in bytes | ||
| "frontend": "blockdev", // Required for data engine v1 | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| } | ||
|
|
||
| t.Logf("Creating Longhorn volume: %s with %d replicas", volumeName, replicaCount) | ||
| _, err = steveClient.SteveType(longhornVolumeType).Create(volumeSpec) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to create volume: %w", err) | ||
| } | ||
|
|
||
| t.Logf("Successfully created volume: %s", volumeName) | ||
| return volumeName, nil | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // ValidateVolumeActive validates that a volume is in an active/detached state and ready to use | ||
| func ValidateVolumeActive(t *testing.T, lc *LonghornClient, volumeName string) error { | ||
| t.Logf("Validating volume %s is active", volumeName) | ||
|
|
||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get downstream client: %w", err) | ||
| } | ||
|
|
||
| err = kwait.PollUntilContextTimeout(context.TODO(), 5*time.Second, defaults.FiveMinuteTimeout, true, func(ctx context.Context) (done bool, err error) { | ||
| volumeID := fmt.Sprintf("%s/%s", charts.LonghornNamespace, volumeName) | ||
| volume, err := steveClient.SteveType(longhornVolumeType).ByID(volumeID) | ||
| if err != nil { | ||
| return false, nil | ||
| } | ||
slickwarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Extract status from the volume | ||
| if volume.Status == nil { | ||
| return false, nil | ||
| } | ||
|
|
||
| statusMap, ok := volume.Status.(map[string]interface{}) | ||
| if !ok { | ||
| return false, nil | ||
| } | ||
|
|
||
| state, _ := statusMap["state"].(string) | ||
| robustness, _ := statusMap["robustness"].(string) | ||
|
|
||
| t.Logf("Volume %s state: %s, robustness: %s", volumeName, state, robustness) | ||
|
|
||
| // Volume is ready when it's in detached state with valid robustness | ||
| // "unknown" robustness is expected for detached volumes with no replicas scheduled | ||
| if state == "detached" && (robustness == "healthy" || robustness == "unknown") { | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return true, nil | ||
| } | ||
|
|
||
| return false, nil | ||
| }) | ||
|
|
||
| if err != nil { | ||
| return fmt.Errorf("volume %s did not become active: %w", volumeName, err) | ||
| } | ||
|
|
||
| t.Logf("Volume %s is active and ready to use", volumeName) | ||
| return nil | ||
| } | ||
|
|
||
| // DeleteVolume deletes a Longhorn volume | ||
| func DeleteVolume(t *testing.T, lc *LonghornClient, volumeName string) error { | ||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get downstream client: %w", err) | ||
| } | ||
|
|
||
| volumeID := fmt.Sprintf("%s/%s", charts.LonghornNamespace, volumeName) | ||
| volume, err := steveClient.SteveType(longhornVolumeType).ByID(volumeID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get volume %s: %w", volumeName, err) | ||
| } | ||
|
|
||
| t.Logf("Deleting volume: %s", volumeName) | ||
| err = steveClient.SteveType(longhornVolumeType).Delete(volume) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to delete volume %s: %w", volumeName, err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // ValidateNodes validates that all Longhorn nodes are in a valid state | ||
| func ValidateNodes(lc *LonghornClient) error { | ||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get downstream client: %w", err) | ||
| } | ||
|
|
||
| nodes, err := steveClient.SteveType(longhornNodeType).NamespacedSteveClient(charts.LonghornNamespace).List(nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to list nodes: %w", err) | ||
| } | ||
|
|
||
| if len(nodes.Data) == 0 { | ||
| return fmt.Errorf("no Longhorn nodes found") | ||
| } | ||
|
|
||
| // Validate each node has valid conditions | ||
| for _, node := range nodes.Data { | ||
| if node.Status == nil { | ||
| return fmt.Errorf("node %s has no status", node.Name) | ||
| } | ||
| } | ||
slickwarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // ValidateSettings validates that Longhorn settings are properly configured | ||
| func ValidateSettings(lc *LonghornClient) error { | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get downstream client: %w", err) | ||
| } | ||
|
|
||
| settings, err := steveClient.SteveType(longhornSettingType).NamespacedSteveClient(charts.LonghornNamespace).List(nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to list settings: %w", err) | ||
| } | ||
|
|
||
| if len(settings.Data) == 0 { | ||
| return fmt.Errorf("no Longhorn settings found") | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // ValidateVolumeInRancherAPI validates that the volume is accessible and in a ready state through Rancher API | ||
| func ValidateVolumeInRancherAPI(t *testing.T, lc *LonghornClient, volumeName string) error { | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| t.Logf("Validating volume %s is accessible through Rancher API", volumeName) | ||
|
|
||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get downstream client: %w", err) | ||
| } | ||
|
|
||
| // Get the volume using the Rancher API path | ||
| volumeID := fmt.Sprintf("%s/%s", charts.LonghornNamespace, volumeName) | ||
| volume, err := steveClient.SteveType(longhornVolumeType).ByID(volumeID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get volume %s through Rancher API: %w", volumeName, err) | ||
| } | ||
|
|
||
| // Validate volume has status | ||
| if volume.Status == nil { | ||
| return fmt.Errorf("volume %s has no status in Rancher API", volumeName) | ||
| } | ||
|
|
||
| statusMap, ok := volume.Status.(map[string]interface{}) | ||
| if !ok { | ||
| return fmt.Errorf("volume %s status is not in expected format", volumeName) | ||
| } | ||
|
|
||
| state, _ := statusMap["state"].(string) | ||
| robustness, _ := statusMap["robustness"].(string) | ||
|
|
||
| t.Logf("Volume %s in Rancher API - state: %s, robustness: %s", volumeName, state, robustness) | ||
|
|
||
| // Verify volume is in a ready state | ||
| if state != "detached" { | ||
| return fmt.Errorf("volume %s is not in detached state through Rancher API, current state: %s", volumeName, state) | ||
| } | ||
|
|
||
| if robustness != "healthy" && robustness != "unknown" { | ||
| return fmt.Errorf("volume %s has invalid robustness through Rancher API: %s", volumeName, robustness) | ||
| } | ||
|
|
||
| t.Logf("Volume %s validated successfully through Rancher API", volumeName) | ||
| return nil | ||
| } | ||
slickwarren marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // ValidateDynamicConfiguration validates Longhorn configuration based on user-provided test config | ||
| func ValidateDynamicConfiguration(t *testing.T, lc *LonghornClient, config longhorn.TestConfig) error { | ||
| steveClient, err := lc.Client.Steve.ProxyDownstream(lc.ClusterID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get downstream client for dynamic validation: %w", err) | ||
| } | ||
|
|
||
| // Validate that the configured storage class exists | ||
| t.Logf("Validating configured storage class: %s", config.LonghornTestStorageClass) | ||
| storageClasses, err := steveClient.SteveType("storage.k8s.io.storageclass").List(nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to list storage classes: %w", err) | ||
| } | ||
|
|
||
| found := false | ||
| for _, sc := range storageClasses.Data { | ||
| if sc.Name == config.LonghornTestStorageClass { | ||
| found = true | ||
| t.Logf("Found configured storage class: %s", config.LonghornTestStorageClass) | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if !found { | ||
| return fmt.Errorf("configured storage class %s not found", config.LonghornTestStorageClass) | ||
| } | ||
|
|
||
| // Validate settings exist | ||
| settings, err := steveClient.SteveType(longhornSettingType).NamespacedSteveClient(charts.LonghornNamespace).List(nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to list settings: %w", err) | ||
| } | ||
|
|
||
| t.Logf("Successfully validated Longhorn configuration with %d settings", len(settings.Data)) | ||
| t.Logf("Validated storage class: %s from test configuration", config.LonghornTestStorageClass) | ||
|
|
||
| return nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.