Skip to content

Commit aa8f72c

Browse files
committed
Add first Longhorn tests
This adds the following tests: - Install Longhorn Using Rancher Charts - Install Longhorn with Custom Configuration - Create Longhorn Volume with Rancher Workloads - Test RBAC Integration with Longhorn - Test Scaling a StatefulSet with a Longhorn PVC Signed-off-by: hamistao <[email protected]>
1 parent 21a00a1 commit aa8f72c

File tree

1 file changed

+350
-0
lines changed

1 file changed

+350
-0
lines changed
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
//go:build validation || pit.daily
2+
3+
package longhorn
4+
5+
import (
6+
"fmt"
7+
"slices"
8+
"strings"
9+
"testing"
10+
11+
"github.com/rancher/norman/types"
12+
"github.com/rancher/shepherd/clients/rancher"
13+
"github.com/rancher/shepherd/clients/rancher/catalog"
14+
management "github.com/rancher/shepherd/clients/rancher/generated/management/v3"
15+
steveV1 "github.com/rancher/shepherd/clients/rancher/v1"
16+
shepherdCharts "github.com/rancher/shepherd/extensions/charts"
17+
"github.com/rancher/shepherd/extensions/clusters"
18+
"github.com/rancher/shepherd/extensions/defaults/namespaces"
19+
"github.com/rancher/shepherd/extensions/kubectl"
20+
shepherdPods "github.com/rancher/shepherd/extensions/workloads/pods"
21+
"github.com/rancher/shepherd/pkg/namegenerator"
22+
"github.com/rancher/shepherd/pkg/session"
23+
"github.com/rancher/tests/actions/charts"
24+
"github.com/rancher/tests/actions/kubeapi/volumes/persistentvolumeclaims"
25+
namespaceActions "github.com/rancher/tests/actions/namespaces"
26+
"github.com/rancher/tests/actions/projects"
27+
"github.com/rancher/tests/actions/rbac"
28+
"github.com/rancher/tests/actions/storage"
29+
"github.com/rancher/tests/actions/workloads/pods"
30+
"github.com/rancher/tests/actions/workloads/statefulset"
31+
"github.com/rancher/tests/interoperability/longhorn"
32+
"github.com/stretchr/testify/require"
33+
"github.com/stretchr/testify/suite"
34+
appv1 "k8s.io/api/apps/v1"
35+
corev1 "k8s.io/api/core/v1"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
)
38+
39+
const (
40+
longhornStorageClass = "longhorn"
41+
longhornStaticStorageClass = "longhorn-static"
42+
createDefaultDiskNodeLabel = "node.longhorn.io/create-default-disk=true"
43+
)
44+
45+
type LonghornTestSuite struct {
46+
suite.Suite
47+
client *rancher.Client
48+
session *session.Session
49+
longhornTestConfig longhorn.TestConfig
50+
cluster *clusters.ClusterMeta
51+
project *management.Project
52+
payloadOpts charts.PayloadOpts
53+
installedLonghorn bool
54+
}
55+
56+
func (l *LonghornTestSuite) TearDownSuite() {
57+
l.session.Cleanup()
58+
}
59+
60+
func (l *LonghornTestSuite) SetupSuite() {
61+
l.session = session.NewSession()
62+
63+
client, err := rancher.NewClient("", l.session)
64+
require.NoError(l.T(), err)
65+
l.client = client
66+
67+
l.longhornTestConfig = *longhorn.GetLonghornTestConfig()
68+
69+
l.cluster, err = clusters.NewClusterMeta(client, client.RancherConfig.ClusterName)
70+
require.NoError(l.T(), err)
71+
72+
projectConfig := &management.Project{
73+
ClusterID: l.cluster.ID,
74+
Name: l.longhornTestConfig.LonghornTestProject,
75+
}
76+
77+
l.project, err = client.Management.Project.Create(projectConfig)
78+
require.NoError(l.T(), err)
79+
80+
// Get latest versions of longhorn
81+
latestLonghornVersion, err := l.client.Catalog.GetLatestChartVersion(charts.LonghornChartName, catalog.RancherChartRepo)
82+
require.NoError(l.T(), err)
83+
84+
l.payloadOpts = charts.PayloadOpts{
85+
Namespace: charts.LonghornNamespace,
86+
Host: l.client.RancherConfig.Host,
87+
InstallOptions: charts.InstallOptions{
88+
Cluster: l.cluster,
89+
Version: latestLonghornVersion,
90+
ProjectID: l.project.ID,
91+
},
92+
}
93+
}
94+
95+
func (l *LonghornTestSuite) TestChartInstall() {
96+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, charts.LonghornNamespace, charts.LonghornChartName)
97+
require.NoError(l.T(), err)
98+
99+
if chart.IsAlreadyInstalled {
100+
l.T().Skip("Skipping installation test because Longhorn is already installed")
101+
}
102+
103+
l.T().Logf("Installing Longhorn chart in cluster [%v] with latest version [%v] in project [%v] and namespace [%v]", l.cluster.Name, l.payloadOpts.Version, l.project.Name, l.payloadOpts.Namespace)
104+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, nil)
105+
require.NoError(l.T(), err)
106+
l.installedLonghorn = true
107+
108+
l.T().Logf("Create nginx deployment with %s PVC on default namespace", longhornStorageClass)
109+
nginxResponse := storage.CreatePVCWorkload(l.T(), l.client, l.cluster.ID, longhornStorageClass)
110+
111+
err = shepherdCharts.WatchAndWaitDeployments(l.client, l.cluster.ID, namespaces.Default, metav1.ListOptions{})
112+
require.NoError(l.T(), err)
113+
114+
steveClient, err := l.client.Steve.ProxyDownstream(l.cluster.ID)
115+
require.NoError(l.T(), err)
116+
117+
pods, err := steveClient.SteveType(shepherdPods.PodResourceSteveType).NamespacedSteveClient(namespaces.Default).List(nil)
118+
require.NotEmpty(l.T(), pods)
119+
require.NoError(l.T(), err)
120+
121+
var podName string
122+
for _, pod := range pods.Data {
123+
if strings.Contains(pod.Name, nginxResponse.ObjectMeta.Name) {
124+
podName = pod.Name
125+
break
126+
}
127+
}
128+
129+
storage.CheckMountedVolume(l.T(), l.client, l.cluster.ID, namespaces.Default, podName, storage.MountPath)
130+
}
131+
132+
func (l *LonghornTestSuite) TestRBACIntegration() {
133+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, charts.LonghornNamespace, charts.LonghornChartName)
134+
require.NoError(l.T(), err)
135+
136+
if !chart.IsAlreadyInstalled {
137+
l.T().Logf("Installing Lonhgorn chart in cluster [%v] with latest version [%v] in project [%v] and namespace [%v]", l.cluster.Name, l.payloadOpts.Version, l.project.Name, l.payloadOpts.Namespace)
138+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, nil)
139+
require.NoError(l.T(), err)
140+
}
141+
142+
cluster, err := l.client.Management.Cluster.ByID(l.cluster.ID)
143+
require.NoError(l.T(), err)
144+
145+
project, namespace, err := projects.CreateProjectAndNamespaceUsingWrangler(l.client, l.cluster.ID)
146+
require.NoError(l.T(), err)
147+
l.T().Logf("Created project: %v", project.Name)
148+
149+
projectUser, projectUserClient, err := rbac.AddUserWithRoleToCluster(l.client, rbac.StandardUser.String(), rbac.ProjectMember.String(), cluster, project)
150+
require.NoError(l.T(), err)
151+
l.T().Logf("Created user: %v", projectUser.Username)
152+
153+
readOnlyUser, readOnlyUserClient, err := rbac.AddUserWithRoleToCluster(l.client, rbac.StandardUser.String(), rbac.ReadOnly.String(), cluster, project)
154+
require.NoError(l.T(), err)
155+
l.T().Logf("Created user: %v", readOnlyUser.Username)
156+
157+
storageClass, err := storage.GetStorageClass(l.client, l.cluster.ID, longhornStorageClass)
158+
require.NoError(l.T(), err)
159+
160+
l.T().Log("Create and delete volume with admin user")
161+
require.NoError(l.T(), storage.CreateAndDeleteVolume(l.client, l.cluster.ID, namespace.Name, storageClass))
162+
163+
l.T().Log("Create and delete volume with project user")
164+
require.NoError(l.T(), storage.CreateAndDeleteVolume(projectUserClient, l.cluster.ID, namespace.Name, storageClass))
165+
166+
l.T().Log("Attempt to create and delete volume with project user on the wrong project")
167+
require.Error(l.T(), storage.CreateAndDeleteVolume(projectUserClient, l.cluster.ID, charts.LonghornNamespace, storageClass))
168+
169+
l.T().Log("Attempt to create and delete volume with read-only user")
170+
require.Error(l.T(), storage.CreateAndDeleteVolume(readOnlyUserClient, l.cluster.ID, namespace.Name, storageClass))
171+
}
172+
173+
func (l *LonghornTestSuite) TestScaleStatefulSetWithPVC() {
174+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, charts.LonghornNamespace, charts.LonghornChartName)
175+
require.NoError(l.T(), err)
176+
177+
if !chart.IsAlreadyInstalled {
178+
l.T().Logf("Installing Lonhgorn chart in cluster [%v] with latest version [%v] in project [%v] and namespace [%v]", l.cluster.Name, l.payloadOpts.Version, l.project.Name, l.payloadOpts.Namespace)
179+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, nil)
180+
require.NoError(l.T(), err)
181+
}
182+
183+
namespaceName := namegenerator.AppendRandomString("lhsts")
184+
namespace, err := namespaceActions.CreateNamespace(l.client, namespaceName, "{}", map[string]string{}, map[string]string{}, l.project)
185+
require.NoError(l.T(), err)
186+
l.T().Logf("Created namespace %s", namespaceName)
187+
188+
podTemplate := pods.CreateContainerAndPodTemplate()
189+
statefulSet, err := statefulset.CreateStatefulSet(l.client, l.cluster.ID, namespace.Name, podTemplate, 3, true, longhornStorageClass)
190+
require.NoError(l.T(), err)
191+
l.T().Logf("Created StetefulSet %s on namespace %s", statefulSet.Name, namespaceName)
192+
193+
// The template we want will always be the last one on the list.
194+
volumeSourceName := statefulSet.Spec.VolumeClaimTemplates[len(statefulSet.Spec.VolumeClaimTemplates)-1].Name
195+
storage.CheckVolumeAllocation(l.T(), l.client, l.cluster.ID, namespace.Name, l.longhornTestConfig.LonghornTestStorageClass, volumeSourceName, storage.MountPath)
196+
197+
var stetefulSetPodReplicas int32 = 5
198+
statefulSet.Spec.Replicas = &stetefulSetPodReplicas
199+
statefulSet, err = statefulset.UpdateStatefulSet(l.client, l.cluster.ID, namespace.Name, statefulSet, true)
200+
require.NoError(l.T(), err)
201+
202+
storage.CheckVolumeAllocation(l.T(), l.client, l.cluster.ID, namespace.Name, l.longhornTestConfig.LonghornTestStorageClass, volumeSourceName, storage.MountPath)
203+
204+
steveClient, err := l.client.Steve.ProxyDownstream(l.cluster.ID)
205+
require.NoError(l.T(), err)
206+
207+
pvcBeforeScaling, err := steveClient.SteveType(persistentvolumeclaims.PersistentVolumeClaimType).NamespacedSteveClient(namespace.Name).List(nil)
208+
require.NoError(l.T(), err)
209+
210+
stetefulSetPodReplicas = 2
211+
statefulSet.Spec.Replicas = &stetefulSetPodReplicas
212+
statefulSet, err = statefulset.UpdateStatefulSet(l.client, l.cluster.ID, namespace.Name, statefulSet, true)
213+
require.NoError(l.T(), err)
214+
215+
l.T().Logf("Verifying old volumes still exist")
216+
volumesAfterScaling, err := steveClient.SteveType(storage.PersistentVolumeEntityType).List(nil)
217+
require.NoError(l.T(), err)
218+
var volumeNamesAfterScaling []string
219+
for _, volume := range volumesAfterScaling.Data {
220+
volumeNamesAfterScaling = append(volumeNamesAfterScaling, volume.Name)
221+
}
222+
223+
var pvcSpec corev1.PersistentVolumeClaimSpec
224+
for _, pvc := range pvcBeforeScaling.Data {
225+
err = steveV1.ConvertToK8sType(pvc.Spec, &pvcSpec)
226+
require.NoError(l.T(), err)
227+
require.True(l.T(), slices.Contains(volumeNamesAfterScaling, pvcSpec.VolumeName))
228+
}
229+
230+
pods, err := steveClient.SteveType(shepherdPods.PodResourceSteveType).NamespacedSteveClient(namespace.Name).List(nil)
231+
require.NoError(l.T(), err)
232+
require.Equal(l.T(), 2, len(pods.Data))
233+
234+
err = steveClient.SteveType(shepherdPods.PodResourceSteveType).Delete(&pods.Data[0])
235+
require.NoError(l.T(), err)
236+
237+
oldPodVolume, err := storage.GetTargetVolume(pods.Data[0], volumeSourceName)
238+
require.NoError(l.T(), err)
239+
l.T().Logf("Deleting pod and checking if the volume bound to PVC %s is successfully reattached", oldPodVolume.PersistentVolumeClaim.ClaimName)
240+
241+
err = shepherdCharts.WatchAndWaitStatefulSets(l.client, l.cluster.ID, namespace.Name, metav1.ListOptions{
242+
FieldSelector: "metadata.name=" + statefulSet.Name,
243+
})
244+
require.NoError(l.T(), err)
245+
246+
newPods, err := steveClient.SteveType(shepherdPods.PodResourceSteveType).NamespacedSteveClient(namespace.Name).List(nil)
247+
require.NoError(l.T(), err)
248+
require.Equal(l.T(), 2, len(newPods.Data))
249+
250+
for _, pod := range newPods.Data {
251+
// We are interested in the pod that was created instead of the one that was deleted.
252+
if pod.Name != pods.Data[1].Name {
253+
newPodVolume, err := storage.GetTargetVolume(pod, volumeSourceName)
254+
require.NoError(l.T(), err)
255+
require.Equal(l.T(), oldPodVolume.PersistentVolumeClaim.ClaimName, newPodVolume.PersistentVolumeClaim.ClaimName)
256+
}
257+
}
258+
}
259+
260+
func (l *LonghornTestSuite) TestChartInstallStaticCustomConfig() {
261+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, charts.LonghornNamespace, charts.LonghornChartName)
262+
require.NoError(l.T(), err)
263+
264+
// If Longhorn was installed by a previous test on this same session, uninstall it to install it again with custom configuration.
265+
// If Longhorn was installed previously to this test run, leave it be and skip this test. This way we allow for running the
266+
// next tests on top of a manually installed Longhorn and avoid accidentally uninstalling something important.
267+
if chart.IsAlreadyInstalled {
268+
if l.installedLonghorn {
269+
l.T().Log("Uninstalling Longhorn as it was installed on a previous test.")
270+
err = charts.UninstallLonghornChart(l.client, charts.LonghornNamespace, l.cluster.ID, l.payloadOpts.Host)
271+
require.NoError(l.T(), err)
272+
} else {
273+
l.T().Skip("Skipping installation test because Longhorn is already installed")
274+
}
275+
}
276+
277+
nodeCollection, err := l.client.Management.Node.List(&types.ListOpts{Filters: map[string]interface{}{
278+
"clusterId": l.cluster.ID,
279+
}})
280+
require.NoError(l.T(), err)
281+
282+
// Label worker nodes to check effectiveness of createDefaultDiskLabeledNodes setting.
283+
// Also save the name of one worker node for future use.
284+
l.T().Log("Label worker nodes with Longhorn's create-default-disk=true")
285+
var workerName string
286+
for _, node := range nodeCollection.Data {
287+
if node.Worker {
288+
labelNodeCommand := []string{"kubectl", "label", "node", node.Hostname, createDefaultDiskNodeLabel}
289+
_, err = kubectl.Command(l.client, nil, l.cluster.ID, labelNodeCommand, "")
290+
require.NoError(l.T(), err)
291+
if workerName == "" {
292+
workerName = node.Hostname
293+
}
294+
}
295+
}
296+
297+
longhornCustomSetting := map[string]any{
298+
"defaultSettings": map[string]any{
299+
"createDefaultDiskLabeledNodes": true,
300+
"defaultDataPath": "/var/lib/longhorn-custom",
301+
"defaultReplicaCount": 2,
302+
"storageOverProvisioningPercentage": 150,
303+
},
304+
}
305+
306+
l.T().Logf("Installing Lonhgorn chart in cluster [%v] with latest version [%v] in project [%v] and namespace [%v]", l.cluster.Name, l.payloadOpts.Version, l.project.Name, l.payloadOpts.Namespace)
307+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, longhornCustomSetting)
308+
require.NoError(l.T(), err)
309+
310+
expectedSettings := map[string]string{
311+
"default-data-path": "/var/lib/longhorn-custom",
312+
"default-replica-count": `{"v1":"2","v2":"2"}`,
313+
"storage-over-provisioning-percentage": "150",
314+
"create-default-disk-labeled-nodes": "true",
315+
}
316+
317+
for setting, expectedValue := range expectedSettings {
318+
getSettingCommand := []string{"kubectl", "-n", charts.LonghornNamespace, "get", "settings.longhorn.io", setting, `-o=jsonpath='{.value}'`}
319+
settingValue, err := kubectl.Command(l.client, nil, l.cluster.ID, getSettingCommand, "")
320+
require.NoError(l.T(), err)
321+
// The output extracted from kubectl has single quotes and a newline on the end.
322+
require.Equal(l.T(), fmt.Sprintf("'%s'\n", expectedValue), settingValue)
323+
}
324+
325+
// Use the "longhorn-static" storage class so we get the expected number of replicas.
326+
// Using the "longhorn" storage class will always result in 3 volume replicas.
327+
l.T().Logf("Create nginx deployment with %s PVC on default namespace", longhornStaticStorageClass)
328+
nginxResponse := storage.CreatePVCWorkload(l.T(), l.client, l.cluster.ID, longhornStaticStorageClass)
329+
330+
nginxSpec := &appv1.DeploymentSpec{}
331+
err = steveV1.ConvertToK8sType(nginxResponse.Spec, nginxSpec)
332+
require.NoError(l.T(), err)
333+
require.NotEmpty(l.T(), nginxSpec.Template.Spec.Volumes[0])
334+
335+
// Even though the Longhorn default for number of replicas is 2, Rancher enforces its own default of 3.
336+
volumeName := nginxSpec.Template.Spec.Volumes[0].Name
337+
checkReplicasCommand := []string{"kubectl", "-n", charts.LonghornNamespace, "get", "volumes.longhorn.io", volumeName, `-o=jsonpath="{.spec.numberOfReplicas}"`}
338+
settingValue, err := kubectl.Command(l.client, nil, l.cluster.ID, checkReplicasCommand, "")
339+
require.NoError(l.T(), err)
340+
require.Equal(l.T(), "\"2\"\n", settingValue)
341+
342+
// Check the node's filesystem contains the expected files.
343+
storage.CheckNodeFilesystem(l.T(), l.client, l.cluster.ID, workerName, "test -d /host/var/lib/longhorn-custom/replicas && test -f /host/var/lib/longhorn-custom/longhorn-disk.cfg", l.project)
344+
}
345+
346+
// In order for 'go test' to run this suite, we need to create
347+
// a normal test function and pass our suite to suite.Run
348+
func TestLonghornTestSuite(t *testing.T) {
349+
suite.Run(t, new(LonghornTestSuite))
350+
}

0 commit comments

Comments
 (0)