Skip to content

Commit 0ebe37f

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 Signed-off-by: hamistao <[email protected]>
1 parent ce96633 commit 0ebe37f

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
//go:build validation || pit.daily
2+
3+
package longhorn
4+
5+
import (
6+
"fmt"
7+
"strings"
8+
"testing"
9+
10+
"github.com/rancher/norman/types"
11+
"github.com/rancher/shepherd/clients/rancher"
12+
"github.com/rancher/shepherd/clients/rancher/catalog"
13+
management "github.com/rancher/shepherd/clients/rancher/generated/management/v3"
14+
steveV1 "github.com/rancher/shepherd/clients/rancher/v1"
15+
shepherdCharts "github.com/rancher/shepherd/extensions/charts"
16+
"github.com/rancher/shepherd/extensions/clusters"
17+
"github.com/rancher/shepherd/extensions/defaults/namespaces"
18+
"github.com/rancher/shepherd/extensions/kubeconfig"
19+
"github.com/rancher/shepherd/extensions/kubectl"
20+
"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/cloudprovider"
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/stretchr/testify/require"
29+
"github.com/stretchr/testify/suite"
30+
appv1 "k8s.io/api/apps/v1"
31+
corev1 "k8s.io/api/core/v1"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/client-go/rest"
34+
)
35+
36+
var (
37+
longhornNamespace = "longhorn-system"
38+
longhornChartName = "longhorn"
39+
longhornProjectName = "longhorn-test"
40+
longhornStorageClass = "longhorn"
41+
longhornStaticDefaultStorageClass = "longhorn-static"
42+
debugNamespace = namegenerator.AppendRandomString("debug-")
43+
)
44+
45+
type LonghornTestSuite struct {
46+
suite.Suite
47+
client *rancher.Client
48+
session *session.Session
49+
cluster *clusters.ClusterMeta
50+
project *management.Project
51+
payloadOpts charts.PayloadOpts
52+
installedLonghorn bool
53+
}
54+
55+
func (l *LonghornTestSuite) TearDownSuite() {
56+
l.session.Cleanup()
57+
}
58+
59+
func (l *LonghornTestSuite) SetupSuite() {
60+
l.session = session.NewSession()
61+
62+
client, err := rancher.NewClient("", l.session)
63+
require.NoError(l.T(), err)
64+
65+
l.client = client
66+
67+
l.cluster, err = clusters.NewClusterMeta(client, client.RancherConfig.ClusterName)
68+
require.NoError(l.T(), err)
69+
70+
projectConfig := &management.Project{
71+
ClusterID: l.cluster.ID,
72+
Name: longhornProjectName,
73+
}
74+
75+
l.project, err = client.Management.Project.Create(projectConfig)
76+
require.NoError(l.T(), err)
77+
78+
// Get latest versions of longhorn
79+
latestLonghornVersion, err := l.client.Catalog.GetLatestChartVersion(longhornChartName, catalog.RancherChartRepo)
80+
require.NoError(l.T(), err)
81+
82+
l.payloadOpts = charts.PayloadOpts{
83+
Namespace: longhornNamespace,
84+
Host: l.client.RancherConfig.Host,
85+
InstallOptions: charts.InstallOptions{
86+
Cluster: l.cluster,
87+
Version: latestLonghornVersion,
88+
ProjectID: l.project.ID,
89+
},
90+
}
91+
}
92+
93+
func (l *LonghornTestSuite) TestChartInstall() {
94+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, longhornNamespace, longhornChartName)
95+
require.NoError(l.T(), err)
96+
97+
if chart.IsAlreadyInstalled {
98+
l.T().Skip("Skipping installation test because Longhorn is already installed")
99+
}
100+
101+
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)
102+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, nil)
103+
require.NoError(l.T(), err)
104+
l.installedLonghorn = true
105+
}
106+
107+
func (l *LonghornTestSuite) TestVolumeCreationOnWorkload() {
108+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, longhornNamespace, longhornChartName)
109+
require.NoError(l.T(), err)
110+
111+
if !chart.IsAlreadyInstalled {
112+
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)
113+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, nil)
114+
require.NoError(l.T(), err)
115+
}
116+
117+
l.T().Logf("Create nginx deployment with %s PVC on default namespace", longhornStorageClass)
118+
nginxResponse := cloudprovider.CreatePVCWorkload(l.T(), l.client, l.cluster.ID, longhornStorageClass)
119+
120+
err = shepherdCharts.WatchAndWaitDeployments(l.client, l.cluster.ID, namespaces.Default, metav1.ListOptions{})
121+
require.NoError(l.T(), err)
122+
123+
kubeConfig, err := kubeconfig.GetKubeconfig(l.client, l.cluster.ID)
124+
require.NoError(l.T(), err)
125+
126+
var restConfig *rest.Config
127+
restConfig, err = (*kubeConfig).ClientConfig()
128+
require.NoError(l.T(), err)
129+
130+
steveClient, err := l.client.Steve.ProxyDownstream(l.cluster.ID)
131+
require.NoError(l.T(), err)
132+
133+
pods, err := steveClient.SteveType(pods.PodResourceSteveType).NamespacedSteveClient(namespaces.Default).List(nil)
134+
require.NotEmpty(l.T(), pods)
135+
require.NoError(l.T(), err)
136+
137+
var podName string
138+
for _, pod := range pods.Data {
139+
if strings.Contains(pod.Name, nginxResponse.ObjectMeta.Name) {
140+
podName = pod.Name
141+
break
142+
}
143+
}
144+
145+
l.T().Logf("Write to mounted volume on pod [%v]", podName)
146+
writeToMountedVolume := []string{"touch", "/auto-mnt/test-file"}
147+
_, err = kubeconfig.KubectlExec(restConfig, podName, namespaces.Default, writeToMountedVolume)
148+
require.NoError(l.T(), err)
149+
150+
checkFileOnVolume := []string{"stat", "/auto-mnt/test-file"}
151+
_, err = kubeconfig.KubectlExec(restConfig, podName, namespaces.Default, checkFileOnVolume)
152+
require.NoError(l.T(), err)
153+
}
154+
155+
func (l *LonghornTestSuite) TestRBACIntegration() {
156+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, longhornNamespace, longhornChartName)
157+
require.NoError(l.T(), err)
158+
159+
if !chart.IsAlreadyInstalled {
160+
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)
161+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, nil)
162+
require.NoError(l.T(), err)
163+
}
164+
165+
cluster, err := l.client.Management.Cluster.ByID(l.cluster.ID)
166+
require.NoError(l.T(), err)
167+
168+
project, namespace, err := projects.CreateProjectAndNamespaceUsingWrangler(l.client, l.cluster.ID)
169+
require.NoError(l.T(), err)
170+
l.T().Logf("Created project: %v", project.Name)
171+
172+
projectUser, projectUserClient, err := rbac.AddUserWithRoleToCluster(l.client, rbac.StandardUser.String(), rbac.ProjectMember.String(), cluster, project)
173+
require.NoError(l.T(), err)
174+
l.T().Logf("Created user: %v", projectUser.Username)
175+
176+
readOnlyUser, readOnlyUserClient, err := rbac.AddUserWithRoleToCluster(l.client, rbac.StandardUser.String(), rbac.ReadOnly.String(), cluster, project)
177+
require.NoError(l.T(), err)
178+
l.T().Logf("Created user: %v", readOnlyUser.Username)
179+
180+
storageClass, err := cloudprovider.GetStorageClass(l.client, l.cluster.ID, longhornStorageClass)
181+
require.NoError(l.T(), err)
182+
183+
l.T().Log("Create and delete volume with admin user")
184+
require.NoError(l.T(), charts.CreateAndDeleteLonghornVolume(l.client, l.cluster.ID, namespace.Name, storageClass))
185+
186+
l.T().Log("Create and delete volume with project user")
187+
require.NoError(l.T(), charts.CreateAndDeleteLonghornVolume(projectUserClient, l.cluster.ID, namespace.Name, storageClass))
188+
189+
l.T().Log("Attempt to create and delete volume with project user on the wrong project")
190+
require.Error(l.T(), charts.CreateAndDeleteLonghornVolume(projectUserClient, l.cluster.ID, longhornNamespace, storageClass))
191+
192+
l.T().Log("Attempt to create and delete volume with read-only user")
193+
require.Error(l.T(), charts.CreateAndDeleteLonghornVolume(readOnlyUserClient, l.cluster.ID, namespace.Name, storageClass))
194+
}
195+
196+
func (l *LonghornTestSuite) TestChartInstallCustomConfig() {
197+
chart, err := shepherdCharts.GetChartStatus(l.client, l.cluster.ID, longhornNamespace, longhornChartName)
198+
require.NoError(l.T(), err)
199+
200+
// If Longhorn was installed by a previous test on this same session, uninstall it to install it again with custom configuration.
201+
// If Longhorn was installed previously to this test run, leave it be and skip this test. This way we allow for running the
202+
// next tests on top of a manually installed Longhorn and avoid accidentally uninstalling something important.
203+
if chart.IsAlreadyInstalled {
204+
if l.installedLonghorn {
205+
l.T().Log("Uninstalling Longhorn that was installed on the previous test.")
206+
err = charts.UninstallLonghornChart(l.client, longhornNamespace, l.cluster.ID, l.payloadOpts.Host)
207+
require.NoError(l.T(), err)
208+
} else {
209+
l.T().Skip("Skipping installation test because Longhorn is already installed")
210+
}
211+
}
212+
213+
nodeCollection, err := l.client.Management.Node.List(&types.ListOpts{Filters: map[string]interface{}{
214+
"clusterId": l.cluster.ID,
215+
}})
216+
require.NoError(l.T(), err)
217+
218+
// Label worker nodes to check effectiveness of createDefaultDiskLabeledNodes setting.
219+
// Also save the name of one worker node for future use.
220+
l.T().Log("Label worker nodes with Longhorn's create-default-disk=true")
221+
var workerName string
222+
for _, node := range nodeCollection.Data {
223+
if node.Worker {
224+
labelNodeCommand := []string{"kubectl", "label", "node", node.Hostname, "node.longhorn.io/create-default-disk=true"}
225+
_, err = kubectl.Command(l.client, nil, l.cluster.ID, labelNodeCommand, "")
226+
require.NoError(l.T(), err)
227+
if workerName == "" {
228+
workerName = node.Hostname
229+
}
230+
}
231+
}
232+
233+
longhornCustomSetting := map[string]any{
234+
"defaultSettings": map[string]any{
235+
"createDefaultDiskLabeledNodes": true,
236+
"defaultDataPath": "/var/lib/longhorn-custom",
237+
"defaultReplicaCount": 2,
238+
"storageOverProvisioningPercentage": 150,
239+
},
240+
}
241+
242+
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)
243+
err = charts.InstallLonghornChart(l.client, l.payloadOpts, longhornCustomSetting)
244+
require.NoError(l.T(), err)
245+
246+
expectedSettings := map[string]string{
247+
"default-data-path": "/var/lib/longhorn-custom",
248+
"default-replica-count": "2",
249+
"storage-over-provisioning-percentage": "150",
250+
"create-default-disk-labeled-nodes": "true",
251+
}
252+
253+
for setting, expectedValue := range expectedSettings {
254+
getSettingCommand := []string{"kubectl", "-n", longhornNamespace, "get", "settings.longhorn.io", setting, `-o=jsonpath='{.value}'`}
255+
settingValue, err := kubectl.Command(l.client, nil, l.cluster.ID, getSettingCommand, "")
256+
require.NoError(l.T(), err)
257+
// The output extracted from kubectl has single quotes and a newline on the end.
258+
require.Equal(l.T(), fmt.Sprintf("'%s'\n", expectedValue), settingValue)
259+
}
260+
261+
// Use the "longhorn-static" storage class so we get the expected number of replicas.
262+
// Using the "longhorn" storage class will always result in 3 volume replicas.
263+
l.T().Logf("Create nginx deployment with %s PVC on default namespace", longhornStaticDefaultStorageClass)
264+
nginxResponse := cloudprovider.CreatePVCWorkload(l.T(), l.client, l.cluster.ID, longhornStaticDefaultStorageClass)
265+
266+
// Fetch the name of the created volume from the nginx response.
267+
nginxSpec := &appv1.DeploymentSpec{}
268+
err = steveV1.ConvertToK8sType(nginxResponse.Spec, nginxSpec)
269+
require.NoError(l.T(), err)
270+
require.NotEmpty(l.T(), nginxSpec.Template.Spec.Volumes[0])
271+
272+
// Even though the Longhorn default for number of replicas is 2, Rancher enforces its own default of 3.
273+
volumeName := nginxSpec.Template.Spec.Volumes[0].Name
274+
checkReplicasCommand := []string{"kubectl", "-n", longhornNamespace, "get", "volumes.longhorn.io", volumeName, `-o=jsonpath="{.spec.numberOfReplicas}"`}
275+
settingValue, err := kubectl.Command(l.client, nil, l.cluster.ID, checkReplicasCommand, "")
276+
require.NoError(l.T(), err)
277+
require.Equal(l.T(), fmt.Sprintf("\"%v\"\n", expectedSettings["default-replica-count"]), settingValue)
278+
279+
// Create a new namespace and a debug pod within it to check the host filesystem for the custom Longhorn data directory.
280+
// We do this in a separate namespace to ease cleanup.
281+
l.T().Logf("Create namespace [%v] to check node filesystem with debugger pod", debugNamespace)
282+
createdNamespace, err := namespaceActions.CreateNamespace(l.client, debugNamespace, "{}", nil, nil, l.project)
283+
require.NoError(l.T(), err)
284+
285+
steveClient, err := l.client.Steve.ProxyDownstream(l.cluster.ID)
286+
require.NoError(l.T(), err)
287+
288+
l.session.RegisterCleanupFunc(func() error {
289+
return steveClient.SteveType(namespaceActions.NamespaceSteveType).Delete(createdNamespace)
290+
})
291+
292+
checkDataPathCommand := []string{"kubectl", "debug", "node/" + workerName, "-n", debugNamespace, "--profile=general", "--image=busybox", "--", "/bin/sh", "-c", "test -d /host/var/lib/longhorn-custom/replicas && test -f /host/var/lib/longhorn-custom/longhorn-disk.cfg"}
293+
_, err = kubectl.Command(l.client, nil, l.cluster.ID, checkDataPathCommand, "")
294+
require.NoError(l.T(), err)
295+
296+
waitForPodCommand := []string{"kubectl", "wait", "--for=jsonpath='{.status.phase}'=Succeeded", "-n", debugNamespace, "pod", "--all"}
297+
_, err = kubectl.Command(l.client, nil, l.cluster.ID, waitForPodCommand, "")
298+
require.NoError(l.T(), err)
299+
300+
debugPods, err := steveClient.SteveType(pods.PodResourceSteveType).NamespacedSteveClient(debugNamespace).List(nil)
301+
require.NotEmpty(l.T(), debugPods)
302+
require.NoError(l.T(), err)
303+
304+
podStatus := &corev1.PodStatus{}
305+
err = steveV1.ConvertToK8sType(debugPods.Data[0].Status, podStatus)
306+
require.NoError(l.T(), err)
307+
require.Equal(l.T(), "Succeeded", string(podStatus.Phase))
308+
}
309+
310+
// In order for 'go test' to run this suite, we need to create
311+
// a normal test function and pass our suite to suite.Run
312+
func TestLonghornTestSuite(t *testing.T) {
313+
suite.Run(t, new(LonghornTestSuite))
314+
}

0 commit comments

Comments
 (0)