diff --git a/cmd/directpv/main.go b/cmd/directpv/main.go index cba0b817..d7cf3db1 100644 --- a/cmd/directpv/main.go +++ b/cmd/directpv/main.go @@ -25,10 +25,10 @@ import ( "syscall" "time" + "github.com/minio/directpv/pkg/admin/installer" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/installer" "github.com/spf13/cobra" "github.com/spf13/viper" "k8s.io/klog/v2" diff --git a/cmd/kubectl-directpv/clean.go b/cmd/kubectl-directpv/clean.go index 4afd69d3..a572a07a 100644 --- a/cmd/kubectl-directpv/clean.go +++ b/cmd/kubectl-directpv/clean.go @@ -1,5 +1,5 @@ // This file is part of MinIO DirectPV -// Copyright (c) 2021, 2022 MinIO, Inc. +// Copyright (c) 2024 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -19,20 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" - "github.com/minio/directpv/pkg/types" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var cleanCmd = &cobra.Command{ @@ -68,7 +60,7 @@ var cleanCmd = &cobra.Command{ volumeNameArgs = args if err := validateCleanCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -138,65 +130,21 @@ func validateCleanCmd() error { } func cleanMain(ctx context.Context) { - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := volume.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - DriveIDSelector(toLabelValues(driveIDArgs)). - PodNameSelector(toLabelValues(podNameArgs)). - PodNSSelector(toLabelValues(podNSArgs)). - StatusSelector(volumeStatusSelectors). - VolumeNameSelector(volumeNameArgs). - List(ctx) - - matchFunc := func(volume *types.Volume) bool { - pv, err := k8s.KubeClient().CoreV1().PersistentVolumes().Get(ctx, volume.Name, metav1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - return true - } - utils.Eprintf(quietFlag, true, "unable to get PV for volume %v; %v\n", volume.Name, err) - return false - } - switch pv.Status.Phase { - case corev1.VolumeReleased, corev1.VolumeFailed: - return true - default: - return false - } - } - - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - if !matchFunc(&result.Volume) { - continue - } - - result.Volume.RemovePVProtection() - - if dryRunFlag { - continue - } - - if _, err := client.VolumeClient().Update(ctx, &result.Volume, metav1.UpdateOptions{ - TypeMeta: types.NewVolumeTypeMeta(), - }); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) - os.Exit(1) - } - if err := client.VolumeClient().Delete(ctx, result.Volume.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { - utils.Eprintf(quietFlag, true, "%v\n", err) - os.Exit(1) - } - - if !quietFlag { - fmt.Println("Removing volume", result.Volume.Name) - } + _, err := adminClient.Clean( + ctx, + admin.CleanArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + DriveIDs: driveIDArgs, + PodNames: podNameArgs, + PodNamespaces: podNSArgs, + VolumeStatus: volumeStatusSelectors, + VolumeNames: volumeNameArgs, + }, + logFunc, + ) + if err != nil { + eprintf(true, "%v\n", err) + os.Exit(1) } } diff --git a/cmd/kubectl-directpv/cordon.go b/cmd/kubectl-directpv/cordon.go index bb80f4a5..752e5b30 100644 --- a/cmd/kubectl-directpv/cordon.go +++ b/cmd/kubectl-directpv/cordon.go @@ -19,18 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var cordonCmd = &cobra.Command{ @@ -60,7 +54,7 @@ var cordonCmd = &cobra.Command{ driveIDArgs = args if err := validateCordonCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -116,64 +110,19 @@ func validateCordonCmd() error { } func cordonMain(ctx context.Context) { - var processed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - StatusSelector(driveStatusSelectors). - DriveIDSelector(driveIDSelectors). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - - if result.Drive.IsUnschedulable() { - continue - } - - volumes := result.Drive.GetVolumes() - if len(volumes) != 0 { - for vresult := range volume.NewLister().VolumeNameSelector(volumes).IgnoreNotFound(true).List(ctx) { - if vresult.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", vresult.Err) - os.Exit(1) - } - - if vresult.Volume.Status.Status == directpvtypes.VolumeStatusPending { - utils.Eprintf(quietFlag, true, "unable to cordon drive %v; pending volumes found\n", result.Drive.GetDriveID()) - os.Exit(1) - } - } - } - - result.Drive.Unschedulable() - if !dryRunFlag { - if _, err := client.DriveClient().Update(ctx, &result.Drive, metav1.UpdateOptions{}); err != nil { - utils.Eprintf(quietFlag, true, "unable to cordon drive %v; %v\n", result.Drive.GetDriveID(), err) - os.Exit(1) - } - } - - if !quietFlag { - fmt.Printf("Drive %v/%v cordoned\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()) - } - } - - if !processed { - if allFlag { - utils.Eprintf(quietFlag, false, "No resources found\n") - } else { - utils.Eprintf(quietFlag, false, "No matching resources found\n") - } - + _, err := adminClient.Cordon( + ctx, + admin.CordonArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + Status: driveStatusSelectors, + DriveIDs: driveIDSelectors, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/discover.go b/cmd/kubectl-directpv/discover.go index 39322c60..61cb6bf9 100644 --- a/cmd/kubectl-directpv/discover.go +++ b/cmd/kubectl-directpv/discover.go @@ -28,16 +28,13 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" + "github.com/minio/directpv/pkg/admin" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/node" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/util/retry" ) var ( @@ -71,7 +68,7 @@ var discoverCmd = &cobra.Command{ ), Run: func(c *cobra.Command, _ []string) { if err := validateDiscoverCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } discoverMain(c.Context()) @@ -95,33 +92,6 @@ func validateDiscoverCmd() error { return validateDriveNameArgs() } -func toInitConfig(resultMap map[directpvtypes.NodeID][]types.Device) InitConfig { - nodeInfo := []NodeInfo{} - initConfig := NewInitConfig() - for node, devices := range resultMap { - driveInfo := []DriveInfo{} - for _, device := range devices { - if device.DeniedReason != "" { - continue - } - driveInfo = append(driveInfo, DriveInfo{ - ID: device.ID, - Name: device.Name, - Size: device.Size, - Make: device.Make, - FS: device.FSType, - Select: driveSelectedValue, - }) - } - nodeInfo = append(nodeInfo, NodeInfo{ - Name: node, - Drives: driveInfo, - }) - } - initConfig.Nodes = nodeInfo - return initConfig -} - func showDevices(resultMap map[directpvtypes.NodeID][]types.Device) error { writer := newTableWriter( table.Row{ @@ -182,14 +152,14 @@ func showDevices(resultMap map[directpvtypes.NodeID][]types.Device) error { } if writer.Length() == 0 || !foundAvailableDrive { - utils.Eprintf(false, false, "%v\n", color.HiYellowString("No drives are available to initialize")) + eprintf(false, color.HiYellowString("No drives are available to initialize")+"\n") return errDiscoveryFailed } return nil } -func writeInitConfig(config InitConfig) error { +func writeInitConfig(config admin.InitConfig) error { f, err := os.OpenFile(outputFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { return err @@ -198,48 +168,101 @@ func writeInitConfig(config InitConfig) error { return config.Write(f) } -func discoverDevices(ctx context.Context, nodes []types.Node, teaProgram *tea.Program) (devices map[directpvtypes.NodeID][]types.Device, err error) { - var nodeNames []string - nodeClient := client.NodeClient() - totalNodeCount := len(nodes) - discoveryProgressMap := make(map[string]progressLog, totalNodeCount) - for i := range nodes { - nodeNames = append(nodeNames, nodes[i].Name) - updateFunc := func() error { - node, err := nodeClient.Get(ctx, nodes[i].Name, metav1.GetOptions{}) - if err != nil { - return err - } - node.Spec.Refresh = true - if _, err := nodeClient.Update(ctx, node, metav1.UpdateOptions{TypeMeta: types.NewNodeTypeMeta()}); err != nil { - return err +func discoverMain(ctx context.Context) { + var teaProgram *tea.Program + var wg sync.WaitGroup + if !quietFlag { + m := newProgressModel(false) + teaProgram = tea.NewProgram(m) + wg.Add(1) + go func() { + defer wg.Done() + if _, err := teaProgram.Run(); err != nil { + fmt.Println("error running program:", err) + os.Exit(1) } - if teaProgram != nil { - discoveryProgressMap[node.Name] = progressLog{ - log: fmt.Sprintf("Discovering node '%v'", node.Name), + }() + } + + nodeCh, errCh, err := adminClient.RefreshNodes(ctx, nodesArgs) + if err != nil { + eprintf(true, "discovery failed; %v\n", err) + os.Exit(1) + } + + var nodeNames []string + discoveryProgressMap := make(map[string]progressLog) + var refreshwg sync.WaitGroup + refreshwg.Add(1) + go func() { + defer refreshwg.Done() + for { + select { + case nodeID, ok := <-nodeCh: + if !ok { + return + } + nodeNames = append(nodeNames, string(nodeID)) + if teaProgram != nil { + discoveryProgressMap[string(nodeID)] = progressLog{ + log: fmt.Sprintf("Discovering node '%v'", nodeID), + } + teaProgram.Send(progressNotification{ + progressLogs: toProgressLogs(discoveryProgressMap), + }) + } + case err, ok := <-errCh: + if !ok { + return } - teaProgram.Send(progressNotification{ - progressLogs: toProgressLogs(discoveryProgressMap), - }) + eprintf(true, "discovery failed; %v\n", err) + os.Exit(1) + case <-ctx.Done(): + eprintf(true, ctx.Err().Error()) + os.Exit(1) } - return nil } - if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { - return nil, err + }() + refreshwg.Wait() + + resultMap, err := discoverDevices(ctx, nodeNames, drivesArgs, teaProgram) + if err != nil { + eprintf(true, "discovery failed; %v\n", err) + os.Exit(1) + } + if teaProgram != nil { + teaProgram.Send(progressNotification{ + done: true, + err: err, + }) + wg.Wait() + } + if err := showDevices(resultMap); err != nil { + if !errors.Is(err, errDiscoveryFailed) { + eprintf(true, "%v\n", err) } + os.Exit(1) } + if err := writeInitConfig(admin.ToInitConfig(resultMap)); err != nil { + eprintf(true, "unable to write init config; %v\n", err) + } else if !quietFlag { + color.HiGreen("Generated '%s' successfully.", outputFile) + } +} +func discoverDevices(ctx context.Context, nodes, drives []string, teaProgram *tea.Program) (devices map[directpvtypes.NodeID][]types.Device, err error) { ctx, cancel := context.WithTimeout(ctx, nodeListTimeout) defer cancel() - eventCh, stop, err := node.NewLister(). - NodeSelector(toLabelValues(nodeNames)). + eventCh, stop, err := adminClient.NewNodeLister(). + NodeSelector(utils.ToLabelValues(nodes)). Watch(ctx) if err != nil { return nil, err } defer stop() + discoveryProgressMap := make(map[string]progressLog) devices = map[directpvtypes.NodeID][]types.Device{} for { select { @@ -253,9 +276,9 @@ func discoverDevices(ctx context.Context, nodes []types.Node, teaProgram *tea.Pr } switch event.Type { case watch.Modified, watch.Added: - node := event.Node + node := event.Item if !node.Spec.Refresh { - devices[directpvtypes.NodeID(node.Name)] = node.GetDevicesByNames(drivesArgs) + devices[directpvtypes.NodeID(node.Name)] = node.GetDevicesByNames(drives) if teaProgram != nil { discoveryProgressMap[node.Name] = progressLog{ log: fmt.Sprintf("Discovered node '%v'", node.Name), @@ -274,109 +297,8 @@ func discoverDevices(ctx context.Context, nodes []types.Node, teaProgram *tea.Pr default: } case <-ctx.Done(): - utils.Eprintf(quietFlag, true, "unable to complete the discovery; %v\n", ctx.Err()) + err = fmt.Errorf("unable to complete the discovery; %v", ctx.Err()) return } } } - -func syncNodes(ctx context.Context) (err error) { - csiNodes, err := getCSINodes(ctx) - if err != nil { - return fmt.Errorf("unable to get CSI nodes; %w", err) - } - - nodes, err := node.NewLister().Get(ctx) - if err != nil { - return fmt.Errorf("unable to get nodes; %w", err) - } - - var nodeNames []string - for _, node := range nodes { - nodeNames = append(nodeNames, node.Name) - } - - // Add missing nodes. - for _, csiNode := range csiNodes { - if !utils.Contains(nodeNames, csiNode) { - node := types.NewNode(directpvtypes.NodeID(csiNode), []types.Device{}) - node.Spec.Refresh = true - if _, err = client.NodeClient().Create(ctx, node, metav1.CreateOptions{}); err != nil { - return fmt.Errorf("unable to create node %v; %w", csiNode, err) - } - } - } - - // Remove non-existing nodes. - for _, nodeName := range nodeNames { - if !utils.Contains(csiNodes, nodeName) { - if err = client.NodeClient().Delete(ctx, nodeName, metav1.DeleteOptions{}); err != nil { - return fmt.Errorf("unable to remove non-existing node %v; %w", nodeName, err) - } - } - } - - return nil -} - -func discoverMain(ctx context.Context) { - if err := syncNodes(ctx); err != nil { - utils.Eprintf(quietFlag, true, "sync failed; %v\n", err) - os.Exit(1) - } - - nodes, err := node.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - Get(ctx) - if err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) - os.Exit(1) - } - if len(nodesArgs) != 0 && len(nodes) == 0 { - suffix := "" - if len(nodesArgs) > 1 { - suffix = "s" - } - utils.Eprintf(quietFlag, true, "node%v %v not found\n", suffix, nodesArgs) - os.Exit(1) - } - - var teaProgram *tea.Program - var wg sync.WaitGroup - if !quietFlag { - m := newProgressModel(false) - teaProgram = tea.NewProgram(m) - wg.Add(1) - go func() { - defer wg.Done() - if _, err := teaProgram.Run(); err != nil { - fmt.Println("error running program:", err) - os.Exit(1) - } - }() - } - resultMap, err := discoverDevices(ctx, nodes, teaProgram) - if err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) - os.Exit(1) - } - if teaProgram != nil { - teaProgram.Send(progressNotification{ - done: true, - err: err, - }) - wg.Wait() - } - if err := showDevices(resultMap); err != nil { - if !errors.Is(err, errDiscoveryFailed) { - utils.Eprintf(quietFlag, true, "%v\n", err) - } - os.Exit(1) - } - - if err := writeInitConfig(toInitConfig(resultMap)); err != nil { - utils.Eprintf(quietFlag, true, "unable to write init config; %v\n", err) - } else if !quietFlag { - color.HiGreen("Generated '%s' successfully.", outputFile) - } -} diff --git a/cmd/kubectl-directpv/info.go b/cmd/kubectl-directpv/info.go index d512fa84..c33fd2f9 100644 --- a/cmd/kubectl-directpv/info.go +++ b/cmd/kubectl-directpv/info.go @@ -20,18 +20,12 @@ import ( "context" "fmt" "os" - "strings" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/k8s" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var infoCmd = &cobra.Command{ @@ -45,50 +39,11 @@ var infoCmd = &cobra.Command{ } func infoMain(ctx context.Context) { - crds, err := k8s.CRDClient().List(ctx, metav1.ListOptions{}) + nodeInfoMap, err := adminClient.Info(ctx) if err != nil { - utils.Eprintf(quietFlag, true, "unable to list CRDs; %v\n", err) + eprintf(true, "%v\n", err) os.Exit(1) } - - drivesFound := false - volumesFound := false - for _, crd := range crds.Items { - if strings.Contains(crd.Name, consts.DriveResource+"."+consts.GroupName) { - drivesFound = true - } - if strings.Contains(crd.Name, consts.VolumeResource+"."+consts.GroupName) { - volumesFound = true - } - } - if !drivesFound || !volumesFound { - utils.Eprintf(quietFlag, false, "%v installation not found\n", consts.AppPrettyName) - os.Exit(1) - } - - nodeList, err := getCSINodes(ctx) - if err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) - os.Exit(1) - } - - if len(nodeList) == 0 { - utils.Eprintf(quietFlag, true, "%v not installed\n", consts.AppPrettyName) - os.Exit(1) - } - - drives, err := drive.NewLister().Get(ctx) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to get drive list; %v\n", err) - os.Exit(1) - } - - volumes, err := volume.NewLister().Get(ctx) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to get volume list; %v\n", err) - os.Exit(1) - } - writer := newTableWriter( table.Row{"NODE", "CAPACITY", "ALLOCATED", "VOLUMES", "DRIVES"}, []table.SortBy{ @@ -118,30 +73,14 @@ func infoMain(ctx context.Context) { var totalDriveSize uint64 var totalVolumeSize uint64 - for _, n := range nodeList { - driveCount := 0 - driveSize := uint64(0) - for _, d := range drives { - if string(d.GetNodeID()) == n { - driveCount++ - driveSize += uint64(d.Status.TotalCapacity) - } - } - totalDriveSize += driveSize - - volumeCount := 0 - volumeSize := uint64(0) - for _, v := range volumes { - if string(v.GetNodeID()) == n { - if v.IsPublished() { - volumeCount++ - volumeSize += uint64(v.Status.TotalCapacity) - } - } - } - totalVolumeSize += volumeSize - - if driveCount == 0 { + var totalDriveCount int + var totalVolumeCount int + for n, info := range nodeInfoMap { + totalDriveSize += info.DriveSize + totalVolumeSize += info.VolumeSize + totalDriveCount += info.DriveCount + totalVolumeCount += info.VolumeCount + if info.DriveCount == 0 { writer.AppendRow([]interface{}{ fmt.Sprintf("%s %s", color.HiYellowString(dot), n), "-", @@ -152,23 +91,23 @@ func infoMain(ctx context.Context) { } else { writer.AppendRow([]interface{}{ fmt.Sprintf("%s %s", color.GreenString(dot), n), - humanize.IBytes(driveSize), - humanize.IBytes(volumeSize), - fmt.Sprintf("%d", volumeCount), - fmt.Sprintf("%d", driveCount), + humanize.IBytes(info.DriveSize), + humanize.IBytes(info.VolumeSize), + fmt.Sprintf("%d", info.VolumeCount), + fmt.Sprintf("%d", info.DriveCount), }) } } if !quietFlag { writer.Render() - if len(drives) > 0 { + if len(nodeInfoMap) > 0 { fmt.Printf( "\n%s/%s used, %s volumes, %s drives\n", humanize.IBytes(totalVolumeSize), humanize.IBytes(totalDriveSize), - color.HiWhiteString("%d", len(volumes)), - color.HiWhiteString("%d", len(drives)), + color.HiWhiteString("%d", totalVolumeCount), + color.HiWhiteString("%d", totalDriveCount), ) } } diff --git a/cmd/kubectl-directpv/init.go b/cmd/kubectl-directpv/init.go index 66d762ba..e0441541 100644 --- a/cmd/kubectl-directpv/init.go +++ b/cmd/kubectl-directpv/init.go @@ -26,12 +26,10 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/fatih/color" - "github.com/google/uuid" "github.com/jedib0t/go-pretty/v6/table" + "github.com/minio/directpv/pkg/admin" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/initrequest" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" @@ -63,15 +61,15 @@ var initCmd = &cobra.Command{ switch len(args) { case 1: case 0: - utils.Eprintf(quietFlag, true, "Please provide the input file. Check `--help` for usage.\n") + eprintf(true, "Please provide the input file. Check `--help` for usage.\n") os.Exit(-1) default: - utils.Eprintf(quietFlag, true, "Too many input args. Check `--help` for usage.\n") + eprintf(true, "Too many input args. Check `--help` for usage.\n") os.Exit(-1) } if !dangerousFlag { - utils.Eprintf(quietFlag, true, "Initializing the drives will permanently erase existing data. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag.\n") + eprintf(true, "Initializing the drives will permanently erase existing data. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag.\n") os.Exit(1) } @@ -86,26 +84,6 @@ func init() { addDangerousFlag(initCmd, "Perform initialization of drives which will permanently erase existing data") } -func toInitRequestObjects(config *InitConfig, requestID string) (initRequests []types.InitRequest) { - for _, node := range config.Nodes { - initDevices := []types.InitDevice{} - for _, device := range node.Drives { - if strings.ToLower(device.Select) != driveSelectedValue { - continue - } - initDevices = append(initDevices, types.InitDevice{ - ID: device.ID, - Name: device.Name, - Force: device.FS != "", - }) - } - if len(initDevices) > 0 { - initRequests = append(initRequests, *types.NewInitRequest(requestID, node.Name, initDevices)) - } - } - return -} - func showResults(results []initResult) { if len(results) == 0 { return @@ -164,20 +142,13 @@ func showResults(results []initResult) { writer.Render() } -func toProgressLogs(progressMap map[string]progressLog) (logs []progressLog) { - for _, v := range progressMap { - logs = append(logs, v) - } - return -} - func initDevices(ctx context.Context, initRequests []types.InitRequest, requestID string, teaProgram *tea.Program) (results []initResult, err error) { totalReqCount := len(initRequests) totalTasks := totalReqCount * 2 var completedTasks int initProgressMap := make(map[string]progressLog, totalReqCount) for i := range initRequests { - initReq, err := client.InitRequestClient().Create(ctx, &initRequests[i], metav1.CreateOptions{TypeMeta: types.NewInitRequestTypeMeta()}) + initReq, err := adminClient.InitRequest().Create(ctx, &initRequests[i], metav1.CreateOptions{TypeMeta: types.NewInitRequestTypeMeta()}) if err != nil { return nil, err } @@ -195,8 +166,8 @@ func initDevices(ctx context.Context, initRequests []types.InitRequest, requestI ctx, cancel := context.WithTimeout(ctx, initRequestListTimeout) defer cancel() - eventCh, stop, err := initrequest.NewLister(). - RequestIDSelector(toLabelValues([]string{requestID})). + eventCh, stop, err := adminClient.NewInitRequestLister(). + RequestIDSelector(utils.ToLabelValues([]string{requestID})). Watch(ctx) if err != nil { return nil, err @@ -216,7 +187,7 @@ func initDevices(ctx context.Context, initRequests []types.InitRequest, requestI } switch event.Type { case watch.Modified, watch.Added: - initReq := event.InitRequest + initReq := event.Item if initReq.Status.Status != directpvtypes.InitStatusPending { results = append(results, initResult{ requestID: initReq.Name, @@ -250,32 +221,23 @@ func initDevices(ctx context.Context, initRequests []types.InitRequest, requestI } } -func readInitConfig(inputFile string) (*InitConfig, error) { - f, err := os.Open(inputFile) - if err != nil { - return nil, err - } - defer f.Close() - return parseInitConfig(f) -} - func initMain(ctx context.Context, inputFile string) { - initConfig, err := readInitConfig(inputFile) + initConfig, err := admin.ReadInitConfig(inputFile) if err != nil { - utils.Eprintf(quietFlag, true, "unable to read the input file; %v", err.Error()) + eprintf(true, "unable to read the input file; %v", err.Error()) os.Exit(1) } - requestID := uuid.New().String() - initRequests := toInitRequestObjects(initConfig, requestID) + + initRequests, requestID := initConfig.ToInitRequestObjects() if len(initRequests) == 0 { - utils.Eprintf(false, false, "%v\n", color.HiYellowString("No drives are available to init")) + eprintf(false, "%v\n", color.HiYellowString("No drives are available to init")) os.Exit(1) } defer func() { labelMap := map[directpvtypes.LabelKey][]directpvtypes.LabelValue{ - directpvtypes.RequestIDLabelKey: toLabelValues([]string{requestID}), + directpvtypes.RequestIDLabelKey: utils.ToLabelValues([]string{requestID}), } - client.InitRequestClient().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + adminClient.InitRequest().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ LabelSelector: directpvtypes.ToLabelSelector(labelMap), }) }() @@ -295,7 +257,7 @@ func initMain(ctx context.Context, inputFile string) { } results, err := initDevices(ctx, initRequests, requestID, teaProgram) if err != nil && quietFlag { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(1) } if teaProgram != nil { diff --git a/cmd/kubectl-directpv/init_config.go b/cmd/kubectl-directpv/init_config.go deleted file mode 100644 index 770dd8ac..00000000 --- a/cmd/kubectl-directpv/init_config.go +++ /dev/null @@ -1,61 +0,0 @@ -// This file is part of MinIO DirectPV -// Copyright (c) 2021, 2022 MinIO, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package main - -import ( - "errors" - "io" - - "gopkg.in/yaml.v3" -) - -var errUnsupportedInitConfigVersion = errors.New("unsupported init config version") - -const latestInitConfigVersion = "v1" - -// InitConfig holds the latest config version -type InitConfig = InitConfigV1 - -// NodeInfo holds the latest node info -type NodeInfo = NodeInfoV1 - -// DriveInfo holds the latest drive info -type DriveInfo = DriveInfoV1 - -// NewInitConfig initializes an init config. -func NewInitConfig() InitConfig { - return InitConfig{ - Version: latestInitConfigVersion, - } -} - -func parseInitConfig(r io.Reader) (*InitConfig, error) { - var config InitConfig - if err := yaml.NewDecoder(r).Decode(&config); err != nil { - return nil, err - } - if config.Version != latestInitConfigVersion { - return nil, errUnsupportedInitConfigVersion - } - return &config, nil -} - -func (config InitConfig) Write(w io.Writer) error { - encoder := yaml.NewEncoder(w) - defer encoder.Close() - return encoder.Encode(config) -} diff --git a/cmd/kubectl-directpv/install.go b/cmd/kubectl-directpv/install.go index 4de78fd4..3e2887ba 100644 --- a/cmd/kubectl-directpv/install.go +++ b/cmd/kubectl-directpv/install.go @@ -27,15 +27,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" - directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/admin" + "github.com/minio/directpv/pkg/admin/installer" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/installer" + "github.com/minio/directpv/pkg/k8s" legacyclient "github.com/minio/directpv/pkg/legacy/client" "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/version" ) @@ -88,7 +87,7 @@ var installCmd = &cobra.Command{ ), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if err := validateInstallCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } disableInit = dryRunPrinter != nil @@ -125,87 +124,19 @@ func init() { installCmd.PersistentFlags().BoolVar(&openshiftFlag, "openshift", openshiftFlag, "Use OpenShift specific installation") } -func validateNodeSelectorArgs() error { - nodeSelector = map[string]string{} - for _, value := range nodeSelectorArgs { - tokens := strings.Split(value, "=") - if len(tokens) != 2 { - return fmt.Errorf("invalid node selector value %v", value) - } - if tokens[0] == "" { - return fmt.Errorf("invalid key in node selector value %v", value) - } - nodeSelector[tokens[0]] = tokens[1] - } - return nil -} - -func validateTolerationsArgs() error { - for _, value := range tolerationArgs { - var k, v, e string - tokens := strings.SplitN(value, "=", 2) - switch len(tokens) { - case 1: - k = tokens[0] - tokens = strings.Split(k, ":") - switch len(tokens) { - case 1: - case 2: - k, e = tokens[0], tokens[1] - default: - if len(tokens) != 2 { - return fmt.Errorf("invalid toleration %v", value) - } - } - case 2: - k, v = tokens[0], tokens[1] - default: - if len(tokens) != 2 { - return fmt.Errorf("invalid toleration %v", value) - } - } - if k == "" { - return fmt.Errorf("invalid key in toleration %v", value) - } - if v != "" { - if tokens = strings.Split(v, ":"); len(tokens) != 2 { - return fmt.Errorf("invalid value in toleration %v", value) - } - v, e = tokens[0], tokens[1] - } - effect := corev1.TaintEffect(e) - switch effect { - case corev1.TaintEffectNoSchedule, corev1.TaintEffectPreferNoSchedule, corev1.TaintEffectNoExecute: - default: - return fmt.Errorf("invalid toleration effect in toleration %v", value) - } - operator := corev1.TolerationOpExists - if v != "" { - operator = corev1.TolerationOpEqual - } - tolerations = append(tolerations, corev1.Toleration{ - Key: k, - Operator: operator, - Value: v, - Effect: effect, - }) - } - - return nil -} - -func validateInstallCmd() error { - if err := validateNodeSelectorArgs(); err != nil { +func validateInstallCmd() (err error) { + nodeSelector, err = k8s.ParseNodeSelector(nodeSelectorArgs) + if err != nil { return fmt.Errorf("%v; format of '--node-selector' flag value must be [=]", err) } - if err := validateTolerationsArgs(); err != nil { + tolerations, err = k8s.ParseTolerations(tolerationArgs) + if err != nil { return fmt.Errorf("%v; format of '--tolerations' flag value must be [=value]:", err) } - if err := validateOutputFormat(false); err != nil { + if err = validateOutputFormat(false); err != nil { return err } if dryRunPrinter != nil && k8sVersion != "" { - var err error if kubeVersion, err = version.ParseSemantic(k8sVersion); err != nil { return fmt.Errorf("invalid kubernetes version %v; %v", k8sVersion, err) } @@ -213,111 +144,61 @@ func validateInstallCmd() error { return nil } -func getLegacyFlag(ctx context.Context) bool { - if dryRunPrinter != nil { - return legacyFlag - } - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := volume.NewLister(). - LabelSelector( - map[directpvtypes.LabelKey]directpvtypes.LabelValue{ - directpvtypes.MigratedLabelKey: "true", - }, - ). - IgnoreNotFound(true). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "unable to get volumes; %v", result.Err) - break - } - - return true - } - - legacyclient.Init() - - for result := range legacyclient.ListVolumes(ctx) { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "unable to get legacy volumes; %v", result.Err) - break - } - - return true - } - - return false -} - func installMain(ctx context.Context) { - legacyFlag = getLegacyFlag(ctx) - + pluginVersion := "dev" + if Version != "" { + pluginVersion = Version + } + dryRun := dryRunPrinter != nil var file *utils.SafeFile var err error - if dryRunPrinter == nil && !declarativeFlag { + if !dryRun && !declarativeFlag { auditFile := fmt.Sprintf("install.log.%v", time.Now().UTC().Format(time.RFC3339Nano)) file, err = openAuditFile(auditFile) if err != nil { - utils.Eprintf(quietFlag, true, "unable to open audit file %v; %v\n", auditFile, err) - utils.Eprintf(false, false, "%v\n", color.HiYellowString("Skipping audit logging")) + eprintf(true, "unable to open audit file %v; %v\n", auditFile, err) + eprintf(false, "%v\n", color.HiYellowString("Skipping audit logging")) } - defer func() { if file != nil { if err := file.Close(); err != nil { - utils.Eprintf(quietFlag, true, "unable to close audit file; %v\n", err) + eprintf(true, "unable to close audit file; %v\n", err) } } }() } - args := installer.NewArgs(image) - if err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) - os.Exit(1) + args := admin.InstallArgs{ + Image: image, + Registry: registry, + Org: org, + ImagePullSecrets: imagePullSecrets, + NodeSelector: nodeSelector, + Tolerations: tolerations, + SeccompProfile: seccompProfile, + AppArmorProfile: apparmorProfile, + EnableLegacy: legacyFlag, + PluginVersion: pluginVersion, + Quiet: quietFlag, + KubeVersion: kubeVersion, + DryRun: dryRunPrinter != nil, + OutputFormat: outputFormat, + Declarative: declarativeFlag, + Openshift: openshiftFlag, + AuditWriter: file, } - pluginVersion := "dev" - if Version != "" { - pluginVersion = Version - } - - args.Registry = registry - args.Org = org - args.ImagePullSecrets = imagePullSecrets - args.NodeSelector = nodeSelector - args.Tolerations = tolerations - args.SeccompProfile = seccompProfile - args.AppArmorProfile = apparmorProfile - args.Quiet = quietFlag - args.KubeVersion = kubeVersion - args.Legacy = legacyFlag - args.PluginVersion = pluginVersion - if file != nil { - args.ObjectWriter = file - } - if dryRunPrinter != nil { - args.DryRun = true - if outputFormat == "yaml" { - args.ObjectMarshaler = func(obj runtime.Object) ([]byte, error) { - return utils.ToYAML(obj) - } - } else { - args.ObjectMarshaler = func(obj runtime.Object) ([]byte, error) { - return utils.ToJSON(obj) - } - } - } - args.Declarative = declarativeFlag - args.Openshift = openshiftFlag - var failed bool - var installedComponents []installer.Component var wg sync.WaitGroup - if dryRunPrinter == nil && !declarativeFlag && !quietFlag { + var installedComponents []installer.Component + legacyClient, err := legacyclient.NewClient(adminClient.K8s()) + if err != nil { + fmt.Println("error creating legacy client:", err) + return + } + installerTasks := installer.GetDefaultTasks(adminClient.Client, legacyClient) + enableProgress := !dryRun && !declarativeFlag && !quietFlag + if enableProgress { m := newProgressModel(true) teaProgram := tea.NewProgram(m) wg.Add(1) @@ -325,15 +206,15 @@ func installMain(ctx context.Context) { defer wg.Done() if _, err := teaProgram.Run(); err != nil { fmt.Println("error running program:", err) - os.Exit(1) + return } }() - wg.Add(1) var totalSteps, step, completedTasks int var currentPercent float64 - totalTasks := len(installer.Tasks) + totalTasks := len(installerTasks) weightagePerTask := 1.0 / totalTasks progressCh := make(chan installer.Message) + wg.Add(1) go func() { defer wg.Done() defer close(args.ProgressCh) @@ -401,8 +282,9 @@ func installMain(ctx context.Context) { }() args.ProgressCh = progressCh } - if err := installer.Install(ctx, args); err != nil && args.ProgressCh == nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + + if err := adminClient.Install(ctx, args, installerTasks); err != nil && args.ProgressCh == nil { + eprintf(true, "%v\n", err) os.Exit(1) } if args.ProgressCh != nil { diff --git a/cmd/kubectl-directpv/label.go b/cmd/kubectl-directpv/label.go index 5000fffd..5462336b 100644 --- a/cmd/kubectl-directpv/label.go +++ b/cmd/kubectl-directpv/label.go @@ -21,24 +21,12 @@ import ( "fmt" "strings" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/apis/directpv.min.io/types" "github.com/spf13/cobra" ) -type label struct { - key types.LabelKey - value types.LabelValue - remove bool -} - -func (l label) String() string { - if l.value == "" { - return string(l.key) - } - return string(l.key) + ":" + string(l.value) -} - -var labels []label +var labels []admin.Label var labelCmd = &cobra.Command{ Use: "label", @@ -71,28 +59,28 @@ func validateLabelCmd() error { return validateDriveNameArgs() } -func validateLabelCmdArgs(args []string) (labels []label, err error) { +func validateLabelCmdArgs(args []string) (labels []admin.Label, err error) { if len(args) == 0 { return nil, errors.New("at least one label must be provided") } for _, arg := range args { - var label label + var label admin.Label tokens := strings.Split(arg, "=") switch len(tokens) { case 1: if !strings.HasSuffix(arg, "-") { return nil, fmt.Errorf("argument %v must end with '-' to remove label", arg) } - label.remove = true - if label.key, err = types.NewLabelKey(arg[:len(arg)-1]); err != nil { + label.Remove = true + if label.Key, err = types.NewLabelKey(arg[:len(arg)-1]); err != nil { return nil, err } case 2: - if label.key, err = types.NewLabelKey(tokens[0]); err != nil { + if label.Key, err = types.NewLabelKey(tokens[0]); err != nil { return nil, err } - if label.value, err = types.NewLabelValue(tokens[1]); err != nil { + if label.Value, err = types.NewLabelValue(tokens[1]); err != nil { return nil, err } default: diff --git a/cmd/kubectl-directpv/label_drives.go b/cmd/kubectl-directpv/label_drives.go index cf52a4d8..ac41e923 100644 --- a/cmd/kubectl-directpv/label_drives.go +++ b/cmd/kubectl-directpv/label_drives.go @@ -19,17 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" ) var labelDrivesCmd = &cobra.Command{ @@ -53,7 +48,7 @@ var labelDrivesCmd = &cobra.Command{ Run: func(c *cobra.Command, args []string) { driveIDArgs = idArgs if err := validateLabelDrivesCmd(args); err != nil { - utils.Eprintf(quietFlag, true, "%s; Check `--help` for usage\n", err.Error()) + eprintf(true, "%s; Check `--help` for usage\n", err.Error()) os.Exit(1) } labelDrivesMain(c.Context()) @@ -97,47 +92,21 @@ func init() { } func labelDrivesMain(ctx context.Context) { - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - StatusSelector(driveStatusSelectors). - DriveIDSelector(driveIDSelectors). - LabelSelector(labelSelectors). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - drive := &result.Drive - var verb string - for i := range labels { - updateFunc := func() (err error) { - if labels[i].remove { - if ok := drive.RemoveLabel(labels[i].key); !ok { - return - } - verb = "removed from" - } else { - if ok := drive.SetLabel(labels[i].key, labels[i].value); !ok { - return - } - verb = "set on" - } - if !dryRunFlag { - drive, err = client.DriveClient().Update(ctx, drive, metav1.UpdateOptions{}) - } - if err != nil { - utils.Eprintf(quietFlag, true, "%v/%v: %v\n", drive.GetNodeID(), drive.GetDriveName(), err) - } else if !quietFlag { - fmt.Printf("Label '%s' successfully %s %v/%v\n", labels[i].String(), verb, drive.GetNodeID(), drive.GetDriveName()) - } - return - } - retry.RetryOnConflict(retry.DefaultRetry, updateFunc) - } + _, err := adminClient.LabelDrives( + ctx, + admin.LabelDriveArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + DriveStatus: driveStatusSelectors, + DriveIDs: driveIDSelectors, + LabelSelectors: labelSelectors, + DryRun: dryRunFlag, + }, + labels, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) + os.Exit(1) } } diff --git a/cmd/kubectl-directpv/label_volumes.go b/cmd/kubectl-directpv/label_volumes.go index c45c9cfb..a3cd8927 100644 --- a/cmd/kubectl-directpv/label_volumes.go +++ b/cmd/kubectl-directpv/label_volumes.go @@ -19,17 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" ) var labelVolumesCmd = &cobra.Command{ @@ -53,7 +48,7 @@ var labelVolumesCmd = &cobra.Command{ Run: func(c *cobra.Command, args []string) { volumeNameArgs = idArgs if err := validateLabelVolumesCmd(args); err != nil { - utils.Eprintf(quietFlag, true, "%s; Check `--help` for usage\n", err.Error()) + eprintf(true, "%s; Check `--help` for usage\n", err.Error()) os.Exit(1) } labelVolumesMain(c.Context()) @@ -106,51 +101,23 @@ func init() { } func labelVolumesMain(ctx context.Context) { - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := volume.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - DriveIDSelector(toLabelValues(driveIDArgs)). - PodNameSelector(toLabelValues(podNameArgs)). - PodNSSelector(toLabelValues(podNSArgs)). - StatusSelector(volumeStatusSelectors). - VolumeNameSelector(volumeNameArgs). - LabelSelector(labelSelectors). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - var verb string - volume := &result.Volume - for i := range labels { - updateFunc := func() (err error) { - if labels[i].remove { - if ok := volume.RemoveLabel(labels[i].key); !ok { - return - } - verb = "removed from" - } else { - if ok := volume.SetLabel(labels[i].key, labels[i].value); !ok { - return - } - verb = "set on" - } - if !dryRunFlag { - volume, err = client.VolumeClient().Update(ctx, volume, metav1.UpdateOptions{}) - } - if err != nil { - utils.Eprintf(quietFlag, true, "%v: %v\n", volume.Name, err) - } else if !quietFlag { - fmt.Printf("Label '%s' successfully %s %v\n", labels[i].String(), verb, volume.Name) - } - return - } - retry.RetryOnConflict(retry.DefaultRetry, updateFunc) - } + _, err := adminClient.LabelVolumes( + ctx, + admin.LabelVolumeArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + DriveIDs: driveIDArgs, + PodNames: podNameArgs, + PodNamespaces: podNSArgs, + VolumeStatus: volumeStatusSelectors, + VolumeNames: volumeNameArgs, + LabelSelectors: labelSelectors, + }, + labels, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) + os.Exit(1) } } diff --git a/cmd/kubectl-directpv/list_drives.go b/cmd/kubectl-directpv/list_drives.go index 5f6dcd9a..7556d53b 100644 --- a/cmd/kubectl-directpv/list_drives.go +++ b/cmd/kubectl-directpv/list_drives.go @@ -25,7 +25,6 @@ import ( "github.com/jedib0t/go-pretty/v6/table" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" @@ -68,7 +67,7 @@ var listDrivesCmd = &cobra.Command{ Run: func(c *cobra.Command, args []string) { driveIDArgs = args if err := validateListDrivesArgs(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -117,15 +116,15 @@ func validateListDrivesArgs() error { } func listDrivesMain(ctx context.Context) { - drives, err := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). + drives, err := adminClient.NewDriveLister(). + NodeSelector(utils.ToLabelValues(nodesArgs)). + DriveNameSelector(utils.ToLabelValues(drivesArgs)). StatusSelector(driveStatusSelectors). DriveIDSelector(driveIDSelectors). LabelSelector(labelSelectors). Get(ctx) if err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(1) } @@ -225,9 +224,9 @@ func listDrivesMain(ctx context.Context) { } if allFlag { - utils.Eprintf(quietFlag, false, "No resources found\n") + eprintf(false, "No resources found\n") } else { - utils.Eprintf(quietFlag, false, "No matching resources found\n") + eprintf(false, "No matching resources found\n") } os.Exit(1) diff --git a/cmd/kubectl-directpv/list_volumes.go b/cmd/kubectl-directpv/list_volumes.go index b86bd694..870e4fb8 100644 --- a/cmd/kubectl-directpv/list_volumes.go +++ b/cmd/kubectl-directpv/list_volumes.go @@ -27,7 +27,6 @@ import ( "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -76,7 +75,7 @@ var listVolumesCmd = &cobra.Command{ Run: func(c *cobra.Command, args []string) { volumeNameArgs = args if err := validateListVolumesArgs(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -155,18 +154,18 @@ func getPVCName(ctx context.Context, volume types.Volume) string { } func listVolumesMain(ctx context.Context) { - volumes, err := volume.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - DriveIDSelector(toLabelValues(driveIDArgs)). - PodNameSelector(toLabelValues(podNameArgs)). - PodNSSelector(toLabelValues(podNSArgs)). + volumes, err := adminClient.NewVolumeLister(). + NodeSelector(utils.ToLabelValues(nodesArgs)). + DriveNameSelector(utils.ToLabelValues(drivesArgs)). + DriveIDSelector(utils.ToLabelValues(driveIDArgs)). + PodNameSelector(utils.ToLabelValues(podNameArgs)). + PodNSSelector(utils.ToLabelValues(podNSArgs)). StatusSelector(volumeStatusSelectors). VolumeNameSelector(volumeNameArgs). LabelSelector(labelSelectors). Get(ctx) if err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(1) } @@ -276,9 +275,9 @@ func listVolumesMain(ctx context.Context) { } if allFlag { - utils.Eprintf(quietFlag, false, "No resources found\n") + eprintf(false, "No resources found\n") } else { - utils.Eprintf(quietFlag, false, "No matching resources found\n") + eprintf(false, "No matching resources found\n") } os.Exit(1) diff --git a/cmd/kubectl-directpv/main.go b/cmd/kubectl-directpv/main.go index 314fe92b..505fd85e 100644 --- a/cmd/kubectl-directpv/main.go +++ b/cmd/kubectl-directpv/main.go @@ -23,11 +23,12 @@ import ( "os/signal" "syscall" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/utils" + "github.com/minio/directpv/pkg/k8s" "github.com/spf13/cobra" "github.com/spf13/viper" + "k8s.io/client-go/rest" "k8s.io/klog/v2" ) @@ -35,7 +36,10 @@ import ( // e.g. $ go build -ldflags="-X main.Version=v4.0.1" var Version string -var disableInit bool +var ( + disableInit bool + adminClient *admin.Client +) var mainCmd = &cobra.Command{ Use: consts.AppName, @@ -48,7 +52,15 @@ var mainCmd = &cobra.Command{ Version: Version, PersistentPreRunE: func(_ *cobra.Command, _ []string) error { if !disableInit { - client.Init() + kubeConfig, err := k8s.GetKubeConfig() + if err != nil { + klog.Fatalf("unable to get kubernetes configuration; %v", err) + } + kubeConfig.WarningHandler = rest.NoWarnings{} + adminClient, err = admin.NewClient(kubeConfig) + if err != nil { + klog.Fatalf("unable to create admin client; %v", err) + } } return nil }, @@ -170,7 +182,7 @@ func main() { go func() { select { case signal := <-signalCh: - utils.Eprintf(quietFlag, false, "\nExiting on signal %v\n", signal) + eprintf(false, "\nExiting on signal %v\n", signal) cancelFunc() os.Exit(1) case <-ctx.Done(): @@ -178,7 +190,7 @@ func main() { }() if err := mainCmd.ExecuteContext(ctx); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/migrate.go b/cmd/kubectl-directpv/migrate.go index 4de0ee78..a9cfc610 100644 --- a/cmd/kubectl-directpv/migrate.go +++ b/cmd/kubectl-directpv/migrate.go @@ -18,14 +18,12 @@ package main import ( "context" - "fmt" "os" "strings" "time" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/installer" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" ) @@ -53,41 +51,14 @@ func init() { } func migrateMain(ctx context.Context) { - if err := installer.Migrate(ctx, &installer.Args{ - Quiet: quietFlag, - Legacy: true, - }, false); err != nil { - utils.Eprintf(quietFlag, true, "migration failed; %v", err) - os.Exit(1) - } - - if !quietFlag { - fmt.Println("Migration successful; Please restart the pods in '" + consts.AppName + "' namespace.") - } - - if retainFlag { - return - } - suffix := time.Now().Format(time.RFC3339) - - drivesBackupFile := "directcsidrives-" + suffix + ".yaml" - backupCreated, err := installer.RemoveLegacyDrives(ctx, drivesBackupFile) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to remove legacy drive CRDs; %v", err) - os.Exit(1) - } - if backupCreated && !quietFlag { - fmt.Println("Legacy drive CRDs backed up to", drivesBackupFile) - } - - volumesBackupFile := "directcsivolumes-" + suffix + ".yaml" - backupCreated, err = installer.RemoveLegacyVolumes(ctx, volumesBackupFile) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to remove legacy volume CRDs; %v", err) + if err := adminClient.Migrate(ctx, admin.MigrateArgs{ + Quiet: quietFlag, + Retain: retainFlag, + DrivesBackupFile: "directcsidrives-" + suffix + ".yaml", + VolumesBackupFile: "directcsivolumes-" + suffix + ".yaml", + }); err != nil { + eprintf(true, "migration failed; %v", err) os.Exit(1) } - if backupCreated && !quietFlag { - fmt.Println("Legacy volume CRDs backed up to", volumesBackupFile) - } } diff --git a/cmd/kubectl-directpv/move.go b/cmd/kubectl-directpv/move.go index a1f7eea7..5a8c8854 100644 --- a/cmd/kubectl-directpv/move.go +++ b/cmd/kubectl-directpv/move.go @@ -18,18 +18,13 @@ package main import ( "context" - "fmt" "os" "strings" + "github.com/minio/directpv/pkg/admin" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/types" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var moveCmd = &cobra.Command{ @@ -46,147 +41,37 @@ var moveCmd = &cobra.Command{ ), Run: func(c *cobra.Command, args []string) { if len(args) != 2 { - utils.Eprintf(quietFlag, true, "only one source and one destination drive must be provided\n") + eprintf(true, "only one source and one destination drive must be provided\n") os.Exit(-1) } src := strings.TrimSpace(args[0]) if src == "" { - utils.Eprintf(quietFlag, true, "empty source drive\n") + eprintf(true, "empty source drive\n") os.Exit(-1) } dest := strings.TrimSpace(args[1]) if dest == "" { - utils.Eprintf(quietFlag, true, "empty destination drive\n") + eprintf(true, "empty destination drive\n") os.Exit(-1) } - moveMain(c.Context(), src, dest) + moveMain(c.Context(), directpvtypes.DriveID(src), directpvtypes.DriveID(dest)) }, } -func moveMain(ctx context.Context, src, dest string) { - if src == dest { - utils.Eprintf(quietFlag, true, "source and destination drives are same\n") - os.Exit(1) - } - - srcDrive, err := client.DriveClient().Get(ctx, src, metav1.GetOptions{}) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to get source drive; %v\n", err) - os.Exit(1) - } - - if !srcDrive.IsUnschedulable() { - utils.Eprintf(quietFlag, true, "source drive is not cordoned\n") - os.Exit(1) - } - - sourceVolumeNames := srcDrive.GetVolumes() - if len(sourceVolumeNames) == 0 { - utils.Eprintf(quietFlag, false, "No volumes found in source drive %v\n", src) - return - } - - var requiredCapacity int64 - var volumes []types.Volume - for result := range volume.NewLister().VolumeNameSelector(sourceVolumeNames).List(ctx) { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - if result.Volume.IsPublished() { - utils.Eprintf(quietFlag, true, "cannot move published volume %v\n", result.Volume.Name) - os.Exit(1) - } - - requiredCapacity += result.Volume.Status.TotalCapacity - volumes = append(volumes, result.Volume) - } - - if len(volumes) == 0 { - utils.Eprintf(quietFlag, false, "No volumes found in source drive %v\n", src) - return - } - - destDrive, err := client.DriveClient().Get(ctx, dest, metav1.GetOptions{}) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to get destination drive; %v\n", err) - os.Exit(1) - } - - if destDrive.GetNodeID() != srcDrive.GetNodeID() { - utils.Eprintf( - quietFlag, - true, - "source and destination drives must be in same node; source node %v; desination node %v\n", - srcDrive.GetNodeID(), - destDrive.GetNodeID(), - ) - os.Exit(1) - } - - if !destDrive.IsUnschedulable() { - utils.Eprintf(quietFlag, true, "destination drive is not cordoned\n") - os.Exit(1) - } - - if destDrive.Status.Status != directpvtypes.DriveStatusReady { - utils.Eprintf(quietFlag, true, "destination drive is not in ready state\n") - os.Exit(1) - } - - if srcDrive.GetAccessTier() != destDrive.GetAccessTier() { - utils.Eprintf( - quietFlag, - true, - "source drive access-tier %v and destination drive access-tier %v differ\n", - srcDrive.GetAccessTier(), - destDrive.GetAccessTier(), - ) - os.Exit(1) - } - - if destDrive.Status.FreeCapacity < requiredCapacity { - utils.Eprintf( - quietFlag, - true, - "insufficient free capacity on destination drive; required=%v free=%v\n", - printableBytes(requiredCapacity), - printableBytes(destDrive.Status.FreeCapacity), - ) - os.Exit(1) - } - - for _, volume := range volumes { - if destDrive.AddVolumeFinalizer(volume.Name) { - destDrive.Status.FreeCapacity -= volume.Status.TotalCapacity - destDrive.Status.AllocatedCapacity += volume.Status.TotalCapacity - } - } - destDrive.Status.Status = directpvtypes.DriveStatusMoving - _, err = client.DriveClient().Update( - ctx, destDrive, metav1.UpdateOptions{TypeMeta: types.NewDriveTypeMeta()}, - ) - if err != nil { - utils.Eprintf(quietFlag, true, "unable to move volumes to destination drive; %v\n", err) - os.Exit(1) - } - - for _, volume := range volumes { - if !quietFlag { - fmt.Println("Moving volume", volume.Name) - } - } - - srcDrive.ResetFinalizers() - _, err = client.DriveClient().Update( - ctx, srcDrive, metav1.UpdateOptions{TypeMeta: types.NewDriveTypeMeta()}, +func moveMain(ctx context.Context, src, dest directpvtypes.DriveID) { + err := adminClient.Move( + ctx, + admin.MoveArgs{ + Source: src, + Destination: dest, + }, + logFunc, ) if err != nil { - utils.Eprintf(quietFlag, true, "unable to remove volume references in source drive; %v\n", err) + eprintf(true, "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/progress_model.go b/cmd/kubectl-directpv/progress_model.go index 4bfe2079..da491ded 100644 --- a/cmd/kubectl-directpv/progress_model.go +++ b/cmd/kubectl-directpv/progress_model.go @@ -171,3 +171,10 @@ func (m progressModel) View() (str string) { } return str + pad } + +func toProgressLogs(progressMap map[string]progressLog) (logs []progressLog) { + for _, v := range progressMap { + logs = append(logs, v) + } + return +} diff --git a/cmd/kubectl-directpv/remove.go b/cmd/kubectl-directpv/remove.go index 39c9d69e..ae50d8a5 100644 --- a/cmd/kubectl-directpv/remove.go +++ b/cmd/kubectl-directpv/remove.go @@ -23,13 +23,9 @@ import ( "os" "strings" - directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var removeCmd = &cobra.Command{ @@ -59,7 +55,7 @@ var removeCmd = &cobra.Command{ driveIDArgs = args if err := validateRemoveCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -115,59 +111,19 @@ func validateRemoveCmd() error { } func removeMain(ctx context.Context) { - var processed bool - var failed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - StatusSelector(driveStatusSelectors). - DriveIDSelector(driveIDSelectors). - IgnoreNotFound(true). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - switch result.Drive.Status.Status { - case directpvtypes.DriveStatusRemoved: - default: - volumeCount := result.Drive.GetVolumeCount() - if volumeCount > 0 { - failed = true - } else { - result.Drive.Status.Status = directpvtypes.DriveStatusRemoved - var err error - if !dryRunFlag { - _, err = client.DriveClient().Update(ctx, &result.Drive, metav1.UpdateOptions{}) - } - if err != nil { - failed = true - utils.Eprintf(quietFlag, true, "%v/%v: %v\n", result.Drive.GetNodeID(), result.Drive.GetDriveName(), err) - } else if !quietFlag { - fmt.Printf("Removing %v/%v\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()) - } - } - } - } - - if !processed { - if allFlag { - utils.Eprintf(quietFlag, false, "No resources found\n") - } else { - utils.Eprintf(quietFlag, false, "No matching resources found\n") - } - - os.Exit(1) - } - - if failed { + _, err := adminClient.Remove( + ctx, + admin.RemoveArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + DriveStatus: driveStatusSelectors, + DriveIDs: driveIDSelectors, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/resume_drives.go b/cmd/kubectl-directpv/resume_drives.go index 5d503c48..852ffe6e 100644 --- a/cmd/kubectl-directpv/resume_drives.go +++ b/cmd/kubectl-directpv/resume_drives.go @@ -19,17 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" ) var resumeDrivesCmd = &cobra.Command{ @@ -53,7 +48,7 @@ var resumeDrivesCmd = &cobra.Command{ driveIDArgs = args if err := validateResumeDrivesCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -91,60 +86,18 @@ func validateResumeDrivesCmd() error { } func resumeDrivesMain(ctx context.Context) { - var processed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - DriveIDSelector(driveIDSelectors). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - - if !result.Drive.IsSuspended() { - // only suspended drives can be resumed. - continue - } - - driveClient := client.DriveClient() - updateFunc := func() error { - drive, err := driveClient.Get(ctx, result.Drive.Name, metav1.GetOptions{}) - if err != nil { - return err - } - drive.Resume() - if !dryRunFlag { - if _, err := driveClient.Update(ctx, drive, metav1.UpdateOptions{}); err != nil { - return err - } - } - return nil - } - if err := retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { - utils.Eprintf(quietFlag, true, "unable to resume drive %v; %v\n", result.Drive.GetDriveID(), err) - os.Exit(1) - } - - if !quietFlag { - fmt.Printf("Drive %v/%v resumed\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()) - } - } - - if !processed { - if allFlag { - utils.Eprintf(quietFlag, false, "No resources found\n") - } else { - utils.Eprintf(quietFlag, false, "No matching resources found\n") - } - + _, err := adminClient.ResumeDrives( + ctx, + admin.ResumeDriveArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + DriveIDSelectors: driveIDSelectors, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/resume_volumes.go b/cmd/kubectl-directpv/resume_volumes.go index 04ff4dff..b271b697 100644 --- a/cmd/kubectl-directpv/resume_volumes.go +++ b/cmd/kubectl-directpv/resume_volumes.go @@ -19,17 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" ) var resumeVolumesCmd = &cobra.Command{ @@ -53,7 +48,7 @@ var resumeVolumesCmd = &cobra.Command{ volumeNameArgs = args if err := validateResumeVolumesCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -101,57 +96,20 @@ func validateResumeVolumesCmd() error { } func resumeVolumesMain(ctx context.Context) { - var processed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := volume.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - PodNameSelector(toLabelValues(podNameArgs)). - PodNSSelector(toLabelValues(podNSArgs)). - VolumeNameSelector(volumeNameArgs). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - - if !result.Volume.IsSuspended() { - // only suspended drives can be resumed. - continue - } - - volumeClient := client.VolumeClient() - updateFunc := func() error { - volume, err := volumeClient.Get(ctx, result.Volume.Name, metav1.GetOptions{}) - if err != nil { - return err - } - volume.Resume() - if !dryRunFlag { - if _, err := volumeClient.Update(ctx, volume, metav1.UpdateOptions{}); err != nil { - return err - } - } - return nil - } - if err := retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { - utils.Eprintf(quietFlag, true, "unable to resume volume %v; %v\n", result.Volume.Name, err) - os.Exit(1) - } - - if !quietFlag { - fmt.Printf("Volume %v/%v resumed\n", result.Volume.GetNodeID(), result.Volume.Name) - } - } - - if !processed { - utils.Eprintf(quietFlag, false, "No matching resources found\n") + _, err := adminClient.ResumeVolumes( + ctx, + admin.ResumeVolumeArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + PodNames: podNameArgs, + PodNamespaces: podNSArgs, + VolumeNames: volumeNameArgs, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/suspend_drives.go b/cmd/kubectl-directpv/suspend_drives.go index e83648d9..ce38c7e9 100644 --- a/cmd/kubectl-directpv/suspend_drives.go +++ b/cmd/kubectl-directpv/suspend_drives.go @@ -19,17 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" ) var suspendDrivesCmd = &cobra.Command{ @@ -54,12 +49,12 @@ var suspendDrivesCmd = &cobra.Command{ driveIDArgs = args if err := validateSuspendDrivesCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } if !dangerousFlag { - utils.Eprintf(quietFlag, true, "Suspending the drives will make the corresponding volumes as read-only. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag..\n") + eprintf(true, "Suspending the drives will make the corresponding volumes as read-only. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag..\n") os.Exit(1) } @@ -98,54 +93,18 @@ func validateSuspendDrivesCmd() error { } func suspendDrivesMain(ctx context.Context) { - var processed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - DriveIDSelector(driveIDSelectors). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - - if result.Drive.IsSuspended() { - continue - } - - driveClient := client.DriveClient() - updateFunc := func() error { - drive, err := driveClient.Get(ctx, result.Drive.Name, metav1.GetOptions{}) - if err != nil { - return err - } - drive.Suspend() - if !dryRunFlag { - if _, err := driveClient.Update(ctx, drive, metav1.UpdateOptions{}); err != nil { - return err - } - } - return nil - } - if err := retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { - utils.Eprintf(quietFlag, true, "unable to suspend drive %v; %v\n", result.Drive.GetDriveID(), err) - os.Exit(1) - } - - if !quietFlag { - fmt.Printf("Drive %v/%v suspended\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()) - } - } - - if !processed { - utils.Eprintf(quietFlag, false, "No matching resources found\n") + _, err := adminClient.SuspendDrives( + ctx, + admin.SuspendDriveArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + DriveIDSelectors: driveIDSelectors, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/suspend_volumes.go b/cmd/kubectl-directpv/suspend_volumes.go index 85761410..781fc2ce 100644 --- a/cmd/kubectl-directpv/suspend_volumes.go +++ b/cmd/kubectl-directpv/suspend_volumes.go @@ -19,17 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/utils" - "github.com/minio/directpv/pkg/volume" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" ) var suspendVolumesCmd = &cobra.Command{ @@ -54,12 +49,12 @@ var suspendVolumesCmd = &cobra.Command{ volumeNameArgs = args if err := validateSuspendVolumesCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } if !dangerousFlag { - utils.Eprintf(quietFlag, true, "Suspending the volumes will make them as read-only. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag.\n") + eprintf(true, "Suspending the volumes will make them as read-only. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag.\n") os.Exit(1) } @@ -108,56 +103,20 @@ func validateSuspendVolumesCmd() error { } func suspendVolumesMain(ctx context.Context) { - var processed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := volume.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - PodNameSelector(toLabelValues(podNameArgs)). - PodNSSelector(toLabelValues(podNSArgs)). - VolumeNameSelector(volumeNameArgs). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - - if result.Volume.IsSuspended() { - continue - } - - volumeClient := client.VolumeClient() - updateFunc := func() error { - volume, err := volumeClient.Get(ctx, result.Volume.Name, metav1.GetOptions{}) - if err != nil { - return err - } - volume.Suspend() - if !dryRunFlag { - if _, err := volumeClient.Update(ctx, volume, metav1.UpdateOptions{}); err != nil { - return err - } - } - return nil - } - if err := retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { - utils.Eprintf(quietFlag, true, "unable to suspend volume %v; %v\n", result.Volume.Name, err) - os.Exit(1) - } - - if !quietFlag { - fmt.Printf("Volume %v/%v suspended\n", result.Volume.GetNodeID(), result.Volume.Name) - } - } - - if !processed { - utils.Eprintf(quietFlag, false, "No matching resources found\n") + _, err := adminClient.SuspendVolumes( + ctx, + admin.SuspendVolumeArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + PodNames: podNameArgs, + PodNamespaces: podNSArgs, + VolumeNames: volumeNameArgs, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/uncordon.go b/cmd/kubectl-directpv/uncordon.go index 5d1e337e..1374db14 100644 --- a/cmd/kubectl-directpv/uncordon.go +++ b/cmd/kubectl-directpv/uncordon.go @@ -19,16 +19,12 @@ package main import ( "context" "errors" - "fmt" "os" "strings" - "github.com/minio/directpv/pkg/client" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var uncordonCmd = &cobra.Command{ @@ -58,7 +54,7 @@ var uncordonCmd = &cobra.Command{ driveIDArgs = args if err := validateUncordonCmd(); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + eprintf(true, "%v\n", err) os.Exit(-1) } @@ -114,52 +110,19 @@ func validateUncordonCmd() error { } func uncordonMain(ctx context.Context) { - var processed bool - - ctx, cancelFunc := context.WithCancel(ctx) - defer cancelFunc() - - resultCh := drive.NewLister(). - NodeSelector(toLabelValues(nodesArgs)). - DriveNameSelector(toLabelValues(drivesArgs)). - StatusSelector(driveStatusSelectors). - DriveIDSelector(driveIDSelectors). - List(ctx) - for result := range resultCh { - if result.Err != nil { - utils.Eprintf(quietFlag, true, "%v\n", result.Err) - os.Exit(1) - } - - processed = true - - if !result.Drive.IsUnschedulable() { - continue - } - - result.Drive.Schedulable() - var err error - if !dryRunFlag { - _, err = client.DriveClient().Update(ctx, &result.Drive, metav1.UpdateOptions{}) - } - - if err != nil { - utils.Eprintf(quietFlag, true, "unable to uncordon drive %v; %v\n", result.Drive.GetDriveID(), err) - os.Exit(1) - } - - if !quietFlag { - fmt.Printf("Drive %v/%v uncordoned\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()) - } - } - - if !processed { - if allFlag { - utils.Eprintf(quietFlag, false, "No resources found\n") - } else { - utils.Eprintf(quietFlag, false, "No matching resources found\n") - } - + _, err := adminClient.Uncordon( + ctx, + admin.UncordonArgs{ + Nodes: nodesArgs, + Drives: drivesArgs, + Status: driveStatusSelectors, + DriveIDs: driveIDSelectors, + DryRun: dryRunFlag, + }, + logFunc, + ) + if err != nil { + eprintf(!errors.Is(err, admin.ErrNoMatchingResourcesFound), "%v\n", err) os.Exit(1) } } diff --git a/cmd/kubectl-directpv/uninstall.go b/cmd/kubectl-directpv/uninstall.go index b77496a7..eee22205 100644 --- a/cmd/kubectl-directpv/uninstall.go +++ b/cmd/kubectl-directpv/uninstall.go @@ -21,9 +21,8 @@ import ( "fmt" "os" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/installer" - "github.com/minio/directpv/pkg/utils" "github.com/spf13/cobra" ) @@ -45,8 +44,15 @@ func init() { } func uninstallMain(ctx context.Context) { - if err := installer.Uninstall(ctx, quietFlag, dangerousFlag); err != nil { - utils.Eprintf(quietFlag, true, "%v\n", err) + err := adminClient.Uninstall( + ctx, + admin.UninstallArgs{ + Quiet: quietFlag, + Dangerous: dangerousFlag, + }, + ) + if err != nil { + eprintf(true, "%v\n", err) os.Exit(1) } if !quietFlag { diff --git a/cmd/kubectl-directpv/utils.go b/cmd/kubectl-directpv/utils.go index 4817706a..355c678e 100644 --- a/cmd/kubectl-directpv/utils.go +++ b/cmd/kubectl-directpv/utils.go @@ -17,24 +17,17 @@ package main import ( - "context" "errors" "fmt" "os" "path" - "time" "github.com/dustin/go-humanize" "github.com/jedib0t/go-pretty/v6/table" - directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/admin" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/utils" "github.com/mitchellh/go-homedir" - storagev1 "k8s.io/api/storage/v1" - storagev1beta1 "k8s.io/api/storage/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" ) @@ -58,25 +51,6 @@ func printJSON(obj interface{}) { fmt.Print(string(data)) } -func getDefaultAuditDir() (string, error) { - homeDir, err := homedir.Dir() - if err != nil { - return "", err - } - return path.Join(homeDir, "."+consts.AppName, "audit"), nil -} - -func openAuditFile(auditFile string) (*utils.SafeFile, error) { - defaultAuditDir, err := getDefaultAuditDir() - if err != nil { - return nil, fmt.Errorf("unable to get default audit directory; %w", err) - } - if err := os.MkdirAll(defaultAuditDir, 0o700); err != nil { - return nil, fmt.Errorf("unable to create default audit directory; %w", err) - } - return utils.NewSafeFile(path.Join(defaultAuditDir, auditFile)) -} - func printableString(s string) string { if s == "" { return "-" @@ -107,13 +81,6 @@ func newTableWriter(header table.Row, sortBy []table.SortBy, noHeader bool) tabl return writer } -func toLabelValues(slice []string) (values []directpvtypes.LabelValue) { - for _, s := range slice { - values = append(values, directpvtypes.ToLabelValue(s)) - } - return -} - func validateOutputFormat(isWideSupported bool) error { switch outputFormat { case "": @@ -135,54 +102,29 @@ func validateOutputFormat(isWideSupported bool) error { return nil } -func getCSINodes(ctx context.Context) (nodes []string, err error) { - storageClient, gvk, err := k8s.GetClientForNonCoreGroupVersionKind("storage.k8s.io", "CSINode", "v1", "v1beta1", "v1alpha1") +func openAuditFile(auditFile string) (*utils.SafeFile, error) { + defaultAuditDir, err := getDefaultAuditDir() if err != nil { - return nil, err + return nil, fmt.Errorf("unable to get default audit directory; %w", err) } + if err := os.MkdirAll(defaultAuditDir, 0o700); err != nil { + return nil, fmt.Errorf("unable to create default audit directory; %w", err) + } + return utils.NewSafeFile(path.Join(defaultAuditDir, auditFile)) +} - switch gvk.Version { - case "v1apha1": - err = fmt.Errorf("unsupported CSINode storage.k8s.io/v1alpha1") - case "v1": - result := &storagev1.CSINodeList{} - if err = storageClient.Get(). - Resource("csinodes"). - VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec). - Timeout(10 * time.Second). - Do(ctx). - Into(result); err != nil { - err = fmt.Errorf("unable to get csinodes; %w", err) - break - } - for _, csiNode := range result.Items { - for _, driver := range csiNode.Spec.Drivers { - if driver.Name == consts.Identity { - nodes = append(nodes, csiNode.Name) - break - } - } - } - case "v1beta1": - result := &storagev1beta1.CSINodeList{} - if err = storageClient.Get(). - Resource(gvk.Kind). - VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec). - Timeout(10 * time.Second). - Do(ctx). - Into(result); err != nil { - err = fmt.Errorf("unable to get csinodes; %w", err) - break - } - for _, csiNode := range result.Items { - for _, driver := range csiNode.Spec.Drivers { - if driver.Name == consts.Identity { - nodes = append(nodes, csiNode.Name) - break - } - } - } +func getDefaultAuditDir() (string, error) { + homeDir, err := homedir.Dir() + if err != nil { + return "", err } + return path.Join(homeDir, "."+consts.AppName, "audit"), nil +} + +func eprintf(isErr bool, format string, args ...any) { + utils.Eprintf(quietFlag, isErr, format, args...) +} - return nodes, err +func logFunc(log admin.LogMessage) { + eprintf(log.Type == admin.ErrorLogType, log.FormattedMessage) } diff --git a/codegen.sh b/codegen.sh index 4281d88d..8ef70c54 100755 --- a/codegen.sh +++ b/codegen.sh @@ -107,8 +107,8 @@ client-gen \ --input-base "${REPOSITORY}/pkg/apis" echo "Running controller-gen ..." -controller-gen crd:crdVersions=v1 paths=./... output:dir=pkg/installer -rm -f pkg/installer/direct.csi.min.io_directcsidrives.yaml pkg/installer/direct.csi.min.io_directcsivolumes.yaml +controller-gen crd:crdVersions=v1 paths=./... output:dir=pkg/admin/installer +rm -f pkg/admin/installer/direct.csi.min.io_directcsidrives.yaml pkg/admin/installer/direct.csi.min.io_directcsivolumes.yaml echo "Running conversion-gen ..." conversion-gen \ diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 00000000..e5b18a59 --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,373 @@ +# DirectPV Admin Client APIs + +The DirectPV Admin Golang Client SDK provides APIs to manage DirectPV drives and volumes. + +This quickstart guide will show you how to use DirectPV Admin client SDK to list the initialized drives. + +```go +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/minio/directpv/pkg/admin" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + MaxThreadCount = 200 +) + +func getKubeConfig() (*rest.Config, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + kubeConfig := filepath.Join(home, ".kube", "config") + config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) + if err != nil { + if config, err = rest.InClusterConfig(); err != nil { + return nil, err + } + } + config.QPS = float32(MaxThreadCount / 2) + config.Burst = MaxThreadCount + return config, nil +} + +func main() { + kubeConfig, err := getKubeConfig() + if err != nil { + fmt.Printf("%s: Could not connect to kubernetes. %s=%s\n", "Error", "KUBECONFIG", kubeConfig) + os.Exit(1) + } + adminClient, err := admin.NewClient(kubeConfig) + if err != nil { + log.Fatalf("unable to initialize client; %v", err) + } + drives, err := adminClient.NewDriveLister().Get(context.Background()) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + for _, drive := range drives { + fmt.Printf("\n DeviceName: %v", drive.GetDriveName()) + fmt.Printf("\n Node: %v", drive.GetNodeID()) + fmt.Printf("\n Make: %v", drive.Status.Make) + fmt.Println() + } +} +``` + +## Install DirectPV + +Installs DirectPV components + +### Install(ctx context.Context, args InstallArgs, installerTasks []installer.Task) error + +__Example__ + +```go +args := admin.InstallArgs{ + Image: image, + Registry: registry, + Org: org, + ImagePullSecrets: imagePullSecrets, + NodeSelector: nodeSelector, + Tolerations: tolerations, + SeccompProfile: seccompProfile, + AppArmorProfile: apparmorProfile, + EnableLegacy: legacyFlag, + PluginVersion: pluginVersion, + Quiet: quietFlag, + KubeVersion: kubeVersion, + DryRun: dryRunPrinter != nil, + OutputFormat: outputFormat, + Declarative: declarativeFlag, + Openshift: openshiftFlag, + AuditWriter: file, +} +var installedComponents []installer.Component +legacyClient, err := legacyclient.NewClient(adminClient.K8s()) +if err != nil { + log.Fatalf("error creating legacy client:", err) +} +installerTasks := installer.GetDefaultTasks(adminClient.Client, legacyClient) +if err := adminClient.Install(ctx, args, installerTasks); err != nil { + log.Fatalf("unable to complete installation; %v", err) +} +``` + +## Refresh Nodes + +Refreshes the nodes to get the latest devices list, this is used for discovering the nodes and devices present in the cluster. + +### RefreshNodes(ctx context.Context, selectedNodes []string) (<-chan directpvtypes.NodeID, <-chan error, error) + +__Example__ + +```go +nodeCh, errCh, err := adminClient.RefreshNodes(ctx, []string{"praveen-thinkpad-x1-carbon-6th"}) +if err != nil { + log.Fatalln(err) +} + +for { + select { + case nodeID, ok := <-nodeCh: + if !ok { + return + } + log.Println("Refreshing node ", nodeID) + case err, ok := <-errCh: + if !ok { + return + } + log.Fatalln(err) + case <-ctx.Done(): + return + } +} +``` + +## DirectPV Info + +Returns the overall information about DirectPV installation + +### Info(ctx context.Context) (map[string]NodeLevelInfo, error) + +__Example__ + +```go +nodeInfoMap, err := adminClient.Info(context.Background()) +if err != nil { + log.Fatalf("unable to get info; %v", err) +} +``` + +## Label DirectPV drives + +Label the directpv drives + +### LabelDrives(ctx context.Context, args LabelDriveArgs, labels []Label, log logFn) (results []LabelDriveResult, err error) + +__Example__ + +```go +labels := []admin.Label{ + { + Key: directpvtypes.LabelKey("example-key"), + Value: directpvtypes.LabelValue("example-value"), + }, +} +if _, err := adminClient.LabelDrives(context.Background(), admin.LabelDriveArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, labels, log); err != nil { + log.Fatalf("unable to label the drive; %v", err) +} +fmt.Println("successfully labeled the drive(s)") +``` + +## Label DirectPV volumes + +Label the directpv volumes + +### LabelVolumes(ctx context.Context, args LabelVolumeArgs, labels []Label, log logFn) (results []LabelVolumeResult, err error) + +__Example__ + +```go +labels := []admin.Label{ + { + Key: directpvtypes.LabelKey("example-key"), + Value: directpvtypes.LabelValue("example-value"), + }, +} +if _, err := adminClient.LabelVolumes(context.Background(), admin.LabelVolumeArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, labels, log); err != nil { + log.Fatalf("unable to label the volume; %v", err) +} +``` + +## Cordon the drive + +Cordon the drive to make it unschedulable + +### Cordon(ctx context.Context, args CordonArgs, log logFn) (results []CordonResult, err error) + +__Example__ + +```go +if _, err := adminClient.Cordon(context.Background(), admin.CordonArgs{ + Drives: []string{"dm-1"}, +}, log); err != nil { + log.Fatalf("unable to cordon the drive; %v", err) +} +``` + +## Uncordon the drive + +Mark drives as schedulable + +### Uncordon(ctx context.Context, args UncordonArgs, log logFn) (results []UncordonResult, err error) + +__Example__ + +```go +if _, err := adminClient.Uncordon(context.Background(), admin.UncordonArgs{ + Drives: []string{"dm-1"}, +}, log); err != nil { + log.Fatalf("unable to uncordon the drive; %v", err) +} +``` + +## Migrate the legacy drives and volumes + +Migrates the legacy direct-csi drives and volumes + +### Migrate(ctx context.Context, args MigrateArgs) error + +__Example__ + +```go +suffix := time.Now().Format(time.RFC3339) +if err := adminClient.Migrate(ctx, admin.MigrateArgs{ + DrivesBackupFile: "directcsidrives-" + suffix + ".yaml", + VolumesBackupFile: "directcsivolumes-" + suffix + ".yaml", +}); err != nil { + log.Fatalf("migration failed; %v", err) +} +``` + +## Move the volume references from one drive to another + +Move volumes excluding data from source drive to destination drive on a same node + +### Move(ctx context.Context, args MoveArgs, log logFn) error + +__Example__ + +```go +if err := adminClient.Move(context.Background(), admin.MoveArgs{ + Source: directpvtypes.DriveID("2786de98-2a84-40d4-8cee-8f73686928f8"), + Destination: directpvtypes.DriveID("b35f1f8e-6bf3-4747-9976-192b23c1a019"), +}, log); err != nil { + log.Fatalf("unable to move the drive; %v", err) +} +fmt.Println("successfully moved the drive") +``` + +## Cleanup volumes + +Cleanup stale volumes + +### Clean(ctx context.Context, args CleanArgs, log logFn) (removedVolumes []string, err error) + +__Example__ + +```go +if _, err := adminClient.Clean(context.Background(), admin.CleanArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, log); err != nil { + log.Fatalf("unable to clean the volume; %v", err) +} +fmt.Println("successfully cleaned the volume(s)") +``` + +## Suspend drives + +Suspend the drives (CAUTION: This will make the corresponding volumes as read-only) + +### SuspendDrives(ctx context.Context, args SuspendDriveArgs, log logFn) (results []SuspendDriveResult, err error) + +__Example__ + +```go +if _, err := adminClient.SuspendDrives(context.Background(), admin.SuspendDriveArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, log); err != nil { + log.Fatalf("unable to suspend the drive; %v", err) +} +fmt.Println("successfully suspended the drive(s)") +``` + +## Suspend volumes + +Suspend the volumes (CAUTION: This will make the corresponding volumes as read-only) + +### SuspendVolumes(ctx context.Context, args SuspendVolumeArgs, log logFn) (results []SuspendVolumeResult, err error) + +__Example__ + +```go +if _, err := adminClient.SuspendVolumes(context.Background(), admin.SuspendVolumeArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, log); err != nil { + log.Fatalf("unable to suspend the volume; %v", err) +} +fmt.Println("successfully suspended the volume(s)") +``` + +## Resume drives + +Resume suspended drives + +### ResumeDrives(ctx context.Context, args ResumeDriveArgs, log logFn) (results []ResumeDriveResult, err error) + +__Example__ + +```go +if _, err := adminClient.ResumeDrives(context.Background(), admin.SuspendDriveArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, log); err != nil { + log.Fatalf("unable to resume the drive; %v", err) +} +fmt.Println("successfully resumed the drive(s)") +``` + +## Resume volumes + +Resume suspended volumes + +### ResumeVolumes(ctx context.Context, args ResumeVolumeArgs, log logFn) (results []ResumeVolumeResult, err error) + +__Example__ + +```go +if _, err := adminClient.ResumeVolumes(context.Background(), admin.ResumeVolumeArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, log); err != nil { + log.Fatalf("unable to resume the volume; %v", err) +} +fmt.Println("successfully resumed the volume(s)") +``` + +## Remove drives + +Remove unused drives from DirectPV + +### Remove(ctx context.Context, args RemoveArgs, log logFn) (results []RemoveResult, err error) + +__Example__ + +```go +if _, err := adminClient.Remove(context.Background(), admin.RemoveArgs{ + Nodes: []string{"praveen-thinkpad-x1-carbon-6th"}, + Drives: []string{"dm-0"}, +}, log); err != nil { + log.Fatalf("unable to remove the drive; %v", err) +} +fmt.Println("successfully removed the drive(s)") +``` diff --git a/docs/command-reference.md b/docs/command-reference.md index ba3183fd..0e558b04 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -62,7 +62,7 @@ FLAGS: --apparmor-profile string Set path to Apparmor profile --seccomp-profile string Set path to Seccomp profile -o, --output string Generate installation manifest. One of: yaml|json - --kube-version string Select the kubernetes version for manifest generation (default "1.27.0") + --kube-version string Select the kubernetes version for manifest generation (default "1.29.0") --legacy Enable legacy mode (Used with '-o') --openshift Use OpenShift specific installation -h, --help help for install diff --git a/pkg/admin/clean.go b/pkg/admin/clean.go new file mode 100644 index 00000000..08a9fb84 --- /dev/null +++ b/pkg/admin/clean.go @@ -0,0 +1,121 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/types" + "github.com/minio/directpv/pkg/utils" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CleanArgs represents the arguments to clean the volumes +type CleanArgs struct { + Nodes []string + Drives []string + DriveIDs []string + PodNames []string + PodNamespaces []string + VolumeStatus []directpvtypes.VolumeStatus + VolumeNames []string + DryRun bool +} + +// Clean removes the stale/abandoned volumes +func (client *Client) Clean(ctx context.Context, args CleanArgs, log LogFunc) (removedVolumes []string, err error) { + if log == nil { + log = nullLogger + } + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewVolumeLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + DriveIDSelector(utils.ToLabelValues(args.DriveIDs)). + PodNameSelector(utils.ToLabelValues(args.PodNames)). + PodNSSelector(utils.ToLabelValues(args.PodNamespaces)). + StatusSelector(args.VolumeStatus). + VolumeNameSelector(args.VolumeNames). + List(ctx) + + matchFunc := func(volume *types.Volume) bool { + pv, err := client.Kube().CoreV1().PersistentVolumes().Get(ctx, volume.Name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return true + } + log( + LogMessage{ + Type: ErrorLogType, + Err: err, + Message: "unable to get PV for volume", + Values: map[string]any{"volume": volume.Name}, + FormattedMessage: fmt.Sprintf("unable to get PV for volume %v; %v\n", volume.Name, err), + }, + ) + return false + } + switch pv.Status.Phase { + case corev1.VolumeReleased, corev1.VolumeFailed: + return true + default: + return false + } + } + + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + if !matchFunc(&result.Volume) { + continue + } + result.Volume.RemovePVProtection() + if args.DryRun { + continue + } + if _, err = client.Volume().Update(ctx, &result.Volume, metav1.UpdateOptions{ + TypeMeta: types.NewVolumeTypeMeta(), + }); err != nil { + return + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "removing volume", + Values: map[string]any{"volume": result.Volume.Name}, + FormattedMessage: fmt.Sprintf("Removing volume %v\n", result.Volume.Name), + }, + ) + + if err = client.Volume().Delete(ctx, result.Volume.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { + return + } + removedVolumes = append(removedVolumes, result.Volume.Name) + } + + return +} diff --git a/pkg/admin/client.go b/pkg/admin/client.go new file mode 100644 index 00000000..3a3057f9 --- /dev/null +++ b/pkg/admin/client.go @@ -0,0 +1,38 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "github.com/minio/directpv/pkg/client" + "k8s.io/client-go/rest" +) + +// Client represents the admin clientset +type Client struct { + *client.Client +} + +// NewClient returns a new admin client +func NewClient(c *rest.Config) (*Client, error) { + directpvClientSet, err := client.NewClient(c) + if err != nil { + return nil, err + } + return &Client{ + Client: directpvClientSet, + }, nil +} diff --git a/pkg/admin/cordon.go b/pkg/admin/cordon.go new file mode 100644 index 00000000..1db313fe --- /dev/null +++ b/pkg/admin/cordon.go @@ -0,0 +1,114 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CordonResult represents the +type CordonResult struct { + NodeID directpvtypes.NodeID + DriveName directpvtypes.DriveName +} + +// CordonArgs represents the args to Cordon the drive +type CordonArgs struct { + Nodes []string + Drives []string + Status []directpvtypes.DriveStatus + DriveIDs []directpvtypes.DriveID + DryRun bool +} + +// Cordon makes a drive unschedulable +func (client *Client) Cordon(ctx context.Context, args CordonArgs, log LogFunc) (results []CordonResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewDriveLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + StatusSelector(args.Status). + DriveIDSelector(args.DriveIDs). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + + processed = true + + if result.Drive.IsUnschedulable() { + continue + } + + volumes := result.Drive.GetVolumes() + if len(volumes) != 0 { + for vresult := range client.NewVolumeLister().VolumeNameSelector(volumes).IgnoreNotFound(true).List(ctx) { + if vresult.Err != nil { + err = vresult.Err + return + } + + if vresult.Volume.Status.Status == directpvtypes.VolumeStatusPending { + err = fmt.Errorf("unable to cordon drive %v; pending volumes found", result.Drive.GetDriveID()) + return + } + } + } + + result.Drive.Unschedulable() + if !args.DryRun { + if _, err = client.Drive().Update(ctx, &result.Drive, metav1.UpdateOptions{}); err != nil { + err = fmt.Errorf("unable to cordon drive %v; %v", result.Drive.GetDriveID(), err) + return + } + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "drive cordoned", + Values: map[string]any{"nodeId": result.Drive.GetNodeID(), "driveName": result.Drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("Drive %v/%v cordoned\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()), + }, + ) + + results = append(results, CordonResult{ + NodeID: result.Drive.GetNodeID(), + DriveName: result.Drive.GetDriveName(), + }) + } + + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/info.go b/pkg/admin/info.go new file mode 100644 index 00000000..b746d209 --- /dev/null +++ b/pkg/admin/info.go @@ -0,0 +1,98 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + "strings" + + "github.com/minio/directpv/pkg/consts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NodeLevelInfo represents the node level information +type NodeLevelInfo struct { + DriveSize uint64 + VolumeSize uint64 + DriveCount int + VolumeCount int +} + +// Info returns the overall info of the directpv installation +func (client *Client) Info(ctx context.Context) (map[string]NodeLevelInfo, error) { + crds, err := client.CRD().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("unable to list CRDs; %v", err) + } + drivesFound := false + volumesFound := false + for _, crd := range crds.Items { + if strings.Contains(crd.Name, consts.DriveResource+"."+consts.GroupName) { + drivesFound = true + } + if strings.Contains(crd.Name, consts.VolumeResource+"."+consts.GroupName) { + volumesFound = true + } + } + if !drivesFound || !volumesFound { + return nil, fmt.Errorf("%v installation not found", consts.AppPrettyName) + } + nodeList, err := client.K8s().GetCSINodes(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get CSI nodes; %v", err) + } + if len(nodeList) == 0 { + return nil, fmt.Errorf("%v not installed", consts.AppPrettyName) + } + drives, err := client.NewDriveLister().Get(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get drive list; %v", err) + } + volumes, err := client.NewVolumeLister().Get(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get volume list; %v", err) + } + nodeInfo := make(map[string]NodeLevelInfo, len(nodeList)) + for _, n := range nodeList { + driveCount := 0 + driveSize := uint64(0) + for _, d := range drives { + if string(d.GetNodeID()) == n { + driveCount++ + driveSize += uint64(d.Status.TotalCapacity) + } + } + volumeCount := 0 + volumeSize := uint64(0) + for _, v := range volumes { + if string(v.GetNodeID()) == n { + if v.IsPublished() { + volumeCount++ + volumeSize += uint64(v.Status.TotalCapacity) + } + } + } + nodeInfo[n] = NodeLevelInfo{ + DriveSize: driveSize, + VolumeSize: volumeSize, + DriveCount: driveCount, + VolumeCount: volumeCount, + } + } + return nodeInfo, nil +} diff --git a/pkg/admin/init_config.go b/pkg/admin/init_config.go new file mode 100644 index 00000000..a4699a58 --- /dev/null +++ b/pkg/admin/init_config.go @@ -0,0 +1,132 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "errors" + "io" + "os" + "strings" + + "github.com/google/uuid" + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/types" + "gopkg.in/yaml.v3" +) + +const ( + // DriveSelectedValue denotes the option in InitConfig + DriveSelectedValue = "yes" +) + +var errUnsupportedInitConfigVersion = errors.New("unsupported init config version") + +const latestInitConfigVersion = "v1" + +// InitConfig holds the latest config version +type InitConfig = InitConfigV1 + +// NodeInfo holds the latest node info +type NodeInfo = NodeInfoV1 + +// DriveInfo holds the latest drive info +type DriveInfo = DriveInfoV1 + +// NewInitConfig initializes an init config. +func NewInitConfig() InitConfig { + return InitConfig{ + Version: latestInitConfigVersion, + } +} + +// ReadInitConfig reads the init config from a file +func ReadInitConfig(inputFile string) (*InitConfig, error) { + f, err := os.Open(inputFile) + if err != nil { + return nil, err + } + defer f.Close() + return parseInitConfig(f) +} + +func parseInitConfig(r io.Reader) (*InitConfig, error) { + var config InitConfig + if err := yaml.NewDecoder(r).Decode(&config); err != nil { + return nil, err + } + if config.Version != latestInitConfigVersion { + return nil, errUnsupportedInitConfigVersion + } + return &config, nil +} + +// Write encodes the YAML to the stream provided +func (config InitConfig) Write(w io.Writer) error { + encoder := yaml.NewEncoder(w) + defer encoder.Close() + return encoder.Encode(config) +} + +// ToInitConfig converts the map to InitConfig +func ToInitConfig(resultMap map[directpvtypes.NodeID][]types.Device) InitConfig { + nodeInfo := []NodeInfo{} + initConfig := NewInitConfig() + for node, devices := range resultMap { + driveInfo := []DriveInfo{} + for _, device := range devices { + if device.DeniedReason != "" { + continue + } + driveInfo = append(driveInfo, DriveInfo{ + ID: device.ID, + Name: device.Name, + Size: device.Size, + Make: device.Make, + FS: device.FSType, + Select: DriveSelectedValue, + }) + } + nodeInfo = append(nodeInfo, NodeInfo{ + Name: node, + Drives: driveInfo, + }) + } + initConfig.Nodes = nodeInfo + return initConfig +} + +// ToInitRequestObjects converts initConfig to init request objects. +func (config *InitConfig) ToInitRequestObjects() (initRequests []types.InitRequest, requestID string) { + requestID = uuid.New().String() + for _, node := range config.Nodes { + initDevices := []types.InitDevice{} + for _, device := range node.Drives { + if strings.ToLower(device.Select) != DriveSelectedValue { + continue + } + initDevices = append(initDevices, types.InitDevice{ + ID: device.ID, + Name: device.Name, + Force: device.FS != "", + }) + } + if len(initDevices) > 0 { + initRequests = append(initRequests, *types.NewInitRequest(requestID, node.Name, initDevices)) + } + } + return +} diff --git a/cmd/kubectl-directpv/init_config_v1.go b/pkg/admin/init_config_v1.go similarity index 64% rename from cmd/kubectl-directpv/init_config_v1.go rename to pkg/admin/init_config_v1.go index 0943ac6f..5a0b9d16 100644 --- a/cmd/kubectl-directpv/init_config_v1.go +++ b/pkg/admin/init_config_v1.go @@ -1,5 +1,5 @@ // This file is part of MinIO DirectPV -// Copyright (c) 2021, 2022 MinIO, Inc. +// Copyright (c) 2024 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -14,32 +14,28 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package main +package admin import directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" -const ( - driveSelectedValue = "yes" -) - // InitConfigV1 defines the config to initialize the devices type InitConfigV1 struct { - Version string `yaml:"version"` - Nodes []NodeInfoV1 `yaml:"nodes,omitempty"` + Version string `yaml:"version" json:"version"` + Nodes []NodeInfoV1 `yaml:"nodes,omitempty" json:"nodes,omitempty"` } // NodeInfoV1 holds the node information type NodeInfoV1 struct { - Name directpvtypes.NodeID `yaml:"name"` - Drives []DriveInfoV1 `yaml:"drives,omitempty"` + Name directpvtypes.NodeID `yaml:"name" json:"name"` + Drives []DriveInfoV1 `yaml:"drives,omitempty" json:"drives,omitempty"` } // DriveInfoV1 represents the drives that are to be initialized type DriveInfoV1 struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - Size uint64 `yaml:"size"` - Make string `yaml:"make"` - FS string `yaml:"fs,omitempty"` - Select string `yaml:"select,omitempty"` + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + Size uint64 `yaml:"size" json:"size"` + Make string `yaml:"make" json:"make"` + FS string `yaml:"fs,omitempty" json:"fs,omitempty"` + Select string `yaml:"select,omitempty" json:"select,omitempty"` } diff --git a/pkg/admin/install.go b/pkg/admin/install.go new file mode 100644 index 00000000..c17843a2 --- /dev/null +++ b/pkg/admin/install.go @@ -0,0 +1,190 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/minio/directpv/pkg/admin/installer" + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + legacyclient "github.com/minio/directpv/pkg/legacy/client" + "github.com/minio/directpv/pkg/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + versionpkg "k8s.io/apimachinery/pkg/util/version" + "k8s.io/klog/v2" +) + +// ErrInstallationIncomplete denotes that the installation couldn't complete +var ErrInstallationIncomplete = errors.New("unable to complete the installation") + +// InstallArgs represents the arguments required for installation +type InstallArgs struct { + // Image of the DirectPV + Image string + // Registry denotes the private registry + Registry string + // Org denotes the organization name + Org string + // ImagePullSecrets for the images from priv registries + ImagePullSecrets []string + // NodeSelector denotes the nodeSelector to be set for the node-server + NodeSelector map[string]string + // Tolerations denotes the tolerations to be set for the node-server + Tolerations []corev1.Toleration + // SeccompProfile denotes the seccomp profile name to be set on the node-server + SeccompProfile string + // AppArmorProfile denotes the apparmor profile name to be set on the node-server + AppArmorProfile string + // EnableLegacy to run in legacy mode + EnableLegacy bool + // PluginVersion denotes the plugin version; this will be set in node-server's annotations + PluginVersion string + // Quiet enables quiet mode + Quiet bool + // KubeVersion is required for declarative and dryrun manifests + KubeVersion *versionpkg.Version + // DryRun when set, runs in dryrun mode and generates the manifests + DryRun bool + // OutputFormat denotes the output format (yaml|json) for the manifests; to be used for DryRun + OutputFormat string + // Declarative when set, generates yaml manifests + Declarative bool + // Openshift when set, runs openshift specific installation + Openshift bool + // ProgressCh represents the progress channel + ProgressCh chan<- installer.Message + // AuditWriter denotes the writer passed to record the audit log + AuditWriter io.Writer +} + +// Validate - validates the args +func (args *InstallArgs) Validate() error { + if args.DryRun || args.Declarative { + switch args.OutputFormat { + case "yaml", "json": + case "": + args.OutputFormat = "yaml" + } + } + return nil +} + +// Install - installs directpv with the provided arguments +func (client *Client) Install(ctx context.Context, args InstallArgs, installerTasks []installer.Task) error { + var err error + if err := args.Validate(); err != nil { + return err + } + installerArgs := installer.NewArgs(args.Image) + var version string + if args.PluginVersion != "" { + version = args.PluginVersion + } + installerArgs.Registry = args.Registry + installerArgs.Org = args.Org + installerArgs.ImagePullSecrets = args.ImagePullSecrets + installerArgs.NodeSelector = args.NodeSelector + installerArgs.Tolerations = args.Tolerations + installerArgs.SeccompProfile = args.SeccompProfile + installerArgs.AppArmorProfile = args.AppArmorProfile + installerArgs.Quiet = args.Quiet + installerArgs.KubeVersion = args.KubeVersion + installerArgs.Legacy = client.isLegacyEnabled(ctx, args) + installerArgs.PluginVersion = version + if args.AuditWriter != nil { + installerArgs.ObjectWriter = args.AuditWriter + } + if args.DryRun { + installerArgs.DryRun = true + if args.OutputFormat == "yaml" { + installerArgs.ObjectMarshaler = func(obj runtime.Object) ([]byte, error) { + return utils.ToYAML(obj) + } + } else { + installerArgs.ObjectMarshaler = func(obj runtime.Object) ([]byte, error) { + return utils.ToJSON(obj) + } + } + if installerArgs.KubeVersion == nil { + // default higher version + if installerArgs.KubeVersion, err = versionpkg.ParseSemantic("1.29.0"); err != nil { + klog.Fatalf("this should not happen; %v", err) + } + } + } else { + major, minor, err := client.K8s().GetKubeVersion() + if err != nil { + return err + } + installerArgs.KubeVersion, err = versionpkg.ParseSemantic(fmt.Sprintf("%v.%v.0", major, minor)) + if err != nil { + klog.Fatalf("this should not happen; %v", err) + } + } + installerArgs.Declarative = args.Declarative + installerArgs.Openshift = args.Openshift + installerArgs.ProgressCh = args.ProgressCh + + return installer.Install(ctx, installerArgs, installerTasks) +} + +func (client Client) isLegacyEnabled(ctx context.Context, args InstallArgs) bool { + if args.DryRun { + return args.EnableLegacy + } + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewVolumeLister(). + LabelSelector( + map[directpvtypes.LabelKey]directpvtypes.LabelValue{ + directpvtypes.MigratedLabelKey: "true", + }, + ). + IgnoreNotFound(true). + List(ctx) + for result := range resultCh { + if result.Err != nil { + utils.Eprintf(args.Quiet, true, "unable to get volumes; %v", result.Err) + break + } + + return true + } + + legacyClient, err := legacyclient.NewClient(client.K8s()) + if err != nil { + utils.Eprintf(args.Quiet, true, "unable to create legacy client; %v", err) + return false + } + + for result := range legacyClient.ListVolumes(ctx) { + if result.Err != nil { + utils.Eprintf(args.Quiet, true, "unable to get legacy volumes; %v", result.Err) + break + } + + return true + } + + return false +} diff --git a/pkg/installer/args.go b/pkg/admin/installer/args.go similarity index 98% rename from pkg/installer/args.go rename to pkg/admin/installer/args.go index 31bde459..10f4867d 100644 --- a/pkg/installer/args.go +++ b/pkg/admin/installer/args.go @@ -117,6 +117,10 @@ func (args *Args) validate() error { return errors.New("object converter must be provided") } + if args.KubeVersion == nil { + return errors.New("kubeversion is not set") + } + return nil } diff --git a/pkg/installer/consts.go b/pkg/admin/installer/consts.go similarity index 100% rename from pkg/installer/consts.go rename to pkg/admin/installer/consts.go diff --git a/pkg/installer/crd.go b/pkg/admin/installer/crd.go similarity index 77% rename from pkg/installer/crd.go rename to pkg/admin/installer/crd.go index b68636d5..5a69013f 100644 --- a/pkg/installer/crd.go +++ b/pkg/admin/installer/crd.go @@ -24,11 +24,6 @@ import ( directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" - "github.com/minio/directpv/pkg/initrequest" - "github.com/minio/directpv/pkg/k8s" - "github.com/minio/directpv/pkg/node" - "github.com/minio/directpv/pkg/volume" "k8s.io/apiextensions-apiserver/pkg/apihelpers" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -49,7 +44,9 @@ var nodesYAML []byte //go:embed directpv.min.io_directpvinitrequests.yaml var initrequestsYAML []byte -type crdTask struct{} +type crdTask struct { + client *client.Client +} func (crdTask) Name() string { return "CRD" @@ -69,12 +66,12 @@ func (crdTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (crdTask) Execute(ctx context.Context, args *Args) error { - return createCRDs(ctx, args) +func (t crdTask) Execute(ctx context.Context, args *Args) error { + return t.createCRDs(ctx, args) } -func (c crdTask) Delete(ctx context.Context, args *Args) error { - return deleteCRDs(ctx, args.ForceUninstall) +func (t crdTask) Delete(ctx context.Context, args *Args) error { + return t.deleteCRDs(ctx, args.ForceUninstall) } func setNoneConversionStrategy(crd *apiextensions.CustomResourceDefinition) { @@ -148,7 +145,7 @@ func updateCRD( return existingCRD, false, nil } -func createCRDs(ctx context.Context, args *Args) (err error) { +func (t crdTask) createCRDs(ctx context.Context, args *Args) (err error) { register := func(data []byte, step int) error { object := map[string]interface{}{} if err := yaml.Unmarshal(data, &object); err != nil { @@ -171,7 +168,7 @@ func createCRDs(ctx context.Context, args *Args) (err error) { return args.writeObject(&crd) } - existingCRD, err := k8s.CRDClient().Get(ctx, crd.Name, metav1.GetOptions{}) + existingCRD, err := t.client.CRD().Get(ctx, crd.Name, metav1.GetOptions{}) if err != nil { if !apierrors.IsNotFound(err) { return err @@ -182,7 +179,7 @@ func createCRDs(ctx context.Context, args *Args) (err error) { } if !args.Declarative { - _, err := k8s.CRDClient().Create(ctx, &crd, metav1.CreateOptions{}) + _, err := t.client.CRD().Create(ctx, &crd, metav1.CreateOptions{}) if err != nil { return err } @@ -204,7 +201,7 @@ func createCRDs(ctx context.Context, args *Args) (err error) { } if !args.Declarative && !isLatest { - updatedCRD, err = k8s.CRDClient().Update(ctx, updatedCRD, metav1.UpdateOptions{}) + updatedCRD, err = t.client.CRD().Update(ctx, updatedCRD, metav1.UpdateOptions{}) if err != nil { return err } @@ -233,11 +230,11 @@ func createCRDs(ctx context.Context, args *Args) (err error) { return register(initrequestsYAML, 4) } -func removeVolumes(ctx context.Context) error { +func (t crdTask) removeVolumes(ctx context.Context) error { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() - for result := range volume.NewLister().List(ctx) { + for result := range t.client.NewVolumeLister().List(ctx) { if result.Err != nil { if apierrors.IsNotFound(result.Err) { break @@ -248,12 +245,12 @@ func removeVolumes(ctx context.Context) error { result.Volume.RemovePVProtection() result.Volume.RemovePurgeProtection() - _, err := client.VolumeClient().Update(ctx, &result.Volume, metav1.UpdateOptions{}) + _, err := t.client.Volume().Update(ctx, &result.Volume, metav1.UpdateOptions{}) if err != nil { return err } - err = client.VolumeClient().Delete(ctx, result.Volume.Name, metav1.DeleteOptions{}) + err = t.client.Volume().Delete(ctx, result.Volume.Name, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -262,11 +259,11 @@ func removeVolumes(ctx context.Context) error { return nil } -func removeDrives(ctx context.Context) error { +func (t crdTask) removeDrives(ctx context.Context) error { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() - for result := range drive.NewLister().List(ctx) { + for result := range t.client.NewDriveLister().List(ctx) { if result.Err != nil { if apierrors.IsNotFound(result.Err) { break @@ -274,12 +271,12 @@ func removeDrives(ctx context.Context) error { return result.Err } result.Drive.Finalizers = []string{} - _, err := client.DriveClient().Update(ctx, &result.Drive, metav1.UpdateOptions{}) + _, err := t.client.Drive().Update(ctx, &result.Drive, metav1.UpdateOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } - err = client.DriveClient().Delete(ctx, result.Drive.Name, metav1.DeleteOptions{}) + err = t.client.Drive().Delete(ctx, result.Drive.Name, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -288,11 +285,11 @@ func removeDrives(ctx context.Context) error { return nil } -func removeNodes(ctx context.Context) error { +func (t crdTask) removeNodes(ctx context.Context) error { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() - for result := range node.NewLister().List(ctx) { + for result := range t.client.NewNodeLister().List(ctx) { if result.Err != nil { if apierrors.IsNotFound(result.Err) { break @@ -300,11 +297,11 @@ func removeNodes(ctx context.Context) error { return result.Err } result.Node.Finalizers = []string{} - _, err := client.NodeClient().Update(ctx, &result.Node, metav1.UpdateOptions{}) + _, err := t.client.Node().Update(ctx, &result.Node, metav1.UpdateOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } - err = client.NodeClient().Delete(ctx, result.Node.Name, metav1.DeleteOptions{}) + err = t.client.Node().Delete(ctx, result.Node.Name, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -313,11 +310,11 @@ func removeNodes(ctx context.Context) error { return nil } -func removeInitRequests(ctx context.Context) error { +func (t crdTask) removeInitRequests(ctx context.Context) error { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() - for result := range initrequest.NewLister().List(ctx) { + for result := range t.client.NewInitRequestLister().List(ctx) { if result.Err != nil { if apierrors.IsNotFound(result.Err) { break @@ -325,11 +322,11 @@ func removeInitRequests(ctx context.Context) error { return result.Err } result.InitRequest.Finalizers = []string{} - _, err := client.InitRequestClient().Update(ctx, &result.InitRequest, metav1.UpdateOptions{}) + _, err := t.client.InitRequest().Update(ctx, &result.InitRequest, metav1.UpdateOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } - err = client.InitRequestClient().Delete(ctx, result.InitRequest.Name, metav1.DeleteOptions{}) + err = t.client.InitRequest().Delete(ctx, result.InitRequest.Name, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -338,47 +335,47 @@ func removeInitRequests(ctx context.Context) error { return nil } -func deleteCRDs(ctx context.Context, force bool) error { +func (t crdTask) deleteCRDs(ctx context.Context, force bool) error { if !force { return nil } - if err := removeVolumes(ctx); err != nil { + if err := t.removeVolumes(ctx); err != nil { return err } - if err := removeDrives(ctx); err != nil { + if err := t.removeDrives(ctx); err != nil { return err } - if err := removeNodes(ctx); err != nil { + if err := t.removeNodes(ctx); err != nil { return err } - if err := removeInitRequests(ctx); err != nil { + if err := t.removeInitRequests(ctx); err != nil { return err } driveCRDName := consts.DriveResource + "." + consts.GroupName - err := k8s.CRDClient().Delete(ctx, driveCRDName, metav1.DeleteOptions{}) + err := t.client.CRD().Delete(ctx, driveCRDName, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } volumeCRDName := consts.VolumeResource + "." + consts.GroupName - err = k8s.CRDClient().Delete(ctx, volumeCRDName, metav1.DeleteOptions{}) + err = t.client.CRD().Delete(ctx, volumeCRDName, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } nodeCRDName := consts.NodeResource + "." + consts.GroupName - err = k8s.CRDClient().Delete(ctx, nodeCRDName, metav1.DeleteOptions{}) + err = t.client.CRD().Delete(ctx, nodeCRDName, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } initRequestCRDName := consts.InitRequestResource + "." + consts.GroupName - err = k8s.CRDClient().Delete(ctx, initRequestCRDName, metav1.DeleteOptions{}) + err = t.client.CRD().Delete(ctx, initRequestCRDName, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return err } diff --git a/pkg/installer/csidriver.go b/pkg/admin/installer/csidriver.go similarity index 74% rename from pkg/installer/csidriver.go rename to pkg/admin/installer/csidriver.go index ba888cdd..7cce21ac 100644 --- a/pkg/installer/csidriver.go +++ b/pkg/admin/installer/csidriver.go @@ -22,8 +22,8 @@ import ( "fmt" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" legacyclient "github.com/minio/directpv/pkg/legacy/client" storagev1 "k8s.io/api/storage/v1" storagev1beta1 "k8s.io/api/storage/v1beta1" @@ -31,7 +31,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type csiDriverTask struct{} +type csiDriverTask struct { + client *client.Client +} func (csiDriverTask) Name() string { return "CSIDriver" @@ -55,17 +57,17 @@ func (csiDriverTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (csiDriverTask) Execute(ctx context.Context, args *Args) error { - return createCSIDriver(ctx, args) +func (t csiDriverTask) Execute(ctx context.Context, args *Args) error { + return t.createCSIDriver(ctx, args) } -func (csiDriverTask) Delete(ctx context.Context, _ *Args) error { - return deleteCSIDriver(ctx) +func (t csiDriverTask) Delete(ctx context.Context, _ *Args) error { + return t.deleteCSIDriver(ctx) } var errCSIDriverVersionUnsupported = errors.New("unsupported CSIDriver version found") -func doCreateCSIDriver(ctx context.Context, args *Args, version string, legacy bool, step int) (err error) { +func (t csiDriverTask) doCreateCSIDriver(ctx context.Context, args *Args, version string, legacy bool, step int) (err error) { name := consts.Identity if legacy { name = legacyclient.Identity @@ -109,7 +111,7 @@ func doCreateCSIDriver(ctx context.Context, args *Args, version string, legacy b } if !args.DryRun && !args.Declarative { - _, err := k8s.KubeClient().StorageV1().CSIDrivers().Create(ctx, csiDriver, metav1.CreateOptions{}) + _, err := t.client.Kube().StorageV1().CSIDrivers().Create(ctx, csiDriver, metav1.CreateOptions{}) if err != nil && !apierrors.IsAlreadyExists(err) { return err } @@ -142,7 +144,7 @@ func doCreateCSIDriver(ctx context.Context, args *Args, version string, legacy b } if !args.DryRun && !args.Declarative { - _, err := k8s.KubeClient().StorageV1beta1().CSIDrivers().Create(ctx, csiDriver, metav1.CreateOptions{}) + _, err := t.client.Kube().StorageV1beta1().CSIDrivers().Create(ctx, csiDriver, metav1.CreateOptions{}) if err != nil && !apierrors.IsAlreadyExists(err) { return err } @@ -155,26 +157,26 @@ func doCreateCSIDriver(ctx context.Context, args *Args, version string, legacy b } } -func createCSIDriver(ctx context.Context, args *Args) (err error) { +func (t csiDriverTask) createCSIDriver(ctx context.Context, args *Args) (err error) { version := "v1" if args.DryRun { if args.KubeVersion.Major() >= 1 && args.KubeVersion.Minor() < 19 { version = "v1beta1" } } else { - gvk, err := k8s.GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") + gvk, err := t.client.K8s().GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") if err != nil { return err } version = gvk.Version } - if err := doCreateCSIDriver(ctx, args, version, false, 1); err != nil { + if err := t.doCreateCSIDriver(ctx, args, version, false, 1); err != nil { return err } if args.Legacy { - if err := doCreateCSIDriver(ctx, args, version, true, 2); err != nil { + if err := t.doCreateCSIDriver(ctx, args, version, true, 2); err != nil { return err } } @@ -182,14 +184,14 @@ func createCSIDriver(ctx context.Context, args *Args) (err error) { return nil } -func doDeleteCSIDriver(ctx context.Context, version, name string) (err error) { +func (t csiDriverTask) doDeleteCSIDriver(ctx context.Context, version, name string) (err error) { switch version { case "v1": - err = k8s.KubeClient().StorageV1().CSIDrivers().Delete( + err = t.client.Kube().StorageV1().CSIDrivers().Delete( ctx, name, metav1.DeleteOptions{}, ) case "v1beta1": - err = k8s.KubeClient().StorageV1beta1().CSIDrivers().Delete( + err = t.client.Kube().StorageV1beta1().CSIDrivers().Delete( ctx, name, metav1.DeleteOptions{}, ) default: @@ -203,15 +205,15 @@ func doDeleteCSIDriver(ctx context.Context, version, name string) (err error) { return nil } -func deleteCSIDriver(ctx context.Context) error { - gvk, err := k8s.GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") +func (t csiDriverTask) deleteCSIDriver(ctx context.Context) error { + gvk, err := t.client.K8s().GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") if err != nil { return err } - if err = doDeleteCSIDriver(ctx, gvk.Version, consts.Identity); err != nil { + if err = t.doDeleteCSIDriver(ctx, gvk.Version, consts.Identity); err != nil { return err } - return doDeleteCSIDriver(ctx, gvk.Version, legacyclient.Identity) + return t.doDeleteCSIDriver(ctx, gvk.Version, legacyclient.Identity) } diff --git a/pkg/installer/daemonset.go b/pkg/admin/installer/daemonset.go similarity index 92% rename from pkg/installer/daemonset.go rename to pkg/admin/installer/daemonset.go index f3fd048a..4ca0fbae 100644 --- a/pkg/installer/daemonset.go +++ b/pkg/admin/installer/daemonset.go @@ -21,8 +21,8 @@ import ( "fmt" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" legacyclient "github.com/minio/directpv/pkg/legacy/client" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -48,7 +48,9 @@ const ( totalDaemonsetSteps = 2 ) -type daemonsetTask struct{} +type daemonsetTask struct { + client *client.Client +} func (daemonsetTask) Name() string { return "Daemonset" @@ -72,12 +74,12 @@ func (daemonsetTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (daemonsetTask) Execute(ctx context.Context, args *Args) error { - return createDaemonset(ctx, args) +func (t daemonsetTask) Execute(ctx context.Context, args *Args) error { + return t.createDaemonset(ctx, args) } -func (daemonsetTask) Delete(ctx context.Context, _ *Args) error { - return deleteDaemonset(ctx) +func (t daemonsetTask) Delete(ctx context.Context, _ *Args) error { + return t.deleteDaemonset(ctx) } func newSecurityContext(seccompProfile string) *corev1.SecurityContext { @@ -249,7 +251,7 @@ func newDaemonset(podSpec corev1.PodSpec, name, selectorValue string, args *Args } } -func doCreateDaemonset(ctx context.Context, args *Args) (err error) { +func (t daemonsetTask) doCreateDaemonset(ctx context.Context, args *Args) (err error) { securityContext := newSecurityContext(args.SeccompProfile) pluginSocketDir := newPluginsSocketDir(kubeletDirPath, consts.Identity) volumes, volumeMounts := getVolumesAndMounts(pluginSocketDir) @@ -286,7 +288,7 @@ func doCreateDaemonset(ctx context.Context, args *Args) (err error) { var selectorValue string if !args.DryRun { - daemonset, err := k8s.KubeClient().AppsV1().DaemonSets(namespace).Get( + daemonset, err := t.client.Kube().AppsV1().DaemonSets(namespace).Get( ctx, consts.NodeServerName, metav1.GetOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { @@ -309,7 +311,7 @@ func doCreateDaemonset(ctx context.Context, args *Args) (err error) { daemonset := newDaemonset(podSpec, consts.NodeServerName, selectorValue, args) if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().AppsV1().DaemonSets(namespace).Create( + _, err = t.client.Kube().AppsV1().DaemonSets(namespace).Create( ctx, daemonset, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -320,7 +322,7 @@ func doCreateDaemonset(ctx context.Context, args *Args) (err error) { return args.writeObject(daemonset) } -func doCreateLegacyDaemonset(ctx context.Context, args *Args) (err error) { +func (t daemonsetTask) doCreateLegacyDaemonset(ctx context.Context, args *Args) (err error) { securityContext := newSecurityContext(args.SeccompProfile) pluginSocketDir := newPluginsSocketDir(kubeletDirPath, legacyclient.Identity) volumes, volumeMounts := getVolumesAndMounts(pluginSocketDir) @@ -349,7 +351,7 @@ func doCreateLegacyDaemonset(ctx context.Context, args *Args) (err error) { var selectorValue string if !args.DryRun { - daemonset, err := k8s.KubeClient().AppsV1().DaemonSets(namespace).Get( + daemonset, err := t.client.Kube().AppsV1().DaemonSets(namespace).Get( ctx, consts.LegacyNodeServerName, metav1.GetOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { @@ -372,7 +374,7 @@ func doCreateLegacyDaemonset(ctx context.Context, args *Args) (err error) { daemonset := newDaemonset(podSpec, consts.LegacyNodeServerName, selectorValue, args) if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().AppsV1().DaemonSets(namespace).Create( + _, err = t.client.Kube().AppsV1().DaemonSets(namespace).Create( ctx, daemonset, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -383,11 +385,11 @@ func doCreateLegacyDaemonset(ctx context.Context, args *Args) (err error) { return args.writeObject(daemonset) } -func createDaemonset(ctx context.Context, args *Args) (err error) { +func (t daemonsetTask) createDaemonset(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, fmt.Sprintf("Creating %s Daemonset", consts.NodeServerName), 1, nil) { return errSendProgress } - if err := doCreateDaemonset(ctx, args); err != nil { + if err := t.doCreateDaemonset(ctx, args); err != nil { return err } if !sendProgressMessage(ctx, args.ProgressCh, fmt.Sprintf("Created %s Daemonset", consts.NodeServerName), 1, daemonsetComponent(consts.NodeServerName)) { @@ -401,7 +403,7 @@ func createDaemonset(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, fmt.Sprintf("Creating %s Daemonset", consts.LegacyNodeServerName), 2, nil) { return errSendProgress } - if err := doCreateLegacyDaemonset(ctx, args); err != nil { + if err := t.doCreateLegacyDaemonset(ctx, args); err != nil { return err } if !sendProgressMessage(ctx, args.ProgressCh, fmt.Sprintf("Created %s Daemonset", consts.LegacyNodeServerName), 2, daemonsetComponent(consts.LegacyNodeServerName)) { @@ -411,15 +413,15 @@ func createDaemonset(ctx context.Context, args *Args) (err error) { return nil } -func deleteDaemonset(ctx context.Context) error { - err := k8s.KubeClient().AppsV1().DaemonSets(namespace).Delete( +func (t daemonsetTask) deleteDaemonset(ctx context.Context) error { + err := t.client.Kube().AppsV1().DaemonSets(namespace).Delete( ctx, consts.NodeServerName, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { return err } - err = k8s.KubeClient().AppsV1().DaemonSets(namespace).Delete( + err = t.client.Kube().AppsV1().DaemonSets(namespace).Delete( ctx, consts.LegacyNodeServerName, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { diff --git a/pkg/installer/deployment.go b/pkg/admin/installer/deployment.go similarity index 87% rename from pkg/installer/deployment.go rename to pkg/admin/installer/deployment.go index 2229e9c8..b6d74d44 100644 --- a/pkg/installer/deployment.go +++ b/pkg/admin/installer/deployment.go @@ -21,8 +21,8 @@ import ( "fmt" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -31,7 +31,9 @@ import ( const deploymentFinalizer = consts.Identity + "/delete-protection" -type deploymentTask struct{} +type deploymentTask struct { + client *client.Client +} func (deploymentTask) Name() string { return "Deployment" @@ -55,15 +57,15 @@ func (deploymentTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (deploymentTask) Execute(ctx context.Context, args *Args) error { - return createDeployment(ctx, args) +func (t deploymentTask) Execute(ctx context.Context, args *Args) error { + return t.createDeployment(ctx, args) } -func (deploymentTask) Delete(ctx context.Context, _ *Args) error { - return deleteDeployment(ctx) +func (t deploymentTask) Delete(ctx context.Context, _ *Args) error { + return t.deleteDeployment(ctx) } -func doCreateDeployment(ctx context.Context, args *Args, legacy bool, step int) (err error) { +func (t deploymentTask) doCreateDeployment(ctx context.Context, args *Args, legacy bool, step int) (err error) { name := consts.ControllerServerName containerArgs := []string{name, fmt.Sprintf("--identity=%s", consts.Identity)} if legacy { @@ -179,7 +181,7 @@ func doCreateDeployment(ctx context.Context, args *Args, legacy bool, step int) var selectorValue string if !args.DryRun { - deployment, err := k8s.KubeClient().AppsV1().Deployments(namespace).Get( + deployment, err := t.client.Kube().AppsV1().Deployments(namespace).Get( ctx, name, metav1.GetOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { @@ -232,7 +234,7 @@ func doCreateDeployment(ctx context.Context, args *Args, legacy bool, step int) } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().AppsV1().Deployments(namespace).Create( + _, err = t.client.Kube().AppsV1().Deployments(namespace).Create( ctx, deployment, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -243,13 +245,13 @@ func doCreateDeployment(ctx context.Context, args *Args, legacy bool, step int) return args.writeObject(deployment) } -func createDeployment(ctx context.Context, args *Args) (err error) { - if err := doCreateDeployment(ctx, args, false, 1); err != nil { +func (t deploymentTask) createDeployment(ctx context.Context, args *Args) (err error) { + if err := t.doCreateDeployment(ctx, args, false, 1); err != nil { return err } if args.Legacy { - if err := doCreateDeployment(ctx, args, true, 2); err != nil { + if err := t.doCreateDeployment(ctx, args, true, 2); err != nil { return err } } @@ -271,8 +273,8 @@ func removeFinalizer(objectMeta *metav1.ObjectMeta, finalizer string) []string { return finalizers } -func doDeleteDeployment(ctx context.Context, name string) error { - deploymentClient := k8s.KubeClient().AppsV1().Deployments(namespace) +func (t deploymentTask) doDeleteDeployment(ctx context.Context, name string) error { + deploymentClient := t.client.Kube().AppsV1().Deployments(namespace) deployment, err := deploymentClient.Get(ctx, name, metav1.GetOptions{}) if err != nil { @@ -294,10 +296,10 @@ func doDeleteDeployment(ctx context.Context, name string) error { return nil } -func deleteDeployment(ctx context.Context) error { - if err := doDeleteDeployment(ctx, consts.ControllerServerName); err != nil { +func (t deploymentTask) deleteDeployment(ctx context.Context) error { + if err := t.doDeleteDeployment(ctx, consts.ControllerServerName); err != nil { return err } - return doDeleteDeployment(ctx, consts.LegacyControllerServerName) + return t.doDeleteDeployment(ctx, consts.LegacyControllerServerName) } diff --git a/pkg/installer/directpv.min.io_directpvdrives.yaml b/pkg/admin/installer/directpv.min.io_directpvdrives.yaml similarity index 100% rename from pkg/installer/directpv.min.io_directpvdrives.yaml rename to pkg/admin/installer/directpv.min.io_directpvdrives.yaml diff --git a/pkg/installer/directpv.min.io_directpvinitrequests.yaml b/pkg/admin/installer/directpv.min.io_directpvinitrequests.yaml similarity index 100% rename from pkg/installer/directpv.min.io_directpvinitrequests.yaml rename to pkg/admin/installer/directpv.min.io_directpvinitrequests.yaml diff --git a/pkg/installer/directpv.min.io_directpvnodes.yaml b/pkg/admin/installer/directpv.min.io_directpvnodes.yaml similarity index 100% rename from pkg/installer/directpv.min.io_directpvnodes.yaml rename to pkg/admin/installer/directpv.min.io_directpvnodes.yaml diff --git a/pkg/installer/directpv.min.io_directpvvolumes.yaml b/pkg/admin/installer/directpv.min.io_directpvvolumes.yaml similarity index 100% rename from pkg/installer/directpv.min.io_directpvvolumes.yaml rename to pkg/admin/installer/directpv.min.io_directpvvolumes.yaml diff --git a/pkg/installer/installer.go b/pkg/admin/installer/installer.go similarity index 51% rename from pkg/installer/installer.go rename to pkg/admin/installer/installer.go index cf80e38b..de9614cb 100644 --- a/pkg/installer/installer.go +++ b/pkg/admin/installer/installer.go @@ -18,61 +18,30 @@ package installer import ( "context" - "fmt" - "strconv" - "strings" "github.com/fatih/color" - "github.com/minio/directpv/pkg/k8s" + "github.com/minio/directpv/pkg/client" + legacyclient "github.com/minio/directpv/pkg/legacy/client" "github.com/minio/directpv/pkg/utils" - "k8s.io/apimachinery/pkg/util/version" - "k8s.io/klog/v2" ) -// Tasks is a list of tasks to performed during installation and uninstallation -var Tasks = []Task{ - namespaceTask{}, - rbacTask{}, - pspTask{}, - crdTask{}, - migrateTask{}, - csiDriverTask{}, - storageClassTask{}, - daemonsetTask{}, - deploymentTask{}, -} - -func getKubeVersion() (major, minor uint, err error) { - versionInfo, err := k8s.DiscoveryClient().ServerVersion() - if err != nil { - return 0, 0, err - } - - var u64 uint64 - if u64, err = strconv.ParseUint(versionInfo.Major, 10, 64); err != nil { - return 0, 0, fmt.Errorf("unable to parse major version %v; %v", versionInfo.Major, err) - } - major = uint(u64) - - minorString := versionInfo.Minor - if strings.Contains(versionInfo.GitVersion, "-eks-") { - // Do trimming only for EKS. - // Refer https://github.com/aws/containers-roadmap/issues/1404 - i := strings.IndexFunc(minorString, func(r rune) bool { return r < '0' || r > '9' }) - if i > -1 { - minorString = minorString[:i] - } +// GetDefaultTasks returns the installer tasks to be run +func GetDefaultTasks(client *client.Client, legacyClient *legacyclient.Client) []Task { + return []Task{ + namespaceTask{client}, + rbacTask{client}, + pspTask{client}, + crdTask{client}, + migrateTask{client, legacyClient}, + csiDriverTask{client}, + storageClassTask{client}, + daemonsetTask{client}, + deploymentTask{client}, } - if u64, err = strconv.ParseUint(minorString, 10, 64); err != nil { - return 0, 0, fmt.Errorf("unable to parse minor version %v; %v", minor, err) - } - minor = uint(u64) - - return major, minor, nil } // Install performs DirectPV installation on kubernetes. -func Install(ctx context.Context, args *Args) (err error) { +func Install(ctx context.Context, args *Args, tasks []Task) (err error) { defer func() { if !sendDoneMessage(ctx, args.ProgressCh, err) { err = errSendProgress @@ -84,25 +53,6 @@ func Install(ctx context.Context, args *Args) (err error) { return err } - switch { - case args.DryRun: - if args.KubeVersion == nil { - // default higher version - if args.KubeVersion, err = version.ParseSemantic("1.29.0"); err != nil { - klog.Fatalf("this should not happen; %v", err) - } - } - default: - major, minor, err := getKubeVersion() - if err != nil { - return err - } - args.KubeVersion, err = version.ParseSemantic(fmt.Sprintf("%v.%v.0", major, minor)) - if err != nil { - klog.Fatalf("this should not happen; %v", err) - } - } - if args.KubeVersion.Major() == 1 { if args.KubeVersion.Minor() < 20 { args.csiProvisionerImage = csiProvisionerImageV2_2_0 @@ -127,7 +77,7 @@ func Install(ctx context.Context, args *Args) (err error) { } } - for _, task := range Tasks { + for _, task := range tasks { if err := task.Start(ctx, args); err != nil { return err } @@ -141,12 +91,12 @@ func Install(ctx context.Context, args *Args) (err error) { } // Uninstall removes DirectPV from kubernetes. -func Uninstall(ctx context.Context, quiet, force bool) (err error) { +func Uninstall(ctx context.Context, quiet, force bool, tasks []Task) (err error) { args := &Args{ ForceUninstall: force, Quiet: quiet, } - for _, task := range Tasks { + for _, task := range tasks { if err := task.Delete(ctx, args); err != nil { return err } diff --git a/pkg/installer/installer_test.go b/pkg/admin/installer/installer_test.go similarity index 60% rename from pkg/installer/installer_test.go rename to pkg/admin/installer/installer_test.go index bdbae0d0..2230f66f 100644 --- a/pkg/installer/installer_test.go +++ b/pkg/admin/installer/installer_test.go @@ -23,12 +23,15 @@ import ( "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/k8s" + legacyclient "github.com/minio/directpv/pkg/legacy/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + versionpkg "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/version" ) func init() { client.FakeInit() + legacyclient.FakeInit() } var ( @@ -93,57 +96,15 @@ func getDiscoveryGroupsAndMethods() ([]*metav1.APIGroup, []*metav1.APIResourceLi return apiGroups, apiResourceList, nil } -func TestGetKubeVersion(t *testing.T) { - testCases := []struct { - info version.Info - major uint - minor uint - expectErr bool - }{ - {version.Info{Major: "a", Minor: "0"}, 0, 0, true}, // invalid major - {version.Info{Major: "-1", Minor: "0"}, 0, 0, true}, // invalid major - {version.Info{Major: "0", Minor: "a"}, 0, 0, true}, // invalid minor - {version.Info{Major: "0", Minor: "-1"}, 0, 0, true}, // invalid minor - {version.Info{Major: "0", Minor: "-1", GitVersion: "commit-eks-id"}, 0, 0, true}, // invalid minor for eks - {version.Info{Major: "0", Minor: "incompat", GitVersion: "commit-eks-"}, 0, 0, true}, // invalid minor for eks - {version.Info{Major: "0", Minor: "0"}, 0, 0, false}, - {version.Info{Major: "1", Minor: "0"}, 1, 0, false}, - {version.Info{Major: "0", Minor: "1"}, 0, 1, false}, - {version.Info{Major: "1", Minor: "18"}, 1, 18, false}, - {version.Info{Major: "1", Minor: "18+", GitVersion: "commit-eks-id"}, 1, 18, false}, - {version.Info{Major: "1", Minor: "18-", GitVersion: "commit-eks-id"}, 1, 18, false}, - {version.Info{Major: "1", Minor: "18incompat", GitVersion: "commit-eks-id"}, 1, 18, false}, - {version.Info{Major: "1", Minor: "18-incompat", GitVersion: "commit-eks-id"}, 1, 18, false}, - } - - for i, testCase := range testCases { - k8s.SetDiscoveryInterface(getDiscoveryGroupsAndMethods, &testCase.info) - major, minor, err := getKubeVersion() - if testCase.expectErr { - if err == nil { - t.Fatalf("case %v: expected error, but succeeded", i+1) - } - continue - } - - if err != nil { - t.Fatalf("case %v: unexpected error: %v", i+1, err) - } - - if major != testCase.major { - t.Fatalf("case %v: major: expected: %v, got: %v", i+1, testCase.major, major) - } - - if minor != testCase.minor { - t.Fatalf("case %v: minor: expected: %v, got: %v", i+1, testCase.minor, minor) - } - } -} - func TestInstallUinstall(t *testing.T) { + kversion, err := versionpkg.ParseSemantic("1.26.0") + if err != nil { + t.Fatalf("unable to parse version; %v", err) + } args := Args{ image: "directpv-0.0.0dev0", ObjectWriter: io.Discard, + KubeVersion: kversion, } testVersions := []version.Info{ @@ -161,16 +122,18 @@ func TestInstallUinstall(t *testing.T) { } for i, testVersion := range testVersions { - k8s.SetDiscoveryInterface(getDiscoveryGroupsAndMethods, &testVersion) + client := client.GetClient() + legacyClient := legacyclient.GetClient() + client.K8sClient.DiscoveryClient = k8s.NewFakeDiscovery(getDiscoveryGroupsAndMethods, &testVersion) ctx := context.TODO() args := args - if err := Install(ctx, &args); err != nil { + tasks := GetDefaultTasks(client, legacyClient) + if err := Install(ctx, &args, tasks); err != nil { t.Fatalf("case %v: unexpected error; %v", i+1, err) } - if err := Uninstall(ctx, false, true); err != nil { + if err := Uninstall(ctx, false, true, tasks); err != nil { t.Fatalf("csae %v: unexpected error; %v", i+1, err) } - _, err := k8s.KubeClient().CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) if err == nil { t.Fatalf("case %v: uninstall on kube version v%v.%v not removed namespace", i+1, testVersion.Major, testVersion.Minor) diff --git a/pkg/installer/migrate.go b/pkg/admin/installer/migrate.go similarity index 74% rename from pkg/installer/migrate.go rename to pkg/admin/installer/migrate.go index bc53f77b..8b3b1a83 100644 --- a/pkg/installer/migrate.go +++ b/pkg/admin/installer/migrate.go @@ -19,7 +19,6 @@ package installer import ( "context" "fmt" - "os" "strings" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" @@ -47,7 +46,10 @@ const ( legacyPurgeProtection = legacyclient.GroupName + "/purge-protection" ) -type migrateTask struct{} +type migrateTask struct { + client *client.Client + legacyClient *legacyclient.Client +} func (migrateTask) Name() string { return "Migration" @@ -67,15 +69,15 @@ func (migrateTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (migrateTask) Execute(ctx context.Context, args *Args) error { - return Migrate(ctx, args, true) +func (t migrateTask) Execute(ctx context.Context, args *Args) error { + return t.migrate(ctx, args, true) } func (migrateTask) Delete(_ context.Context, _ *Args) error { return nil } -func migrateDrives(ctx context.Context, dryRun bool, progressCh chan<- Message) (driveMap map[string]string, legacyDriveErrors, driveErrors map[string]error, err error) { +func (t migrateTask) migrateDrives(ctx context.Context, dryRun bool, progressCh chan<- Message) (driveMap map[string]string, legacyDriveErrors, driveErrors map[string]error, err error) { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() @@ -84,7 +86,7 @@ func migrateDrives(ctx context.Context, dryRun bool, progressCh chan<- Message) driveErrors = map[string]error{} fsUUIDs := make(utils.StringSet) - for result := range legacyclient.ListDrives(ctx) { + for result := range t.legacyClient.ListDrives(ctx) { if result.Err != nil { return nil, legacyDriveErrors, driveErrors, fmt.Errorf( "unable to get legacy drives; %v", result.Err, @@ -181,13 +183,13 @@ func migrateDrives(ctx context.Context, dryRun bool, progressCh chan<- Message) } } - existingDrive, err := client.DriveClient().Get(ctx, string(driveID), metav1.GetOptions{}) + existingDrive, err := t.client.Drive().Get(ctx, string(driveID), metav1.GetOptions{}) if err != nil { switch { case apierrors.IsNotFound(err): if !dryRun { sendProgressMessage(ctx, progressCh, fmt.Sprintf("Migrating directcsidrive %s to directpvdrive %s", result.Drive.Name, drive.Name), 1, nil) - _, err = client.DriveClient().Create(ctx, drive, metav1.CreateOptions{}) + _, err = t.client.Drive().Create(ctx, drive, metav1.CreateOptions{}) if err != nil { legacyDriveErrors[result.Drive.Name] = fmt.Errorf( "unable to create drive %v by migrating legacy drive %v; %w", @@ -219,14 +221,14 @@ func migrateDrives(ctx context.Context, dryRun bool, progressCh chan<- Message) return driveMap, legacyDriveErrors, driveErrors, nil } -func migrateVolumes(ctx context.Context, driveMap map[string]string, dryRun bool, progressCh chan<- Message) (legacyVolumeErrors, volumeErrors map[string]error, err error) { +func (t migrateTask) migrateVolumes(ctx context.Context, driveMap map[string]string, dryRun bool, progressCh chan<- Message) (legacyVolumeErrors, volumeErrors map[string]error, err error) { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() legacyVolumeErrors = map[string]error{} volumeErrors = map[string]error{} - for result := range legacyclient.ListVolumes(ctx) { + for result := range t.legacyClient.ListVolumes(ctx) { if result.Err != nil { return legacyVolumeErrors, volumeErrors, fmt.Errorf( "unable to get legacy volumes; %v", result.Err, @@ -296,13 +298,13 @@ func migrateVolumes(ctx context.Context, driveMap map[string]string, dryRun bool } } - existingVolume, err := client.VolumeClient().Get(ctx, name, metav1.GetOptions{}) + existingVolume, err := t.client.Volume().Get(ctx, name, metav1.GetOptions{}) if err != nil { switch { case apierrors.IsNotFound(err): if !dryRun { sendProgressMessage(ctx, progressCh, fmt.Sprintf("Migrating directcsivolume %s to directpvvolume %s", result.Volume.Name, volume.Name), 2, nil) - _, err = client.VolumeClient().Create(ctx, volume, metav1.CreateOptions{}) + _, err = t.client.Volume().Create(ctx, volume, metav1.CreateOptions{}) if err != nil { legacyVolumeErrors[result.Volume.Name] = fmt.Errorf( "unable to create volume %v by migrating legacy volume %v; %w", @@ -332,14 +334,14 @@ func migrateVolumes(ctx context.Context, driveMap map[string]string, dryRun bool } // Migrate migrates legacy drives and volumes. -func Migrate(ctx context.Context, args *Args, installer bool) (err error) { +func (t migrateTask) migrate(ctx context.Context, args *Args, installer bool) (err error) { if (installer && args.DryRun) || args.Declarative || !args.Legacy { return nil } legacyclient.Init() - version, _, err := legacyclient.GetGroupVersion("DirectCSIDrive") + version, _, err := legacyclient.GetGroupVersion(t.legacyClient.K8sClient, "DirectCSIDrive") if err != nil { return fmt.Errorf("unable to probe DirectCSIDrive version; %w", err) } @@ -350,7 +352,7 @@ func Migrate(ctx context.Context, args *Args, installer bool) (err error) { return fmt.Errorf("migration does not support DirectCSIDrive version %v", version) } - version, _, err = legacyclient.GetGroupVersion("DirectCSIVolume") + version, _, err = legacyclient.GetGroupVersion(t.legacyClient.K8sClient, "DirectCSIVolume") if err != nil { return fmt.Errorf("unable to probe DirectCSIVolume version; %w", err) } @@ -361,14 +363,14 @@ func Migrate(ctx context.Context, args *Args, installer bool) (err error) { return fmt.Errorf("migration does not support DirectCSIVolume version %v", version) } - driveMap, legacyDriveErrors, driveErrors, err := migrateDrives(ctx, args.DryRun, args.ProgressCh) + driveMap, legacyDriveErrors, driveErrors, err := t.migrateDrives(ctx, args.DryRun, args.ProgressCh) if err != nil { return err } if !sendProgressMessage(ctx, args.ProgressCh, "Migrated the drives", 1, nil) { return errSendProgress } - legacyVolumeErrors, volumeErrors, err := migrateVolumes(ctx, driveMap, args.DryRun, args.ProgressCh) + legacyVolumeErrors, volumeErrors, err := t.migrateVolumes(ctx, driveMap, args.DryRun, args.ProgressCh) if err != nil { return err } @@ -403,84 +405,7 @@ func Migrate(ctx context.Context, args *Args, installer bool) (err error) { return nil } -// RemoveLegacyDrives removes legacy drive CRDs. -func RemoveLegacyDrives(ctx context.Context, backupFile string) (backupCreated bool, err error) { - var drives []directv1beta5.DirectCSIDrive - for result := range legacyclient.ListDrives(ctx) { - if result.Err != nil { - return false, fmt.Errorf("unable to get legacy drives; %w", result.Err) - } - drives = append(drives, result.Drive) - } - if len(drives) == 0 { - return false, nil - } - - data, err := utils.ToYAML(directv1beta5.DirectCSIDriveList{ - TypeMeta: metav1.TypeMeta{ - Kind: "List", - APIVersion: "v1", - }, - Items: drives, - }) - if err != nil { - return false, fmt.Errorf("unable to generate legacy drives YAML; %w", err) - } - - if err = os.WriteFile(backupFile, data, os.ModePerm); err != nil { - return false, fmt.Errorf("unable to write legacy drives YAML; %w", err) - } - - for _, drive := range drives { - drive.Finalizers = []string{} - if _, err := legacyclient.DriveClient().Update(ctx, &drive, metav1.UpdateOptions{}); err != nil { - return false, fmt.Errorf("unable to update legacy drive %v; %w", drive.Name, err) - } - if err := legacyclient.DriveClient().Delete(ctx, drive.Name, metav1.DeleteOptions{}); err != nil { - return false, fmt.Errorf("unable to remove legacy drive %v; %w", drive.Name, err) - } - } - - return true, nil -} - -// RemoveLegacyVolumes removes legacy volume CRDs. -func RemoveLegacyVolumes(ctx context.Context, backupFile string) (backupCreated bool, err error) { - var volumes []directv1beta5.DirectCSIVolume - for result := range legacyclient.ListVolumes(ctx) { - if result.Err != nil { - return false, fmt.Errorf("unable to get legacy volumes; %w", result.Err) - } - volumes = append(volumes, result.Volume) - } - if len(volumes) == 0 { - return false, nil - } - - data, err := utils.ToYAML(directv1beta5.DirectCSIVolumeList{ - TypeMeta: metav1.TypeMeta{ - Kind: "List", - APIVersion: "v1", - }, - Items: volumes, - }) - if err != nil { - return false, fmt.Errorf("unable to generate legacy volumes YAML; %w", err) - } - - if err = os.WriteFile(backupFile, data, os.ModePerm); err != nil { - return false, fmt.Errorf("unable to write legacy volumes YAML; %w", err) - } - - for _, volume := range volumes { - volume.Finalizers = nil - if _, err := legacyclient.VolumeClient().Update(ctx, &volume, metav1.UpdateOptions{}); err != nil { - return false, fmt.Errorf("unable to update legacy volume %v; %w", volume.Name, err) - } - if err := legacyclient.VolumeClient().Delete(ctx, volume.Name, metav1.DeleteOptions{}); err != nil { - return false, fmt.Errorf("unable to remove legacy volume %v; %w", volume.Name, err) - } - } - - return true, nil +// Migrate migrates the resources using the provided clients +func Migrate(ctx context.Context, args *Args, client *client.Client, legacyClient *legacyclient.Client) error { + return migrateTask{client, legacyClient}.migrate(ctx, args, false) } diff --git a/pkg/installer/migrate_test.go b/pkg/admin/installer/migrate_test.go similarity index 94% rename from pkg/installer/migrate_test.go rename to pkg/admin/installer/migrate_test.go index 5aa2ab0e..c52e62f3 100644 --- a/pkg/installer/migrate_test.go +++ b/pkg/admin/installer/migrate_test.go @@ -41,7 +41,8 @@ func TestMigrateDrivesError(t *testing.T) { } legacyclient.SetDriveClient(legacyclientsetfake.NewSimpleClientset(drive)) - driveMap, legacyDriveErrors, driveErrors, err := migrateDrives(context.TODO(), false, nil) + task := migrateTask{client.GetClient(), legacyclient.GetClient()} + driveMap, legacyDriveErrors, driveErrors, err := task.migrateDrives(context.TODO(), false, nil) if len(driveMap) == 0 && len(legacyDriveErrors) == 0 && len(driveErrors) == 0 && err == nil { t.Fatalf("expected error, but succeeded\n") } @@ -55,7 +56,7 @@ func TestMigrateDrivesError(t *testing.T) { }, } legacyclient.SetDriveClient(legacyclientsetfake.NewSimpleClientset(drive)) - driveMap, legacyDriveErrors, driveErrors, err = migrateDrives(context.TODO(), false, nil) + driveMap, legacyDriveErrors, driveErrors, err = task.migrateDrives(context.TODO(), false, nil) if len(driveMap) == 0 && len(legacyDriveErrors) == 0 && len(driveErrors) == 0 && err == nil { t.Fatalf("expected error, but succeeded\n") } @@ -78,7 +79,7 @@ func TestMigrateDrivesError(t *testing.T) { }, } legacyclient.SetDriveClient(legacyclientsetfake.NewSimpleClientset(drive1, drive2)) - driveMap, legacyDriveErrors, driveErrors, err = migrateDrives(context.TODO(), false, nil) + driveMap, legacyDriveErrors, driveErrors, err = task.migrateDrives(context.TODO(), false, nil) if len(driveMap) == 0 && len(legacyDriveErrors) == 0 && len(driveErrors) == 0 && err == nil { t.Fatalf("expected error, but succeeded\n") } @@ -88,8 +89,8 @@ func TestMigrateNoDrives(t *testing.T) { legacyclient.SetDriveClient(legacyclientsetfake.NewSimpleClientset()) clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - - _, legacyDriveErrors, driveErrors, err := migrateDrives(context.TODO(), false, nil) + task := migrateTask{client.GetClient(), legacyclient.GetClient()} + _, legacyDriveErrors, driveErrors, err := task.migrateDrives(context.TODO(), false, nil) if len(legacyDriveErrors) != 0 || len(driveErrors) != 0 || err != nil { t.Fatalf("unexpected error; %v\n", err) } @@ -115,7 +116,7 @@ func TestMigrateNoDrives(t *testing.T) { clientset = types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - _, legacyDriveErrors, driveErrors, err = migrateDrives(context.TODO(), false, nil) + _, legacyDriveErrors, driveErrors, err = task.migrateDrives(context.TODO(), false, nil) if len(legacyDriveErrors) != 0 || len(driveErrors) != 0 || err != nil { t.Fatalf("unexpected error; %v\n", err) } @@ -204,8 +205,8 @@ func TestMigrateReadyDrive(t *testing.T) { legacyclient.SetDriveClient(legacyclientsetfake.NewSimpleClientset(drive)) clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - - driveMap, legacyDriveErrors, driveErrors, err := migrateDrives(context.TODO(), false, nil) + task := migrateTask{client.GetClient(), legacyclient.GetClient()} + driveMap, legacyDriveErrors, driveErrors, err := task.migrateDrives(context.TODO(), false, nil) if len(legacyDriveErrors) != 0 || len(driveErrors) != 0 || err != nil { t.Fatalf("unexpected error; %v, %v, %v\n", legacyDriveErrors, driveErrors, err) } @@ -340,8 +341,8 @@ func TestMigrateInUseDrive(t *testing.T) { legacyclient.SetDriveClient(legacyclientsetfake.NewSimpleClientset(drive)) clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - - driveMap, legacyDriveErrors, driveErrors, err := migrateDrives(context.TODO(), false, nil) + task := migrateTask{client.GetClient(), legacyclient.GetClient()} + driveMap, legacyDriveErrors, driveErrors, err := task.migrateDrives(context.TODO(), false, nil) if len(legacyDriveErrors) != 0 || len(driveErrors) != 0 || err != nil { t.Fatalf("unexpected error; %v, %v, %v\n", legacyDriveErrors, driveErrors, err) } @@ -451,8 +452,8 @@ func TestMigrateVolumes(t *testing.T) { legacyclient.SetVolumeClient(legacyclientsetfake.NewSimpleClientset(volume)) clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) client.SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) - - legacyVolumeErrors, volumeErrors, err := migrateVolumes( + task := migrateTask{client.GetClient(), legacyclient.GetClient()} + legacyVolumeErrors, volumeErrors, err := task.migrateVolumes( context.TODO(), map[string]string{"08450612-7ab3-40f9-ab83-38645fba6d29": "a9908089-96dd-4e8b-8f06-0c0b5e391f39"}, false, @@ -492,7 +493,7 @@ func TestMigrateVolumes(t *testing.T) { // no fsuuid found error legacyclient.SetVolumeClient(legacyclientsetfake.NewSimpleClientset(volume)) - legacyVolumeErrors, volumeErrors, err = migrateVolumes(context.TODO(), map[string]string{}, false, nil) + legacyVolumeErrors, volumeErrors, err = task.migrateVolumes(context.TODO(), map[string]string{}, false, nil) if len(legacyVolumeErrors) == 0 && len(volumeErrors) == 0 && err == nil { t.Fatalf("expected error; but succeeded\n") } diff --git a/pkg/installer/namespace.go b/pkg/admin/installer/namespace.go similarity index 82% rename from pkg/installer/namespace.go rename to pkg/admin/installer/namespace.go index feeb624d..5e7a1fb8 100644 --- a/pkg/installer/namespace.go +++ b/pkg/admin/installer/namespace.go @@ -20,14 +20,16 @@ import ( "context" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/k8s" + "github.com/minio/directpv/pkg/client" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" podsecurityadmissionapi "k8s.io/pod-security-admission/api" ) -type namespaceTask struct{} +type namespaceTask struct { + client *client.Client +} func (namespaceTask) Name() string { return "Namespace" @@ -40,8 +42,8 @@ func (namespaceTask) Start(ctx context.Context, args *Args) error { return nil } -func (namespaceTask) Execute(ctx context.Context, args *Args) error { - return createNamespace(ctx, args) +func (t namespaceTask) Execute(ctx context.Context, args *Args) error { + return t.createNamespace(ctx, args) } func (namespaceTask) End(ctx context.Context, args *Args, err error) error { @@ -51,11 +53,11 @@ func (namespaceTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (namespaceTask) Delete(ctx context.Context, _ *Args) error { - return deleteNamespace(ctx) +func (t namespaceTask) Delete(ctx context.Context, _ *Args) error { + return t.deleteNamespace(ctx) } -func createNamespace(ctx context.Context, args *Args) (err error) { +func (t namespaceTask) createNamespace(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating namespace", 1, nil) { return errSendProgress } @@ -99,7 +101,7 @@ func createNamespace(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) + _, err = t.client.Kube().CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) if err != nil && !apierrors.IsAlreadyExists(err) { return err } @@ -108,9 +110,9 @@ func createNamespace(ctx context.Context, args *Args) (err error) { return args.writeObject(ns) } -func deleteNamespace(ctx context.Context) error { +func (t namespaceTask) deleteNamespace(ctx context.Context) error { propagationPolicy := metav1.DeletePropagationForeground - err := k8s.KubeClient().CoreV1().Namespaces().Delete( + err := t.client.Kube().CoreV1().Namespaces().Delete( ctx, namespace, metav1.DeleteOptions{PropagationPolicy: &propagationPolicy}, ) if err != nil { diff --git a/pkg/installer/progress.go b/pkg/admin/installer/progress.go similarity index 100% rename from pkg/installer/progress.go rename to pkg/admin/installer/progress.go diff --git a/pkg/installer/psp.go b/pkg/admin/installer/psp.go similarity index 82% rename from pkg/installer/psp.go rename to pkg/admin/installer/psp.go index 9af06d56..9efa3e77 100644 --- a/pkg/installer/psp.go +++ b/pkg/admin/installer/psp.go @@ -21,8 +21,8 @@ import ( "errors" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" corev1 "k8s.io/api/core/v1" policy "k8s.io/api/policy/v1beta1" rbac "k8s.io/api/rbac/v1" @@ -35,7 +35,9 @@ const pspClusterRoleBindingName = "psp-" + consts.Identity var errPSPUnsupported = errors.New("pod security policy is not supported in your kubernetes version") -type pspTask struct{} +type pspTask struct { + client *client.Client +} func (pspTask) Name() string { return "PSP" @@ -55,12 +57,12 @@ func (pspTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (pspTask) Execute(ctx context.Context, args *Args) error { - return createPSP(ctx, args) +func (t pspTask) Execute(ctx context.Context, args *Args) error { + return t.createPSP(ctx, args) } -func (pspTask) Delete(ctx context.Context, _ *Args) error { - major, minor, err := getKubeVersion() +func (t pspTask) Delete(ctx context.Context, _ *Args) error { + major, minor, err := t.client.K8s().GetKubeVersion() if err != nil { return err } @@ -68,10 +70,10 @@ func (pspTask) Delete(ctx context.Context, _ *Args) error { if podSecurityAdmission { return nil } - return deletePSP(ctx) + return t.deletePSP(ctx) } -func createPSPClusterRoleBinding(ctx context.Context, args *Args) (err error) { +func (t pspTask) createPSPClusterRoleBinding(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating psp cluster role binding", 2, nil) { return errSendProgress } @@ -110,7 +112,7 @@ func createPSPClusterRoleBinding(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{}) + _, err = t.client.Kube().RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{}) if err != nil && !apierrors.IsAlreadyExists(err) { return err } @@ -119,7 +121,7 @@ func createPSPClusterRoleBinding(ctx context.Context, args *Args) (err error) { return args.writeObject(crb) } -func createPodSecurityPolicy(ctx context.Context, args *Args) (err error) { +func (t pspTask) createPodSecurityPolicy(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating pod security policy", 1, nil) { return errSendProgress } @@ -173,7 +175,7 @@ func createPodSecurityPolicy(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().PolicyV1beta1().PodSecurityPolicies().Create( + _, err = t.client.Kube().PolicyV1beta1().PodSecurityPolicies().Create( ctx, psp, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -184,14 +186,14 @@ func createPodSecurityPolicy(ctx context.Context, args *Args) (err error) { return args.writeObject(psp) } -func createPSP(ctx context.Context, args *Args) error { +func (t pspTask) createPSP(ctx context.Context, args *Args) error { if args.podSecurityAdmission { return nil } var gvk *schema.GroupVersionKind if !args.DryRun { var err error - if gvk, err = k8s.GetGroupVersionKind("policy", "PodSecurityPolicy", "v1beta1"); err != nil { + if gvk, err = t.client.K8s().GetGroupVersionKind("policy", "PodSecurityPolicy", "v1beta1"); err != nil { return err } } else { @@ -199,24 +201,24 @@ func createPSP(ctx context.Context, args *Args) error { } if gvk.Version == "v1beta1" { - if err := createPodSecurityPolicy(ctx, args); err != nil { + if err := t.createPodSecurityPolicy(ctx, args); err != nil { return err } - return createPSPClusterRoleBinding(ctx, args) + return t.createPSPClusterRoleBinding(ctx, args) } return errPSPUnsupported } -func deletePSP(ctx context.Context) error { - err := k8s.KubeClient().RbacV1().ClusterRoleBindings().Delete( +func (t pspTask) deletePSP(ctx context.Context) error { + err := t.client.Kube().RbacV1().ClusterRoleBindings().Delete( ctx, pspClusterRoleBindingName, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { return err } - err = k8s.KubeClient().PolicyV1beta1().PodSecurityPolicies().Delete( + err = t.client.Kube().PolicyV1beta1().PodSecurityPolicies().Delete( ctx, consts.Identity, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { diff --git a/pkg/installer/rbac.go b/pkg/admin/installer/rbac.go similarity index 85% rename from pkg/installer/rbac.go rename to pkg/admin/installer/rbac.go index 190b653c..4bd1773b 100644 --- a/pkg/installer/rbac.go +++ b/pkg/admin/installer/rbac.go @@ -20,8 +20,8 @@ import ( "context" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -50,7 +50,9 @@ func newPolicyRule(resources []string, apiGroups []string, verbs ...string) rbac } } -type rbacTask struct{} +type rbacTask struct { + client *client.Client +} func (rbacTask) Name() string { return "RBAC" @@ -70,15 +72,15 @@ func (rbacTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (rbacTask) Execute(ctx context.Context, args *Args) error { - return createRBAC(ctx, args) +func (t rbacTask) Execute(ctx context.Context, args *Args) error { + return t.createRBAC(ctx, args) } -func (rbacTask) Delete(ctx context.Context, _ *Args) error { - return deleteRBAC(ctx) +func (t rbacTask) Delete(ctx context.Context, _ *Args) error { + return t.deleteRBAC(ctx) } -func createServiceAccount(ctx context.Context, args *Args) (err error) { +func (t rbacTask) createServiceAccount(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating service account", 1, nil) { return errSendProgress } @@ -108,7 +110,7 @@ func createServiceAccount(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().CoreV1().ServiceAccounts(namespace).Create( + _, err = t.client.Kube().CoreV1().ServiceAccounts(namespace).Create( ctx, serviceAccount, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -119,7 +121,7 @@ func createServiceAccount(ctx context.Context, args *Args) (err error) { return args.writeObject(serviceAccount) } -func createClusterRole(ctx context.Context, args *Args) (err error) { +func (t rbacTask) createClusterRole(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating cluster role", 2, nil) { return errSendProgress } @@ -175,7 +177,7 @@ func createClusterRole(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().RbacV1().ClusterRoles().Create( + _, err = t.client.Kube().RbacV1().ClusterRoles().Create( ctx, clusterRole, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -186,7 +188,7 @@ func createClusterRole(ctx context.Context, args *Args) (err error) { return args.writeObject(clusterRole) } -func createClusterRoleBinding(ctx context.Context, args *Args) (err error) { +func (t rbacTask) createClusterRoleBinding(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating cluster role binding", 3, nil) { return errSendProgress } @@ -226,7 +228,7 @@ func createClusterRoleBinding(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().RbacV1().ClusterRoleBindings().Create( + _, err = t.client.Kube().RbacV1().ClusterRoleBindings().Create( ctx, clusterRoleBinding, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -237,7 +239,7 @@ func createClusterRoleBinding(ctx context.Context, args *Args) (err error) { return args.writeObject(clusterRoleBinding) } -func createRole(ctx context.Context, args *Args) (err error) { +func (t rbacTask) createRole(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating role", 4, nil) { return errSendProgress } @@ -268,7 +270,7 @@ func createRole(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().RbacV1().Roles(namespace).Create( + _, err = t.client.Kube().RbacV1().Roles(namespace).Create( ctx, role, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -279,7 +281,7 @@ func createRole(ctx context.Context, args *Args) (err error) { return args.writeObject(role) } -func createRoleBinding(ctx context.Context, args *Args) (err error) { +func (t rbacTask) createRoleBinding(ctx context.Context, args *Args) (err error) { if !sendProgressMessage(ctx, args.ProgressCh, "Creating role binding", 5, nil) { return errSendProgress } @@ -319,7 +321,7 @@ func createRoleBinding(ctx context.Context, args *Args) (err error) { } if !args.DryRun && !args.Declarative { - _, err = k8s.KubeClient().RbacV1().RoleBindings(namespace).Create( + _, err = t.client.Kube().RbacV1().RoleBindings(namespace).Create( ctx, roleBinding, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -330,52 +332,52 @@ func createRoleBinding(ctx context.Context, args *Args) (err error) { return args.writeObject(roleBinding) } -func createRBAC(ctx context.Context, args *Args) (err error) { - if err = createServiceAccount(ctx, args); err != nil { +func (t rbacTask) createRBAC(ctx context.Context, args *Args) (err error) { + if err = t.createServiceAccount(ctx, args); err != nil { return err } - if err = createClusterRole(ctx, args); err != nil { + if err = t.createClusterRole(ctx, args); err != nil { return err } - if err = createClusterRoleBinding(ctx, args); err != nil { + if err = t.createClusterRoleBinding(ctx, args); err != nil { return err } - if err = createRole(ctx, args); err != nil { + if err = t.createRole(ctx, args); err != nil { return err } - return createRoleBinding(ctx, args) + return t.createRoleBinding(ctx, args) } -func deleteRBAC(ctx context.Context) error { - err := k8s.KubeClient().CoreV1().ServiceAccounts(namespace).Delete( +func (t rbacTask) deleteRBAC(ctx context.Context) error { + err := t.client.Kube().CoreV1().ServiceAccounts(namespace).Delete( ctx, consts.Identity, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { return err } - err = k8s.KubeClient().RbacV1().ClusterRoles().Delete( + err = t.client.Kube().RbacV1().ClusterRoles().Delete( ctx, consts.Identity, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { return err } - err = k8s.KubeClient().RbacV1().ClusterRoleBindings().Delete( + err = t.client.Kube().RbacV1().ClusterRoleBindings().Delete( ctx, consts.Identity, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { return err } - err = k8s.KubeClient().RbacV1().Roles(namespace).Delete( + err = t.client.Kube().RbacV1().Roles(namespace).Delete( ctx, consts.Identity, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { return err } - err = k8s.KubeClient().RbacV1().RoleBindings(namespace).Delete( + err = t.client.Kube().RbacV1().RoleBindings(namespace).Delete( ctx, consts.Identity, metav1.DeleteOptions{}, ) if err != nil && !apierrors.IsNotFound(err) { diff --git a/pkg/installer/storageclass.go b/pkg/admin/installer/storageclass.go similarity index 77% rename from pkg/installer/storageclass.go rename to pkg/admin/installer/storageclass.go index d25bf600..8c4fce60 100644 --- a/pkg/installer/storageclass.go +++ b/pkg/admin/installer/storageclass.go @@ -22,8 +22,8 @@ import ( "fmt" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/k8s" legacyclient "github.com/minio/directpv/pkg/legacy/client" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" @@ -34,7 +34,9 @@ import ( var errStorageClassVersionUnsupported = errors.New("unsupported StorageClass version found") -type storageClassTask struct{} +type storageClassTask struct { + client *client.Client +} func (storageClassTask) Name() string { return "StorageClass" @@ -58,15 +60,15 @@ func (storageClassTask) End(ctx context.Context, args *Args, err error) error { return nil } -func (storageClassTask) Execute(ctx context.Context, args *Args) error { - return createStorageClass(ctx, args) +func (t storageClassTask) Execute(ctx context.Context, args *Args) error { + return t.createStorageClass(ctx, args) } -func (storageClassTask) Delete(ctx context.Context, _ *Args) error { - return deleteStorageClass(ctx) +func (t storageClassTask) Delete(ctx context.Context, _ *Args) error { + return t.deleteStorageClass(ctx) } -func doCreateStorageClass(ctx context.Context, args *Args, version string, legacy bool, step int) (err error) { +func (t storageClassTask) doCreateStorageClass(ctx context.Context, args *Args, version string, legacy bool, step int) (err error) { name := consts.Identity if legacy { name = legacyclient.Identity @@ -118,7 +120,7 @@ func doCreateStorageClass(ctx context.Context, args *Args, version string, legac } if !args.DryRun && !args.Declarative { - _, err := k8s.KubeClient().StorageV1().StorageClasses().Create( + _, err := t.client.Kube().StorageV1().StorageClasses().Create( ctx, storageClass, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -152,7 +154,7 @@ func doCreateStorageClass(ctx context.Context, args *Args, version string, legac } if !args.DryRun && !args.Declarative { - _, err := k8s.KubeClient().StorageV1beta1().StorageClasses().Create( + _, err := t.client.Kube().StorageV1beta1().StorageClasses().Create( ctx, storageClass, metav1.CreateOptions{}, ) if err != nil && !apierrors.IsAlreadyExists(err) { @@ -167,7 +169,7 @@ func doCreateStorageClass(ctx context.Context, args *Args, version string, legac } } -func createStorageClass(ctx context.Context, args *Args) (err error) { +func (t storageClassTask) createStorageClass(ctx context.Context, args *Args) (err error) { version := "v1" switch { case args.DryRun: @@ -175,19 +177,19 @@ func createStorageClass(ctx context.Context, args *Args) (err error) { version = "v1beta1" } default: - gvk, err := k8s.GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") + gvk, err := t.client.K8s().GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") if err != nil { return err } version = gvk.Version } - if err := doCreateStorageClass(ctx, args, version, false, 1); err != nil { + if err := t.doCreateStorageClass(ctx, args, version, false, 1); err != nil { return err } if args.Legacy { - if err := doCreateStorageClass(ctx, args, version, true, 2); err != nil { + if err := t.doCreateStorageClass(ctx, args, version, true, 2); err != nil { return err } } @@ -195,14 +197,14 @@ func createStorageClass(ctx context.Context, args *Args) (err error) { return nil } -func doDeleteStorageClass(ctx context.Context, version, name string) (err error) { +func (t storageClassTask) doDeleteStorageClass(ctx context.Context, version, name string) (err error) { switch version { case "v1": - err = k8s.KubeClient().StorageV1().StorageClasses().Delete( + err = t.client.Kube().StorageV1().StorageClasses().Delete( ctx, name, metav1.DeleteOptions{}, ) case "v1beta1": - err = k8s.KubeClient().StorageV1beta1().StorageClasses().Delete( + err = t.client.Kube().StorageV1beta1().StorageClasses().Delete( ctx, name, metav1.DeleteOptions{}, ) default: @@ -216,15 +218,15 @@ func doDeleteStorageClass(ctx context.Context, version, name string) (err error) return nil } -func deleteStorageClass(ctx context.Context) error { - gvk, err := k8s.GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") +func (t storageClassTask) deleteStorageClass(ctx context.Context) error { + gvk, err := t.client.K8s().GetGroupVersionKind("storage.k8s.io", "CSIDriver", "v1", "v1beta1") if err != nil { return err } - if err = doDeleteStorageClass(ctx, gvk.Version, consts.StorageClassName); err != nil { + if err = t.doDeleteStorageClass(ctx, gvk.Version, consts.StorageClassName); err != nil { return err } - return doDeleteStorageClass(ctx, gvk.Version, legacyclient.Identity) + return t.doDeleteStorageClass(ctx, gvk.Version, legacyclient.Identity) } diff --git a/pkg/installer/utils.go b/pkg/admin/installer/utils.go similarity index 100% rename from pkg/installer/utils.go rename to pkg/admin/installer/utils.go diff --git a/pkg/installer/vars.go b/pkg/admin/installer/vars.go similarity index 100% rename from pkg/installer/vars.go rename to pkg/admin/installer/vars.go diff --git a/pkg/admin/label_drives.go b/pkg/admin/label_drives.go new file mode 100644 index 00000000..6b036a44 --- /dev/null +++ b/pkg/admin/label_drives.go @@ -0,0 +1,133 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// Label represents the label to be set +type Label struct { + Key directpvtypes.LabelKey + Value directpvtypes.LabelValue + Remove bool +} + +func (l Label) String() string { + if l.Value == "" { + return string(l.Key) + } + return string(l.Key) + ":" + string(l.Value) +} + +// LabelDriveResult represents the labeled drives +type LabelDriveResult struct { + NodeID directpvtypes.NodeID + DriveName directpvtypes.DriveName +} + +// LabelDriveArgs represents the arguments for adding/removing labels on/from the drives +type LabelDriveArgs struct { + Nodes []string + Drives []string + DriveStatus []directpvtypes.DriveStatus + DriveIDs []directpvtypes.DriveID + LabelSelectors map[directpvtypes.LabelKey]directpvtypes.LabelValue + DryRun bool +} + +// LabelDrives sets/removes labels on/from the drives +func (client *Client) LabelDrives(ctx context.Context, args LabelDriveArgs, labels []Label, log LogFunc) (results []LabelDriveResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewDriveLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + StatusSelector(args.DriveStatus). + DriveIDSelector(args.DriveIDs). + LabelSelector(args.LabelSelectors). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + processed = true + drive := &result.Drive + var verb string + for i := range labels { + updateFunc := func() (err error) { + if labels[i].Remove { + if ok := drive.RemoveLabel(labels[i].Key); !ok { + return + } + verb = "removed from" + } else { + if ok := drive.SetLabel(labels[i].Key, labels[i].Value); !ok { + return + } + verb = "set on" + } + if !args.DryRun { + drive, err = client.Drive().Update(ctx, drive, metav1.UpdateOptions{}) + } + if err != nil { + log( + LogMessage{ + Type: ErrorLogType, + Err: err, + Message: "unable to " + verb + " label to drive", + Values: map[string]any{"node": drive.GetNodeID(), "driveName": drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("%v/%v: %v\n", drive.GetNodeID(), drive.GetDriveName(), err), + }, + ) + } else { + log( + LogMessage{ + Type: InfoLogType, + Message: "label successfully " + verb + " label to drive", + Values: map[string]any{"label": labels[i].String(), "verb": verb, "node": drive.GetNodeID(), "driveName": drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("Label '%s' successfully %s %v/%v\n", labels[i].String(), verb, drive.GetNodeID(), drive.GetDriveName()), + }, + ) + } + results = append(results, LabelDriveResult{ + NodeID: drive.GetNodeID(), + DriveName: drive.GetDriveName(), + }) + return + } + retry.RetryOnConflict(retry.DefaultRetry, updateFunc) + } + } + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/label_volumes.go b/pkg/admin/label_volumes.go new file mode 100644 index 00000000..ddf887f6 --- /dev/null +++ b/pkg/admin/label_volumes.go @@ -0,0 +1,125 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// LabelVolumeArgs represents the arguments for adding/removing labels on/from the volumes +type LabelVolumeArgs struct { + Nodes []string + Drives []string + DriveIDs []string + PodNames []string + PodNamespaces []string + VolumeStatus []directpvtypes.VolumeStatus + VolumeNames []string + LabelSelectors map[directpvtypes.LabelKey]directpvtypes.LabelValue + DryRun bool +} + +// LabelVolumeResult represents the labeled volume +type LabelVolumeResult struct { + NodeID directpvtypes.NodeID + VolumeName string +} + +// LabelVolumes sets/removes labels on/from the volumes +func (client *Client) LabelVolumes(ctx context.Context, args LabelVolumeArgs, labels []Label, log LogFunc) (results []LabelVolumeResult, err error) { + if log == nil { + log = nullLogger + } + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + var processed bool + resultCh := client.NewVolumeLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + DriveIDSelector(utils.ToLabelValues(args.DriveIDs)). + PodNameSelector(utils.ToLabelValues(args.PodNames)). + PodNSSelector(utils.ToLabelValues(args.PodNamespaces)). + StatusSelector(args.VolumeStatus). + VolumeNameSelector(args.VolumeNames). + LabelSelector(args.LabelSelectors). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + var verb string + processed = true + volume := &result.Volume + for i := range labels { + updateFunc := func() (err error) { + if labels[i].Remove { + if ok := volume.RemoveLabel(labels[i].Key); !ok { + return + } + verb = "removed from" + } else { + if ok := volume.SetLabel(labels[i].Key, labels[i].Value); !ok { + return + } + verb = "set on" + } + if !args.DryRun { + volume, err = client.Volume().Update(ctx, volume, metav1.UpdateOptions{}) + } + if err != nil { + log( + LogMessage{ + Type: ErrorLogType, + Err: err, + Message: "unable to " + verb + " label to volume", + Values: map[string]any{"verb": verb, "volume": volume.Name}, + FormattedMessage: fmt.Sprintf("%v: %v\n", volume.Name, err), + }, + ) + } else { + log( + LogMessage{ + Type: InfoLogType, + Message: "label successfully " + verb + " label to volume", + Values: map[string]any{"label": labels[i].String(), "verb": verb, "volume": volume.Name}, + FormattedMessage: fmt.Sprintf("Label '%s' successfully %s %v\n", labels[i].String(), verb, volume.Name), + }, + ) + } + results = append(results, LabelVolumeResult{ + NodeID: volume.GetNodeID(), + VolumeName: volume.Name, + }) + return + } + retry.RetryOnConflict(retry.DefaultRetry, updateFunc) + } + } + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/logger.go b/pkg/admin/logger.go new file mode 100644 index 00000000..8b57daf4 --- /dev/null +++ b/pkg/admin/logger.go @@ -0,0 +1,44 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +// LogType represents the type of the log +type LogType int + +const ( + // UnknownLogType denotes the dummy LogType + UnknownLogType LogType = iota + // ErrorLogType denotes the error LogType + ErrorLogType + // InfoLogType denotes the non-error info LogType + InfoLogType +) + +// LogMessage represents the log message +type LogMessage struct { + Type LogType + Err error + Code string + Message string + Values map[string]any + FormattedMessage string +} + +// LogFunc represents the logger +type LogFunc func(LogMessage) + +func nullLogger(_ LogMessage) {} diff --git a/pkg/admin/migrate.go b/pkg/admin/migrate.go new file mode 100644 index 00000000..96977a75 --- /dev/null +++ b/pkg/admin/migrate.go @@ -0,0 +1,78 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "errors" + "fmt" + + "github.com/minio/directpv/pkg/admin/installer" + "github.com/minio/directpv/pkg/consts" + legacyclient "github.com/minio/directpv/pkg/legacy/client" +) + +// MigrateArgs denotest the migrate arguments +type MigrateArgs struct { + Quiet bool + Retain bool + DrivesBackupFile string + VolumesBackupFile string +} + +// Migrate migrates the directpv resources +func (client *Client) Migrate(ctx context.Context, args MigrateArgs) error { + if !args.Retain { + if args.DrivesBackupFile == "" || args.VolumesBackupFile == "" { + return errors.New("backup file should not be empty") + } + if args.DrivesBackupFile == args.VolumesBackupFile { + return errors.New("backup filenames are same") + } + } + legacyClient, err := legacyclient.NewClient(client.K8s()) + if err != nil { + return fmt.Errorf("unable to create legacy client; %v", err) + } + if err := installer.Migrate(ctx, &installer.Args{ + Quiet: args.Quiet, + Legacy: true, + }, client.Client, legacyClient); err != nil { + return fmt.Errorf("migration failed; %v", err) + } + if !args.Quiet { + fmt.Println("Migration successful; Please restart the pods in '" + consts.AppName + "' namespace.") + } + if args.Retain { + return nil + } + backupCreated, err := legacyClient.RemoveAllDrives(ctx, args.DrivesBackupFile) + if err != nil { + return fmt.Errorf("unable to remove legacy drive CRDs; %v", err) + } + if backupCreated && !args.Quiet { + fmt.Println("Legacy drive CRDs backed up to", args.DrivesBackupFile) + } + backupCreated, err = legacyClient.RemoveAllVolumes(ctx, args.VolumesBackupFile) + if err != nil { + return fmt.Errorf("unable to remove legacy volume CRDs; %v", err) + } + if backupCreated && !args.Quiet { + fmt.Println("Legacy volume CRDs backed up to", args.VolumesBackupFile) + } + return nil +} diff --git a/pkg/admin/move.go b/pkg/admin/move.go new file mode 100644 index 00000000..3d5c52bc --- /dev/null +++ b/pkg/admin/move.go @@ -0,0 +1,138 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "errors" + "fmt" + + "github.com/dustin/go-humanize" + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MoveArgs represents the args for moving +type MoveArgs struct { + Source directpvtypes.DriveID + Destination directpvtypes.DriveID +} + +// Move - moves the volume references from source to destination +func (client *Client) Move(ctx context.Context, args MoveArgs, log LogFunc) error { + if log == nil { + log = nullLogger + } + + if args.Source == args.Destination { + return errors.New("source and destination drives are same") + } + + srcDrive, err := client.Drive().Get(ctx, string(args.Source), metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("unable to get source drive; %v", err) + } + + if !srcDrive.IsUnschedulable() { + return errors.New("source drive is not cordoned") + } + + sourceVolumeNames := srcDrive.GetVolumes() + if len(sourceVolumeNames) == 0 { + return fmt.Errorf("no volumes found in source drive %v", args.Source) + } + + var requiredCapacity int64 + var volumes []types.Volume + for result := range client.NewVolumeLister().VolumeNameSelector(sourceVolumeNames).List(ctx) { + if result.Err != nil { + return result.Err + } + if result.Volume.IsPublished() { + return fmt.Errorf("cannot move published volume %v", result.Volume.Name) + } + requiredCapacity += result.Volume.Status.TotalCapacity + volumes = append(volumes, result.Volume) + } + + if len(volumes) == 0 { + return fmt.Errorf("no volumes found in source drive %v", args.Source) + } + + destDrive, err := client.Drive().Get(ctx, string(args.Destination), metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("unable to get destination drive %v; %v", args.Destination, err) + } + if destDrive.GetNodeID() != srcDrive.GetNodeID() { + return fmt.Errorf("source and destination drives must be in same node; source node %v; desination node %v", + srcDrive.GetNodeID(), + destDrive.GetNodeID()) + } + if !destDrive.IsUnschedulable() { + return errors.New("destination drive is not cordoned") + } + if destDrive.Status.Status != directpvtypes.DriveStatusReady { + return errors.New("destination drive is not in ready state") + } + + if srcDrive.GetAccessTier() != destDrive.GetAccessTier() { + return fmt.Errorf("source drive access-tier %v and destination drive access-tier %v differ", + srcDrive.GetAccessTier(), + destDrive.GetAccessTier()) + } + + if destDrive.Status.FreeCapacity < requiredCapacity { + return fmt.Errorf("insufficient free capacity on destination drive; required=%v free=%v", + humanize.IBytes(uint64(requiredCapacity)), + humanize.IBytes(uint64(destDrive.Status.FreeCapacity))) + } + + for _, volume := range volumes { + if destDrive.AddVolumeFinalizer(volume.Name) { + destDrive.Status.FreeCapacity -= volume.Status.TotalCapacity + destDrive.Status.AllocatedCapacity += volume.Status.TotalCapacity + } + } + destDrive.Status.Status = directpvtypes.DriveStatusMoving + _, err = client.Drive().Update( + ctx, destDrive, metav1.UpdateOptions{TypeMeta: types.NewDriveTypeMeta()}, + ) + if err != nil { + return fmt.Errorf("unable to move volumes to destination drive; %v", err) + } + + for _, volume := range volumes { + log( + LogMessage{ + Type: InfoLogType, + Message: "moving volume", + Values: map[string]any{"volume": volume.Name}, + FormattedMessage: fmt.Sprintf("Moving volume %v\n", volume.Name), + }, + ) + } + + srcDrive.ResetFinalizers() + _, err = client.Drive().Update( + ctx, srcDrive, metav1.UpdateOptions{TypeMeta: types.NewDriveTypeMeta()}, + ) + if err != nil { + return fmt.Errorf("unable to remove volume references in source drive; %v", err) + } + return nil +} diff --git a/pkg/admin/refresh.go b/pkg/admin/refresh.go new file mode 100644 index 00000000..9e53c176 --- /dev/null +++ b/pkg/admin/refresh.go @@ -0,0 +1,84 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + "strings" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// RefreshNodes refreshes the nodes provided in the input +func (client *Client) RefreshNodes(ctx context.Context, selectedNodes []string) (<-chan directpvtypes.NodeID, <-chan error, error) { + if err := client.SyncNodes(ctx); err != nil { + return nil, nil, err + } + + nodes, err := client.NewNodeLister(). + NodeSelector(utils.ToLabelValues(selectedNodes)). + Get(ctx) + if err != nil { + return nil, nil, err + } + if len(selectedNodes) != 0 && len(nodes) == 0 { + suffix := "" + if len(selectedNodes) > 1 { + suffix = "s" + } + return nil, nil, fmt.Errorf("node%v %v not found", suffix, strings.Join(selectedNodes, ",")) + } + + nodeCh := make(chan directpvtypes.NodeID) + errCh := make(chan error) + + go func() { + defer close(nodeCh) + defer close(errCh) + + nodeClient := client.Node() + for i := range nodes { + updateFunc := func() error { + node, err := nodeClient.Get(ctx, nodes[i].Name, metav1.GetOptions{}) + if err != nil { + return err + } + select { + case nodeCh <- directpvtypes.NodeID(node.Name): + case <-ctx.Done(): + return ctx.Err() + } + node.Spec.Refresh = true + if _, err := nodeClient.Update(ctx, node, metav1.UpdateOptions{TypeMeta: types.NewNodeTypeMeta()}); err != nil { + return err + } + return nil + } + if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { + errCh <- err + return + } + } + }() + + return nodeCh, errCh, nil +} diff --git a/pkg/admin/remove.go b/pkg/admin/remove.go new file mode 100644 index 00000000..314cb236 --- /dev/null +++ b/pkg/admin/remove.go @@ -0,0 +1,118 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "errors" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RemoveArgs represents the arguments to remove a drive +type RemoveArgs struct { + Nodes []string + Drives []string + DriveStatus []directpvtypes.DriveStatus + DriveIDs []directpvtypes.DriveID + DryRun bool +} + +// RemoveResult represents the removed drive +type RemoveResult struct { + NodeID directpvtypes.NodeID + DriveName directpvtypes.DriveName +} + +// Remove removes the initialized drive(s) +func (client *Client) Remove(ctx context.Context, args RemoveArgs, log LogFunc) (results []RemoveResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + var failed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewDriveLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + StatusSelector(args.DriveStatus). + DriveIDSelector(args.DriveIDs). + IgnoreNotFound(true). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + + processed = true + switch result.Drive.Status.Status { + case directpvtypes.DriveStatusRemoved: + default: + volumeCount := result.Drive.GetVolumeCount() + if volumeCount > 0 { + failed = true + } else { + result.Drive.Status.Status = directpvtypes.DriveStatusRemoved + var err error + if !args.DryRun { + _, err = client.Drive().Update(ctx, &result.Drive, metav1.UpdateOptions{}) + } + if err != nil { + failed = true + log( + LogMessage{ + Type: ErrorLogType, + Err: err, + Message: "unable to remove drive", + Values: map[string]any{"node": result.Drive.GetNodeID(), "driveName": result.Drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("%v/%v: %v\n", result.Drive.GetNodeID(), result.Drive.GetDriveName(), err), + }, + ) + } else { + log( + LogMessage{ + Type: InfoLogType, + Message: "removing drive", + Values: map[string]any{"node": result.Drive.GetNodeID(), "driveName": result.Drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("Removing %v/%v\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()), + }, + ) + } + results = append(results, RemoveResult{ + NodeID: result.Drive.GetNodeID(), + DriveName: result.Drive.GetDriveName(), + }) + } + } + } + if !processed { + return nil, ErrNoMatchingResourcesFound + } + if failed { + err = errors.New("unable to remove drive(s)") + return + } + return +} diff --git a/pkg/admin/resume_drives.go b/pkg/admin/resume_drives.go new file mode 100644 index 00000000..d26acff1 --- /dev/null +++ b/pkg/admin/resume_drives.go @@ -0,0 +1,97 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// ResumeDriveArgs represents the args to be passed for resuming the drive +type ResumeDriveArgs = SuspendDriveArgs + +// ResumeDriveResult represents the resumed drive +type ResumeDriveResult = SuspendDriveResult + +// ResumeDrives will resume the suspended drives +func (client *Client) ResumeDrives(ctx context.Context, args ResumeDriveArgs, log LogFunc) (results []ResumeDriveResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewDriveLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + DriveIDSelector(args.DriveIDSelectors). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + processed = true + if !result.Drive.IsSuspended() { + // only suspended drives can be resumed. + continue + } + driveClient := client.Drive() + updateFunc := func() error { + drive, err := driveClient.Get(ctx, result.Drive.Name, metav1.GetOptions{}) + if err != nil { + return err + } + drive.Resume() + if !args.DryRun { + if _, err := driveClient.Update(ctx, drive, metav1.UpdateOptions{}); err != nil { + return err + } + } + return nil + } + if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { + err = fmt.Errorf("unable to resume drive %v; %v", result.Drive.GetDriveID(), err) + return + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "drive resumed", + Values: map[string]any{"node": result.Drive.GetNodeID(), "driveName": result.Drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("Drive %v/%v resumed\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()), + }, + ) + + results = append(results, ResumeDriveResult{ + NodeID: result.Drive.GetNodeID(), + DriveName: result.Drive.GetDriveName(), + }) + } + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/resume_volumes.go b/pkg/admin/resume_volumes.go new file mode 100644 index 00000000..c425758c --- /dev/null +++ b/pkg/admin/resume_volumes.go @@ -0,0 +1,100 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// ResumeVolumeArgs represents the args to be passed for resuming the volume +type ResumeVolumeArgs = SuspendVolumeArgs + +// ResumeVolumeResult represents the suspended volume +type ResumeVolumeResult = SuspendVolumeResult + +// ResumeVolumes will resume the suspended volumes +func (client *Client) ResumeVolumes(ctx context.Context, args ResumeVolumeArgs, log LogFunc) (results []ResumeVolumeResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewVolumeLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + PodNameSelector(utils.ToLabelValues(args.PodNames)). + PodNSSelector(utils.ToLabelValues(args.PodNamespaces)). + VolumeNameSelector(args.VolumeNames). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + processed = true + if !result.Volume.IsSuspended() { + // only suspended drives can be resumed. + continue + } + volumeClient := client.Volume() + updateFunc := func() error { + volume, err := volumeClient.Get(ctx, result.Volume.Name, metav1.GetOptions{}) + if err != nil { + return err + } + volume.Resume() + if !args.DryRun { + if _, err := volumeClient.Update(ctx, volume, metav1.UpdateOptions{}); err != nil { + return err + } + } + return nil + } + + if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { + err = fmt.Errorf("unable to resume volume %v; %v", result.Volume.Name, err) + return + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "volume resumed", + Values: map[string]any{"node": result.Volume.GetNodeID(), "volume": result.Volume.Name}, + FormattedMessage: fmt.Sprintf("Volume %v/%v resumed\n", result.Volume.GetNodeID(), result.Volume.Name), + }, + ) + + results = append(results, ResumeVolumeResult{ + NodeID: result.Volume.GetNodeID(), + VolumeName: result.Volume.Name, + }) + } + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/suspend_drives.go b/pkg/admin/suspend_drives.go new file mode 100644 index 00000000..f2a6b9f5 --- /dev/null +++ b/pkg/admin/suspend_drives.go @@ -0,0 +1,114 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "errors" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// ErrNoMatchingResourcesFound denotes that no matching resources are found for processing +var ErrNoMatchingResourcesFound = errors.New("no matching resources found") + +// SuspendDriveArgs denotes the args for suspending the drive +type SuspendDriveArgs struct { + Nodes []string + Drives []string + DriveIDSelectors []directpvtypes.DriveID + DryRun bool +} + +// SuspendDriveResult represents the suspended drive +type SuspendDriveResult struct { + NodeID directpvtypes.NodeID + DriveName directpvtypes.DriveName +} + +// SuspendDrives suspends the drive +func (client *Client) SuspendDrives(ctx context.Context, args SuspendDriveArgs, log LogFunc) (results []SuspendDriveResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewDriveLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + DriveIDSelector(args.DriveIDSelectors). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + + processed = true + + if result.Drive.IsSuspended() { + continue + } + + driveClient := client.Drive() + updateFunc := func() error { + drive, err := driveClient.Get(ctx, result.Drive.Name, metav1.GetOptions{}) + if err != nil { + return err + } + drive.Suspend() + if !args.DryRun { + if _, err := driveClient.Update(ctx, drive, metav1.UpdateOptions{}); err != nil { + return err + } + } + return nil + } + if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { + err = fmt.Errorf("unable to suspend drive %v; %v", result.Drive.GetDriveID(), err) + return + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "drive suspended", + Values: map[string]any{"node": result.Drive.GetNodeID(), "driveName": result.Drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("Drive %v/%v suspended\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()), + }, + ) + + results = append(results, SuspendDriveResult{ + NodeID: result.Drive.GetNodeID(), + DriveName: result.Drive.GetDriveName(), + }) + } + + if !processed { + return nil, ErrNoMatchingResourcesFound + } + + return results, nil +} diff --git a/pkg/admin/suspend_volumes.go b/pkg/admin/suspend_volumes.go new file mode 100644 index 00000000..13a6a113 --- /dev/null +++ b/pkg/admin/suspend_volumes.go @@ -0,0 +1,109 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +// SuspendVolumeArgs denotes the args for suspending the volume +type SuspendVolumeArgs struct { + Nodes []string + Drives []string + PodNames []string + PodNamespaces []string + VolumeNames []string + DryRun bool +} + +// SuspendVolumeResult represents the suspended volume +type SuspendVolumeResult struct { + NodeID directpvtypes.NodeID + VolumeName string +} + +// SuspendVolumes suspends the volume +func (client *Client) SuspendVolumes(ctx context.Context, args SuspendVolumeArgs, log LogFunc) (results []SuspendVolumeResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewVolumeLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + PodNameSelector(utils.ToLabelValues(args.PodNames)). + PodNSSelector(utils.ToLabelValues(args.PodNamespaces)). + VolumeNameSelector(args.VolumeNames). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + processed = true + if result.Volume.IsSuspended() { + continue + } + volumeClient := client.Volume() + updateFunc := func() error { + volume, err := volumeClient.Get(ctx, result.Volume.Name, metav1.GetOptions{}) + if err != nil { + return err + } + volume.Suspend() + if !args.DryRun { + if _, err := volumeClient.Update(ctx, volume, metav1.UpdateOptions{}); err != nil { + return err + } + } + return nil + } + if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { + err = fmt.Errorf("unable to suspend volume %v; %v", result.Volume.Name, err) + return + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "volume suspended", + Values: map[string]any{"node": result.Volume.GetNodeID(), "volume": result.Volume.Name}, + FormattedMessage: fmt.Sprintf("Volume %v/%v suspended\n", result.Volume.GetNodeID(), result.Volume.Name), + }, + ) + + results = append(results, SuspendVolumeResult{ + NodeID: result.Volume.GetNodeID(), + VolumeName: result.Volume.Name, + }) + } + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/uncordon.go b/pkg/admin/uncordon.go new file mode 100644 index 00000000..2341b7b7 --- /dev/null +++ b/pkg/admin/uncordon.go @@ -0,0 +1,90 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + "fmt" + + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// UncordonArgs represents the args for uncordoning a drive +type UncordonArgs = CordonArgs + +// UncordonResult represents the uncordoned drive +type UncordonResult = CordonResult + +// Uncordon makes the drive schedulable again +func (client *Client) Uncordon(ctx context.Context, args UncordonArgs, log LogFunc) (results []UncordonResult, err error) { + if log == nil { + log = nullLogger + } + + var processed bool + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + + resultCh := client.NewDriveLister(). + NodeSelector(utils.ToLabelValues(args.Nodes)). + DriveNameSelector(utils.ToLabelValues(args.Drives)). + StatusSelector(args.Status). + DriveIDSelector(args.DriveIDs). + List(ctx) + for result := range resultCh { + if result.Err != nil { + err = result.Err + return + } + + processed = true + + if !result.Drive.IsUnschedulable() { + continue + } + + result.Drive.Schedulable() + if !args.DryRun { + _, err = client.Drive().Update(ctx, &result.Drive, metav1.UpdateOptions{}) + } + if err != nil { + err = fmt.Errorf("unable to uncordon drive %v; %v", result.Drive.GetDriveID(), err) + return + } + + log( + LogMessage{ + Type: InfoLogType, + Message: "drive uncordoned", + Values: map[string]any{"node": result.Drive.GetNodeID(), "driveName": result.Drive.GetDriveName()}, + FormattedMessage: fmt.Sprintf("Drive %v/%v uncordoned\n", result.Drive.GetNodeID(), result.Drive.GetDriveName()), + }, + ) + + results = append(results, UncordonResult{ + NodeID: result.Drive.GetNodeID(), + DriveName: result.Drive.GetDriveName(), + }) + } + + if !processed { + return nil, ErrNoMatchingResourcesFound + } + return +} diff --git a/pkg/admin/uninstall.go b/pkg/admin/uninstall.go new file mode 100644 index 00000000..91538f97 --- /dev/null +++ b/pkg/admin/uninstall.go @@ -0,0 +1,39 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "context" + + "github.com/minio/directpv/pkg/admin/installer" + legacyclient "github.com/minio/directpv/pkg/legacy/client" +) + +// UninstallArgs represents the args to uninstall +type UninstallArgs struct { + Quiet bool + Dangerous bool +} + +// Uninstall uninstalls directpv +func (client Client) Uninstall(ctx context.Context, args UninstallArgs) error { + legacyClient, err := legacyclient.NewClient(client.K8s()) + if err != nil { + return err + } + return installer.Uninstall(ctx, args.Quiet, args.Dangerous, installer.GetDefaultTasks(client.Client, legacyClient)) +} diff --git a/pkg/client/clients.go b/pkg/client/clients.go index 4179b83e..fa24e8c7 100644 --- a/pkg/client/clients.go +++ b/pkg/client/clients.go @@ -22,36 +22,56 @@ import ( ) var ( - initialized int32 - clientsetInterface types.ExtClientsetInterface - restClient rest.Interface - driveClient types.LatestDriveInterface - volumeClient types.LatestVolumeInterface - nodeClient types.LatestNodeInterface - initRequestClient types.LatestInitRequestInterface + initialized int32 + client *Client ) +// GetClient returns the client +func GetClient() *Client { + return client +} + // RESTClient gets latest versioned REST client. func RESTClient() rest.Interface { - return restClient + return client.REST() } // DriveClient gets latest versioned drive interface. func DriveClient() types.LatestDriveInterface { - return driveClient + return client.Drive() } // VolumeClient gets latest versioned volume interface. func VolumeClient() types.LatestVolumeInterface { - return volumeClient + return client.Volume() } // NodeClient gets latest versioned node interface. func NodeClient() types.LatestNodeInterface { - return nodeClient + return client.Node() } // InitRequestClient gets latest versioned init request interface. func InitRequestClient() types.LatestInitRequestInterface { - return initRequestClient + return client.InitRequest() +} + +// NewDriveLister returns the new drive lister +func NewDriveLister() *DriveLister { + return client.NewDriveLister() +} + +// NewVolumeLister returns the new volume lister +func NewVolumeLister() *VolumeLister { + return client.NewVolumeLister() +} + +// NewNodeLister returns the new node lister +func NewNodeLister() *NodeLister { + return client.NewNodeLister() +} + +// NewInitRequestLister returns the new initrequest lister +func NewInitRequestLister() *InitRequestLister { + return client.NewInitRequestLister() } diff --git a/pkg/client/discovery.go b/pkg/client/discovery.go new file mode 100644 index 00000000..afbd3b53 --- /dev/null +++ b/pkg/client/discovery.go @@ -0,0 +1,66 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/types" + "github.com/minio/directpv/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SyncNodes compares the csinodes with directpvnode list and syncs them +// It adds the missing nodes and deletes the non-existing nodes +func (c Client) SyncNodes(ctx context.Context) (err error) { + csiNodes, err := c.K8s().GetCSINodes(ctx) + if err != nil { + return fmt.Errorf("unable to get CSI nodes; %w", err) + } + nodes, err := c.NewNodeLister().Get(ctx) + if err != nil { + return fmt.Errorf("unable to get nodes; %w", err) + } + + var nodeNames []string + for _, node := range nodes { + nodeNames = append(nodeNames, node.Name) + } + + // Add missing nodes. + for _, csiNode := range csiNodes { + if !utils.Contains(nodeNames, csiNode) { + node := types.NewNode(directpvtypes.NodeID(csiNode), []types.Device{}) + node.Spec.Refresh = true + if _, err = c.Node().Create(ctx, node, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("unable to create node %v; %w", csiNode, err) + } + } + } + + // Remove non-existing nodes. + for _, nodeName := range nodeNames { + if !utils.Contains(csiNodes, nodeName) { + if err = c.Node().Delete(ctx, nodeName, metav1.DeleteOptions{}); err != nil { + return fmt.Errorf("unable to remove non-existing node %v; %w", nodeName, err) + } + } + } + return nil +} diff --git a/pkg/drive/list.go b/pkg/client/drive_lister.go similarity index 77% rename from pkg/drive/list.go rename to pkg/client/drive_lister.go index 7130c462..20f6d448 100644 --- a/pkg/drive/list.go +++ b/pkg/client/drive_lister.go @@ -1,5 +1,5 @@ // This file is part of MinIO DirectPV -// Copyright (c) 2021, 2022 MinIO, Inc. +// Copyright (c) 2021-2024 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -14,13 +14,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package drive +package client import ( "context" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" @@ -34,8 +33,8 @@ type ListDriveResult struct { Err error } -// Lister is drive lister. -type Lister struct { +// DriveLister is lister wrapper for DirectPVDrive listing. +type DriveLister struct { nodes []directpvtypes.LabelValue driveNames []directpvtypes.LabelValue accessTiers []directpvtypes.LabelValue @@ -44,59 +43,61 @@ type Lister struct { labels map[directpvtypes.LabelKey]directpvtypes.LabelValue maxObjects int64 ignoreNotFound bool + driveClient types.LatestDriveInterface } -// NewLister creates new drive lister. -func NewLister() *Lister { - return &Lister{ - maxObjects: k8s.MaxThreadCount, +// NewDriveLister creates new drive lister. +func (client Client) NewDriveLister() *DriveLister { + return &DriveLister{ + maxObjects: k8s.MaxThreadCount, + driveClient: client.Drive(), } } // NodeSelector adds filter listing by nodes. -func (lister *Lister) NodeSelector(nodes []directpvtypes.LabelValue) *Lister { +func (lister *DriveLister) NodeSelector(nodes []directpvtypes.LabelValue) *DriveLister { lister.nodes = nodes return lister } // DriveNameSelector adds filter listing by drive names. -func (lister *Lister) DriveNameSelector(driveNames []directpvtypes.LabelValue) *Lister { +func (lister *DriveLister) DriveNameSelector(driveNames []directpvtypes.LabelValue) *DriveLister { lister.driveNames = driveNames return lister } // StatusSelector adds filter listing by drive status. -func (lister *Lister) StatusSelector(statusList []directpvtypes.DriveStatus) *Lister { +func (lister *DriveLister) StatusSelector(statusList []directpvtypes.DriveStatus) *DriveLister { lister.statusList = statusList return lister } // DriveIDSelector adds filter listing by drive IDs. -func (lister *Lister) DriveIDSelector(driveIDs []directpvtypes.DriveID) *Lister { +func (lister *DriveLister) DriveIDSelector(driveIDs []directpvtypes.DriveID) *DriveLister { lister.driveIDs = driveIDs return lister } // LabelSelector adds filter listing by labels. -func (lister *Lister) LabelSelector(labels map[directpvtypes.LabelKey]directpvtypes.LabelValue) *Lister { +func (lister *DriveLister) LabelSelector(labels map[directpvtypes.LabelKey]directpvtypes.LabelValue) *DriveLister { lister.labels = labels return lister } // MaxObjects controls number of items to be fetched in every iteration. -func (lister *Lister) MaxObjects(n int64) *Lister { +func (lister *DriveLister) MaxObjects(n int64) *DriveLister { lister.maxObjects = n return lister } // IgnoreNotFound controls listing to ignore drive not found error. -func (lister *Lister) IgnoreNotFound(b bool) *Lister { +func (lister *DriveLister) IgnoreNotFound(b bool) *DriveLister { lister.ignoreNotFound = b return lister } // List returns channel to loop through drive items. -func (lister *Lister) List(ctx context.Context) <-chan ListDriveResult { +func (lister *DriveLister) List(ctx context.Context) <-chan ListDriveResult { getOnly := len(lister.nodes) == 0 && len(lister.driveNames) == 0 && len(lister.accessTiers) == 0 && @@ -133,7 +134,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListDriveResult { LabelSelector: labelSelector, } for { - result, err := client.DriveClient().List(ctx, options) + result, err := lister.driveClient.List(ctx, options) if err != nil { if apierrors.IsNotFound(err) && lister.ignoreNotFound { break @@ -170,7 +171,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListDriveResult { } for _, driveID := range lister.driveIDs { - drive, err := client.DriveClient().Get(ctx, string(driveID), metav1.GetOptions{}) + drive, err := lister.driveClient.Get(ctx, string(driveID), metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) && lister.ignoreNotFound { continue @@ -190,7 +191,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListDriveResult { } // Get returns list of drives. -func (lister *Lister) Get(ctx context.Context) ([]types.Drive, error) { +func (lister *DriveLister) Get(ctx context.Context) ([]types.Drive, error) { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() diff --git a/pkg/drive/list_test.go b/pkg/client/drive_lister_test.go similarity index 84% rename from pkg/drive/list_test.go rename to pkg/client/drive_lister_test.go index 70e6115d..27aa4f18 100644 --- a/pkg/drive/list_test.go +++ b/pkg/client/drive_lister_test.go @@ -14,14 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package drive +package client import ( "context" "fmt" "testing" - "github.com/minio/directpv/pkg/client" clientsetfake "github.com/minio/directpv/pkg/clientset/fake" "github.com/minio/directpv/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,8 +29,8 @@ import ( func TestGetDriveList(t *testing.T) { clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) - client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - drives, err := NewLister().Get(context.TODO()) + SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) + drives, err := client.NewDriveLister().Get(context.TODO()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -47,8 +46,8 @@ func TestGetDriveList(t *testing.T) { } clientset = types.NewExtFakeClientset(clientsetfake.NewSimpleClientset(objects...)) - client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - drives, err = NewLister().Get(context.TODO()) + SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) + drives, err = client.NewDriveLister().Get(context.TODO()) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/client/dynamic.go b/pkg/client/dynamic.go index 2e994191..8a0e90a2 100644 --- a/pkg/client/dynamic.go +++ b/pkg/client/dynamic.go @@ -31,7 +31,6 @@ import ( apimachinerytypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" "k8s.io/klog/v2" ) @@ -40,12 +39,12 @@ type dynamicInterface struct { groupVersion schema.GroupVersion } -func dynamicInterfaceForConfig(config *rest.Config, kind, resource string) (*dynamicInterface, error) { - gvk, err := k8s.GetGroupVersionKind(consts.GroupName, kind, types.Versions...) +func dynamicInterfaceForConfig(k8sClient *k8s.Client, kind, resource string) (*dynamicInterface, error) { + gvk, err := k8sClient.GetGroupVersionKind(consts.GroupName, kind, types.Versions...) if err != nil && !meta.IsNoMatchError(err) { return nil, err } - resourceInterface, err := dynamic.NewForConfig(config) + resourceInterface, err := dynamic.NewForConfig(k8sClient.KubeConfig) if err != nil { return nil, err } @@ -229,8 +228,8 @@ type latestDriveClient struct { } // latestDriveClientForConfig creates new dynamic drive interface. -func latestDriveClientForConfig(config *rest.Config) (*latestDriveClient, error) { - inter, err := dynamicInterfaceForConfig(config, consts.DriveKind, consts.DriveResource) +func latestDriveClientForConfig(k8sClient *k8s.Client) (*latestDriveClient, error) { + inter, err := dynamicInterfaceForConfig(k8sClient, consts.DriveKind, consts.DriveResource) if err != nil { return nil, err } @@ -351,8 +350,8 @@ type latestVolumeClient struct { } // latestVolumeClientForConfig creates new dynamic volume interface. -func latestVolumeClientForConfig(config *rest.Config) (*latestVolumeClient, error) { - inter, err := dynamicInterfaceForConfig(config, consts.VolumeKind, consts.VolumeResource) +func latestVolumeClientForConfig(k8sClient *k8s.Client) (*latestVolumeClient, error) { + inter, err := dynamicInterfaceForConfig(k8sClient, consts.VolumeKind, consts.VolumeResource) if err != nil { return nil, err } @@ -473,8 +472,8 @@ type latestNodeClient struct { } // latestNodeClientForConfig creates new dynamic node interface. -func latestNodeClientForConfig(config *rest.Config) (*latestNodeClient, error) { - inter, err := dynamicInterfaceForConfig(config, consts.NodeKind, consts.NodeResource) +func latestNodeClientForConfig(k8sClient *k8s.Client) (*latestNodeClient, error) { + inter, err := dynamicInterfaceForConfig(k8sClient, consts.NodeKind, consts.NodeResource) if err != nil { return nil, err } @@ -597,8 +596,8 @@ type latestInitRequestClient struct { } // latestInitRequestClientForConfig creates new dynamic initrequest interface. -func latestInitRequestClientForConfig(config *rest.Config) (*latestInitRequestClient, error) { - inter, err := dynamicInterfaceForConfig(config, consts.InitRequestKind, consts.InitRequestResource) +func latestInitRequestClientForConfig(k8sClient *k8s.Client) (*latestInitRequestClient, error) { + inter, err := dynamicInterfaceForConfig(k8sClient, consts.InitRequestKind, consts.InitRequestResource) if err != nil { return nil, err } diff --git a/pkg/client/fake.go b/pkg/client/fake.go index 09c48e13..360419db 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -25,36 +25,46 @@ import ( // FakeInit initializes fake clients. func FakeInit() { k8s.FakeInit() + k8sClient := k8s.GetClient() + clientsetInterface := types.NewExtFakeClientset(fake.NewSimpleClientset()) + driveClient := clientsetInterface.DirectpvLatest().DirectPVDrives() + volumeClient := clientsetInterface.DirectpvLatest().DirectPVVolumes() + nodeClient := clientsetInterface.DirectpvLatest().DirectPVNodes() + initRequestClient := clientsetInterface.DirectpvLatest().DirectPVInitRequests() + restClient := clientsetInterface.DirectpvLatest().RESTClient() - clientsetInterface = types.NewExtFakeClientset(fake.NewSimpleClientset()) - driveClient = clientsetInterface.DirectpvLatest().DirectPVDrives() - volumeClient = clientsetInterface.DirectpvLatest().DirectPVVolumes() - nodeClient = clientsetInterface.DirectpvLatest().DirectPVNodes() - initRequestClient = clientsetInterface.DirectpvLatest().DirectPVInitRequests() - - initEvent(k8s.KubeClient()) + initEvent(k8sClient.KubeClient) + client = &Client{ + K8sClient: k8sClient, + ClientsetInterface: clientsetInterface, + RESTClient: restClient, + DriveClient: driveClient, + VolumeClient: volumeClient, + NodeClient: nodeClient, + InitRequestClient: initRequestClient, + } } // SetDriveInterface sets latest drive interface. // Note: To be used for writing test cases only func SetDriveInterface(i types.LatestDriveInterface) { - driveClient = i + client.DriveClient = i } // SetVolumeInterface sets the latest volume interface. // Note: To be used for writing test cases only func SetVolumeInterface(i types.LatestVolumeInterface) { - volumeClient = i + client.VolumeClient = i } // SetNodeInterface sets latest node interface. // Note: To be used for writing test cases only func SetNodeInterface(i types.LatestNodeInterface) { - nodeClient = i + client.NodeClient = i } // SetInitRequestInterface sets latest initrequest interface. // Note: To be used for writing test cases only func SetInitRequestInterface(i types.LatestInitRequestInterface) { - initRequestClient = i + client.InitRequestClient = i } diff --git a/pkg/client/init.go b/pkg/client/init.go index 38965350..289d8672 100644 --- a/pkg/client/init.go +++ b/pkg/client/init.go @@ -17,12 +17,22 @@ package client import ( + "fmt" "sync/atomic" "github.com/minio/directpv/pkg/clientset" "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/types" + apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/client-go/discovery" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/klog/v2" + + // support gcp, azure, and oidc client auth + _ "k8s.io/client-go/plugin/pkg/client/auth/azure" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) // Init initializes various clients. @@ -30,32 +40,123 @@ func Init() { if atomic.AddInt32(&initialized, 1) != 1 { return } - - k8s.Init() - - cs, err := clientset.NewForConfig(k8s.KubeConfig()) + var err error + if err = k8s.Init(); err != nil { + klog.Fatalf("unable to initialize k8s clients; %v", err) + } + client, err = newClient(k8s.GetClient()) if err != nil { - klog.Fatalf("unable to create new clientset interface; %v", err) + klog.Fatalf("unable to initialize client; %v", err) } - clientsetInterface = types.NewExtClientset(cs) + initEvent(k8s.KubeClient()) +} - restClient = clientsetInterface.DirectpvLatest().RESTClient() +// Client represents the directpv client set +type Client struct { + ClientsetInterface types.ExtClientsetInterface + RESTClient rest.Interface + DriveClient types.LatestDriveInterface + VolumeClient types.LatestVolumeInterface + NodeClient types.LatestNodeInterface + InitRequestClient types.LatestInitRequestInterface + K8sClient *k8s.Client +} - if driveClient, err = latestDriveClientForConfig(k8s.KubeConfig()); err != nil { - klog.Fatalf("unable to create new drive interface; %v", err) - } +// REST returns the REST client +func (c Client) REST() rest.Interface { + return c.RESTClient +} - if volumeClient, err = latestVolumeClientForConfig(k8s.KubeConfig()); err != nil { - klog.Fatalf("unable to create new volume interface; %v", err) - } +// Drive returns the DirectPV Drive interface +func (c Client) Drive() types.LatestDriveInterface { + return c.DriveClient +} - if nodeClient, err = latestNodeClientForConfig(k8s.KubeConfig()); err != nil { - klog.Fatalf("unable to create new node interface; %v", err) - } +// Volume returns the DirectPV Volume interface +func (c Client) Volume() types.LatestVolumeInterface { + return c.VolumeClient +} + +// Node returns the DirectPV Node interface +func (c Client) Node() types.LatestNodeInterface { + return c.NodeClient +} + +// InitRequest returns the DirectPV InitRequest interface +func (c Client) InitRequest() types.LatestInitRequestInterface { + return c.InitRequestClient +} + +// K8s returns the kubernetes client +func (c Client) K8s() *k8s.Client { + return c.K8sClient +} + +// KubeConfig returns the kubeconfig +func (c Client) KubeConfig() *rest.Config { + return c.K8sClient.KubeConfig +} - if initRequestClient, err = latestInitRequestClientForConfig(k8s.KubeConfig()); err != nil { - klog.Fatalf("unable to create new initrequest interface; %v", err) +// Kube returns the kube client +func (c Client) Kube() kubernetes.Interface { + return c.K8sClient.KubeClient +} + +// APIextensions returns the APIextensionsClient +func (c Client) APIextensions() apiextensions.ApiextensionsV1Interface { + return c.K8sClient.APIextensionsClient +} + +// CRD returns the CRD client +func (c Client) CRD() apiextensions.CustomResourceDefinitionInterface { + return c.K8sClient.CRDClient +} + +// Discovery returns the discovery client +func (c Client) Discovery() discovery.DiscoveryInterface { + return c.K8sClient.DiscoveryClient +} + +// NewClient returns the directpv client +func NewClient(c *rest.Config) (*Client, error) { + k8sClient, err := k8s.NewClient(c) + if err != nil { + return nil, fmt.Errorf("unable to create kubernetes client; %v", err) } + return newClient(k8sClient) +} - initEvent(k8s.KubeClient()) +// newClient returns the directpv client +func newClient(k8sClient *k8s.Client) (*Client, error) { + cs, err := clientset.NewForConfig(k8sClient.KubeConfig) + if err != nil { + return nil, fmt.Errorf("unable to create new clientset interface; %v", err) + } + clientsetInterface := types.NewExtClientset(cs) + restClient := clientsetInterface.DirectpvLatest().RESTClient() + driveClient, err := latestDriveClientForConfig(k8sClient) + if err != nil { + return nil, fmt.Errorf("unable to create new drive interface; %v", err) + } + volumeClient, err := latestVolumeClientForConfig(k8sClient) + if err != nil { + return nil, fmt.Errorf("unable to create new volume interface; %v", err) + } + nodeClient, err := latestNodeClientForConfig(k8sClient) + if err != nil { + return nil, fmt.Errorf("unable to create new node interface; %v", err) + } + initRequestClient, err := latestInitRequestClientForConfig(k8sClient) + if err != nil { + return nil, fmt.Errorf("unable to create new initrequest interface; %v", err) + } + return &Client{ + ClientsetInterface: clientsetInterface, + RESTClient: restClient, + DriveClient: driveClient, + VolumeClient: volumeClient, + NodeClient: nodeClient, + InitRequestClient: initRequestClient, + K8sClient: k8sClient, + }, nil } diff --git a/pkg/initrequest/list.go b/pkg/client/initrequest_lister.go similarity index 73% rename from pkg/initrequest/list.go rename to pkg/client/initrequest_lister.go index 6ca663b5..213529aa 100644 --- a/pkg/initrequest/list.go +++ b/pkg/client/initrequest_lister.go @@ -14,21 +14,19 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package initrequest +package client import ( "context" "fmt" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" ) // ListInitRequestResult denotes list of initrequest result. @@ -37,54 +35,56 @@ type ListInitRequestResult struct { Err error } -// Lister is initRequest lister. -type Lister struct { - nodes []directpvtypes.LabelValue - requestIDs []directpvtypes.LabelValue - initRequestNames []string - maxObjects int64 - ignoreNotFound bool +// InitRequestLister is initRequest lister. +type InitRequestLister struct { + nodes []directpvtypes.LabelValue + requestIDs []directpvtypes.LabelValue + initRequestNames []string + maxObjects int64 + ignoreNotFound bool + initRequestClient types.LatestInitRequestInterface } -// NewLister creates new volume lister. -func NewLister() *Lister { - return &Lister{ - maxObjects: k8s.MaxThreadCount, +// NewInitRequestLister creates new volume lister. +func (client Client) NewInitRequestLister() *InitRequestLister { + return &InitRequestLister{ + maxObjects: k8s.MaxThreadCount, + initRequestClient: client.InitRequest(), } } // NodeSelector adds filter listing by nodes. -func (lister *Lister) NodeSelector(nodes []directpvtypes.LabelValue) *Lister { +func (lister *InitRequestLister) NodeSelector(nodes []directpvtypes.LabelValue) *InitRequestLister { lister.nodes = nodes return lister } // RequestIDSelector adds filter listing by its request IDs. -func (lister *Lister) RequestIDSelector(requestIDs []directpvtypes.LabelValue) *Lister { +func (lister *InitRequestLister) RequestIDSelector(requestIDs []directpvtypes.LabelValue) *InitRequestLister { lister.requestIDs = requestIDs return lister } // InitRequestNameSelector adds filter listing by InitRequestNames. -func (lister *Lister) InitRequestNameSelector(initRequestNames []string) *Lister { +func (lister *InitRequestLister) InitRequestNameSelector(initRequestNames []string) *InitRequestLister { lister.initRequestNames = initRequestNames return lister } // MaxObjects controls number of items to be fetched in every iteration. -func (lister *Lister) MaxObjects(n int64) *Lister { +func (lister *InitRequestLister) MaxObjects(n int64) *InitRequestLister { lister.maxObjects = n return lister } // IgnoreNotFound controls listing to ignore not found error. -func (lister *Lister) IgnoreNotFound(b bool) *Lister { +func (lister *InitRequestLister) IgnoreNotFound(b bool) *InitRequestLister { lister.ignoreNotFound = b return lister } // List returns channel to loop through initrequest items. -func (lister *Lister) List(ctx context.Context) <-chan ListInitRequestResult { +func (lister *InitRequestLister) List(ctx context.Context) <-chan ListInitRequestResult { getOnly := len(lister.nodes) == 0 && len(lister.requestIDs) == 0 && len(lister.initRequestNames) != 0 @@ -114,7 +114,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListInitRequestResult { LabelSelector: labelSelector, } for { - result, err := client.InitRequestClient().List(ctx, options) + result, err := lister.initRequestClient.List(ctx, options) if err != nil { send(ListInitRequestResult{Err: err}) return @@ -148,7 +148,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListInitRequestResult { } for _, initRequestName := range lister.initRequestNames { - initRequest, err := client.InitRequestClient().Get(ctx, initRequestName, metav1.GetOptions{}) + initRequest, err := lister.initRequestClient.Get(ctx, initRequestName, metav1.GetOptions{}) if err != nil { send(ListInitRequestResult{Err: err}) return @@ -163,7 +163,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListInitRequestResult { } // Get returns list of initrequest. -func (lister *Lister) Get(ctx context.Context) ([]types.InitRequest, error) { +func (lister *InitRequestLister) Get(ctx context.Context) ([]types.InitRequest, error) { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() @@ -178,20 +178,13 @@ func (lister *Lister) Get(ctx context.Context) ([]types.InitRequest, error) { return initRequestList, nil } -// WatchEvent represents the initrequest events. -type WatchEvent struct { - Type watch.EventType - InitRequest *types.InitRequest - Err error -} - // Watch looks for changes in InitRequestList and reports them. -func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), error) { +func (lister *InitRequestLister) Watch(ctx context.Context) (<-chan WatchEvent[*types.InitRequest], func(), error) { labelMap := map[directpvtypes.LabelKey][]directpvtypes.LabelValue{ directpvtypes.NodeLabelKey: lister.nodes, directpvtypes.RequestIDLabelKey: lister.requestIDs, } - initRequestWatchInterface, err := client.InitRequestClient().Watch(ctx, metav1.ListOptions{ + initRequestWatchInterface, err := lister.initRequestClient.Watch(ctx, metav1.ListOptions{ LabelSelector: directpvtypes.ToLabelSelector(labelMap), }) if err != nil { @@ -199,7 +192,7 @@ func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), err } stopFn := initRequestWatchInterface.Stop - watchCh := make(chan WatchEvent) + watchCh := make(chan WatchEvent[*types.InitRequest]) go func() { defer close(watchCh) resultCh := initRequestWatchInterface.ResultChan() @@ -212,7 +205,7 @@ func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), err var initRequest types.InitRequest err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured.Object, &initRequest) if err != nil { - watchCh <- WatchEvent{ + watchCh <- WatchEvent[*types.InitRequest]{ Type: result.Type, Err: fmt.Errorf("unable to convert unstructured object %s; %v", unstructured.GetName(), err), } @@ -221,9 +214,9 @@ func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), err if len(lister.initRequestNames) > 0 && !utils.Contains(lister.initRequestNames, initRequest.Name) { continue } - watchCh <- WatchEvent{ - Type: result.Type, - InitRequest: &initRequest, + watchCh <- WatchEvent[*types.InitRequest]{ + Type: result.Type, + Item: &initRequest, } } }() diff --git a/pkg/node/list.go b/pkg/client/node_lister.go similarity index 78% rename from pkg/node/list.go rename to pkg/client/node_lister.go index 1af5d920..9ce5edac 100644 --- a/pkg/node/list.go +++ b/pkg/client/node_lister.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package node +package client import ( "context" @@ -22,7 +22,6 @@ import ( "fmt" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,47 +36,49 @@ type ListNodeResult struct { Err error } -// Lister is node lister. -type Lister struct { +// NodeLister is node lister. +type NodeLister struct { nodes []directpvtypes.LabelValue nodeNames []string maxObjects int64 ignoreNotFound bool + nodeClient types.LatestNodeInterface } -// NewLister creates new volume lister. -func NewLister() *Lister { - return &Lister{ +// NewNodeLister creates new volume lister. +func (client Client) NewNodeLister() *NodeLister { + return &NodeLister{ maxObjects: k8s.MaxThreadCount, + nodeClient: client.Node(), } } // NodeSelector adds filter listing by nodes. -func (lister *Lister) NodeSelector(nodes []directpvtypes.LabelValue) *Lister { +func (lister *NodeLister) NodeSelector(nodes []directpvtypes.LabelValue) *NodeLister { lister.nodes = nodes return lister } // NodeNameSelector adds filter listing by node names. -func (lister *Lister) NodeNameSelector(nodeNames []string) *Lister { +func (lister *NodeLister) NodeNameSelector(nodeNames []string) *NodeLister { lister.nodeNames = nodeNames return lister } // MaxObjects controls number of items to be fetched in every iteration. -func (lister *Lister) MaxObjects(n int64) *Lister { +func (lister *NodeLister) MaxObjects(n int64) *NodeLister { lister.maxObjects = n return lister } // IgnoreNotFound controls listing to ignore node not found error. -func (lister *Lister) IgnoreNotFound(b bool) *Lister { +func (lister *NodeLister) IgnoreNotFound(b bool) *NodeLister { lister.ignoreNotFound = b return lister } // List returns channel to loop through node items. -func (lister *Lister) List(ctx context.Context) <-chan ListNodeResult { +func (lister *NodeLister) List(ctx context.Context) <-chan ListNodeResult { getOnly := len(lister.nodes) == 0 && len(lister.nodeNames) != 0 @@ -105,7 +106,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListNodeResult { LabelSelector: labelSelector, } for { - result, err := client.NodeClient().List(ctx, options) + result, err := lister.nodeClient.List(ctx, options) if err != nil { send(ListNodeResult{Err: err}) return @@ -139,7 +140,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListNodeResult { } for _, nodeName := range lister.nodeNames { - node, err := client.NodeClient().Get(ctx, nodeName, metav1.GetOptions{}) + node, err := lister.nodeClient.Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { send(ListNodeResult{Err: err}) return @@ -154,7 +155,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListNodeResult { } // Get returns list of nodes. -func (lister *Lister) Get(ctx context.Context) ([]types.Node, error) { +func (lister *NodeLister) Get(ctx context.Context) ([]types.Node, error) { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() @@ -169,22 +170,22 @@ func (lister *Lister) Get(ctx context.Context) ([]types.Node, error) { return nodeList, nil } -// WatchEvent represents the node events. -type WatchEvent struct { +// WatchEvent represents the events used in the Lister. +type WatchEvent[I any] struct { Type watch.EventType - Node *types.Node + Item I Err error } // Watch looks for changes in NodeList and reports them. -func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), error) { +func (lister *NodeLister) Watch(ctx context.Context) (<-chan WatchEvent[*types.Node], func(), error) { if len(lister.nodeNames) > 0 { return nil, nil, errors.New("unsupported selector") } labelMap := map[directpvtypes.LabelKey][]directpvtypes.LabelValue{ directpvtypes.NodeLabelKey: lister.nodes, } - nodeWatchInterface, err := client.NodeClient().Watch(ctx, metav1.ListOptions{ + nodeWatchInterface, err := lister.nodeClient.Watch(ctx, metav1.ListOptions{ LabelSelector: directpvtypes.ToLabelSelector(labelMap), }) if err != nil { @@ -192,7 +193,7 @@ func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), err } stopFn := nodeWatchInterface.Stop - watchCh := make(chan WatchEvent) + watchCh := make(chan WatchEvent[*types.Node]) go func() { defer close(watchCh) resultCh := nodeWatchInterface.ResultChan() @@ -205,15 +206,15 @@ func (lister *Lister) Watch(ctx context.Context) (<-chan WatchEvent, func(), err var node types.Node err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured.Object, &node) if err != nil { - watchCh <- WatchEvent{ + watchCh <- WatchEvent[*types.Node]{ Type: result.Type, Err: fmt.Errorf("unable to convert unstructured object %s; %v", unstructured.GetName(), err), } continue } - watchCh <- WatchEvent{ + watchCh <- WatchEvent[*types.Node]{ Type: result.Type, - Node: &node, + Item: &node, } } }() diff --git a/pkg/volume/list.go b/pkg/client/volume_lister.go similarity index 76% rename from pkg/volume/list.go rename to pkg/client/volume_lister.go index 2ac5e518..cf9aadf9 100644 --- a/pkg/volume/list.go +++ b/pkg/client/volume_lister.go @@ -1,5 +1,5 @@ // This file is part of MinIO DirectPV -// Copyright (c) 2021, 2022 MinIO, Inc. +// Copyright (c) 2023 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -14,13 +14,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package volume +package client import ( "context" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" - "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/k8s" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" @@ -34,8 +33,8 @@ type ListVolumeResult struct { Err error } -// Lister is volume lister. -type Lister struct { +// VolumeLister is volume lister. +type VolumeLister struct { nodes []directpvtypes.LabelValue driveNames []directpvtypes.LabelValue driveIDs []directpvtypes.LabelValue @@ -46,77 +45,79 @@ type Lister struct { labels map[directpvtypes.LabelKey]directpvtypes.LabelValue maxObjects int64 ignoreNotFound bool + volumeClient types.LatestVolumeInterface } -// NewLister creates new volume lister. -func NewLister() *Lister { - return &Lister{ - maxObjects: k8s.MaxThreadCount, +// NewVolumeLister creates new volume lister. +func (client Client) NewVolumeLister() *VolumeLister { + return &VolumeLister{ + maxObjects: k8s.MaxThreadCount, + volumeClient: client.Volume(), } } // NodeSelector adds filter listing by nodes. -func (lister *Lister) NodeSelector(nodes []directpvtypes.LabelValue) *Lister { +func (lister *VolumeLister) NodeSelector(nodes []directpvtypes.LabelValue) *VolumeLister { lister.nodes = nodes return lister } // DriveNameSelector adds filter listing by drive names. -func (lister *Lister) DriveNameSelector(driveNames []directpvtypes.LabelValue) *Lister { +func (lister *VolumeLister) DriveNameSelector(driveNames []directpvtypes.LabelValue) *VolumeLister { lister.driveNames = driveNames return lister } // DriveIDSelector adds filter listing by drive IDs. -func (lister *Lister) DriveIDSelector(driveIDs []directpvtypes.LabelValue) *Lister { +func (lister *VolumeLister) DriveIDSelector(driveIDs []directpvtypes.LabelValue) *VolumeLister { lister.driveIDs = driveIDs return lister } // PodNameSelector adds filter listing by pod names. -func (lister *Lister) PodNameSelector(podNames []directpvtypes.LabelValue) *Lister { +func (lister *VolumeLister) PodNameSelector(podNames []directpvtypes.LabelValue) *VolumeLister { lister.podNames = podNames return lister } // PodNSSelector adds filter listing by pod namespaces. -func (lister *Lister) PodNSSelector(podNSs []directpvtypes.LabelValue) *Lister { +func (lister *VolumeLister) PodNSSelector(podNSs []directpvtypes.LabelValue) *VolumeLister { lister.podNSs = podNSs return lister } // StatusSelector adds filter listing by volume status. -func (lister *Lister) StatusSelector(statusList []directpvtypes.VolumeStatus) *Lister { +func (lister *VolumeLister) StatusSelector(statusList []directpvtypes.VolumeStatus) *VolumeLister { lister.statusList = statusList return lister } // VolumeNameSelector adds filter listing by volume names. -func (lister *Lister) VolumeNameSelector(volumeNames []string) *Lister { +func (lister *VolumeLister) VolumeNameSelector(volumeNames []string) *VolumeLister { lister.volumeNames = volumeNames return lister } // LabelSelector adds filter listing by labels. -func (lister *Lister) LabelSelector(labels map[directpvtypes.LabelKey]directpvtypes.LabelValue) *Lister { +func (lister *VolumeLister) LabelSelector(labels map[directpvtypes.LabelKey]directpvtypes.LabelValue) *VolumeLister { lister.labels = labels return lister } // MaxObjects controls number of items to be fetched in every iteration. -func (lister *Lister) MaxObjects(n int64) *Lister { +func (lister *VolumeLister) MaxObjects(n int64) *VolumeLister { lister.maxObjects = n return lister } // IgnoreNotFound controls listing to ignore drive not found error. -func (lister *Lister) IgnoreNotFound(b bool) *Lister { +func (lister *VolumeLister) IgnoreNotFound(b bool) *VolumeLister { lister.ignoreNotFound = b return lister } // List returns channel to loop through volume items. -func (lister *Lister) List(ctx context.Context) <-chan ListVolumeResult { +func (lister *VolumeLister) List(ctx context.Context) <-chan ListVolumeResult { getOnly := len(lister.nodes) == 0 && len(lister.driveNames) == 0 && len(lister.driveIDs) == 0 && @@ -158,7 +159,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListVolumeResult { } for { - result, err := client.VolumeClient().List(ctx, options) + result, err := lister.volumeClient.List(ctx, options) if err != nil { if apierrors.IsNotFound(err) && lister.ignoreNotFound { break @@ -196,7 +197,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListVolumeResult { } for _, volumeName := range lister.volumeNames { - volume, err := client.VolumeClient().Get(ctx, volumeName, metav1.GetOptions{}) + volume, err := lister.volumeClient.Get(ctx, volumeName, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) && lister.ignoreNotFound { continue @@ -215,7 +216,7 @@ func (lister *Lister) List(ctx context.Context) <-chan ListVolumeResult { } // Get returns list of volumes. -func (lister *Lister) Get(ctx context.Context) ([]types.Volume, error) { +func (lister *VolumeLister) Get(ctx context.Context) ([]types.Volume, error) { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() diff --git a/pkg/volume/list_test.go b/pkg/client/volume_lister_test.go similarity index 78% rename from pkg/volume/list_test.go rename to pkg/client/volume_lister_test.go index d5e43c62..529df48c 100644 --- a/pkg/volume/list_test.go +++ b/pkg/client/volume_lister_test.go @@ -14,14 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package volume +package client import ( "context" "fmt" "testing" - "github.com/minio/directpv/pkg/client" clientsetfake "github.com/minio/directpv/pkg/clientset/fake" "github.com/minio/directpv/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,10 +29,10 @@ import ( func TestGetVolumeList(t *testing.T) { clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) - client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - client.SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) + SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) + SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) - volumes, err := NewLister().Get(context.TODO()) + volumes, err := client.NewVolumeLister().Get(context.TODO()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -49,10 +48,10 @@ func TestGetVolumeList(t *testing.T) { } clientset = types.NewExtFakeClientset(clientsetfake.NewSimpleClientset(objects...)) - client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - client.SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) + SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) + SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) - volumes, err = NewLister().Get(context.TODO()) + volumes, err = client.NewVolumeLister().Get(context.TODO()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -63,10 +62,10 @@ func TestGetVolumeList(t *testing.T) { func TestGetSortedVolumeList(t *testing.T) { clientset := types.NewExtFakeClientset(clientsetfake.NewSimpleClientset()) - client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - client.SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) + SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) + SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) - volumes, err := NewLister().Get(context.TODO()) + volumes, err := client.NewVolumeLister().Get(context.TODO()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -92,10 +91,10 @@ func TestGetSortedVolumeList(t *testing.T) { } clientset = types.NewExtFakeClientset(clientsetfake.NewSimpleClientset(objects...)) - client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - client.SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) + SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) + SetVolumeInterface(clientset.DirectpvLatest().DirectPVVolumes()) - volumes, err = NewLister().Get(context.TODO()) + volumes, err = client.NewVolumeLister().Get(context.TODO()) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/csi/controller/server_test.go b/pkg/csi/controller/server_test.go index 72ea13f6..eba3ef8d 100644 --- a/pkg/csi/controller/server_test.go +++ b/pkg/csi/controller/server_test.go @@ -27,7 +27,6 @@ import ( "github.com/minio/directpv/pkg/client" clientsetfake "github.com/minio/directpv/pkg/clientset/fake" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" "github.com/minio/directpv/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -177,7 +176,7 @@ func TestCreateAndDeleteVolumeRPCs(t *testing.T) { // Fetch the drive objects client.SetDriveInterface(clientset.DirectpvLatest().DirectPVDrives()) - driveList, err := drive.NewLister().Get(ctx) + driveList, err := client.NewDriveLister().Get(ctx) if err != nil { t.Errorf("Listing drives failed: %v", err) } diff --git a/pkg/csi/controller/utils.go b/pkg/csi/controller/utils.go index cd370f7c..059380ca 100644 --- a/pkg/csi/controller/utils.go +++ b/pkg/csi/controller/utils.go @@ -25,8 +25,8 @@ import ( "github.com/container-storage-interface/spec/lib/go/csi" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" + "github.com/minio/directpv/pkg/client" "github.com/minio/directpv/pkg/consts" - "github.com/minio/directpv/pkg/drive" "github.com/minio/directpv/pkg/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -107,7 +107,7 @@ func getFilteredDrives(ctx context.Context, req *csi.CreateVolumeRequest) (drive ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() - for result := range drive.NewLister().List(ctx) { + for result := range client.NewDriveLister().List(ctx) { if result.Err != nil { return nil, result.Err } diff --git a/pkg/csi/node/server_test.go b/pkg/csi/node/server_test.go index 51db5b2f..0e68f4ac 100644 --- a/pkg/csi/node/server_test.go +++ b/pkg/csi/node/server_test.go @@ -27,6 +27,10 @@ import ( "github.com/minio/directpv/pkg/xfs" ) +func init() { + client.FakeInit() +} + func TestNodeExpandVolume(t *testing.T) { volumeID := "volume-id-1" volume := types.NewVolume(volumeID, "fsuuid1", "node-1", "drive-1", "sda", 100*MiB) diff --git a/pkg/device/sync.go b/pkg/device/sync.go index 4a14c6ef..c4915f2f 100644 --- a/pkg/device/sync.go +++ b/pkg/device/sync.go @@ -24,7 +24,6 @@ import ( directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" "github.com/minio/directpv/pkg/client" - "github.com/minio/directpv/pkg/drive" "github.com/minio/directpv/pkg/types" "github.com/minio/directpv/pkg/utils" "github.com/minio/directpv/pkg/xfs" @@ -220,7 +219,7 @@ func Sync(ctx context.Context, nodeID directpvtypes.NodeID) error { if err != nil { return err } - drives, err := drive.NewLister().NodeSelector([]directpvtypes.LabelValue{directpvtypes.ToLabelValue(string(nodeID))}).Get(ctx) + drives, err := client.NewDriveLister().NodeSelector([]directpvtypes.LabelValue{directpvtypes.ToLabelValue(string(nodeID))}).Get(ctx) if err != nil { return err } diff --git a/pkg/k8s/clients.go b/pkg/k8s/clients.go index 372d5e09..0ade6408 100644 --- a/pkg/k8s/clients.go +++ b/pkg/k8s/clients.go @@ -27,30 +27,31 @@ import ( const MaxThreadCount = 200 var ( - initialized int32 - kubeConfig *rest.Config - kubeClient kubernetes.Interface - apiextensionsClient apiextensions.ApiextensionsV1Interface - crdClient apiextensions.CustomResourceDefinitionInterface - discoveryClient discovery.DiscoveryInterface + initialized int32 + client *Client ) +// GetClient returns kubernetes client. +func GetClient() *Client { + return client +} + // KubeConfig gets kubernetes client configuration. func KubeConfig() *rest.Config { - return kubeConfig + return client.KubeConfig } // KubeClient gets kubernetes client. func KubeClient() kubernetes.Interface { - return kubeClient + return client.KubeClient } // CRDClient gets kubernetes CRD client. func CRDClient() apiextensions.CustomResourceDefinitionInterface { - return crdClient + return client.CRDClient } // DiscoveryClient gets kubernetes discovery client. func DiscoveryClient() discovery.DiscoveryInterface { - return discoveryClient + return client.DiscoveryClient } diff --git a/pkg/k8s/fake.go b/pkg/k8s/fake.go index cbbf1e0a..4917c0d9 100644 --- a/pkg/k8s/fake.go +++ b/pkg/k8s/fake.go @@ -47,28 +47,34 @@ func (fd *FakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*meta // FakeInit initializes fake clients. func FakeInit() { + var kubeClient kubernetes.Interface kubeClient = kubernetesfake.NewSimpleClientset() - crdClient = &apiextensionsv1fake.FakeCustomResourceDefinitions{ + crdClient := &apiextensionsv1fake.FakeCustomResourceDefinitions{ Fake: &apiextensionsv1fake.FakeApiextensionsV1{ Fake: &kubeClient.(*kubernetesfake.Clientset).Fake, }, } - discoveryClient = &discoveryfake.FakeDiscovery{} + discoveryClient := &discoveryfake.FakeDiscovery{} scheme := runtime.NewScheme() _ = metav1.AddMetaToScheme(scheme) + client = &Client{ + KubeClient: kubeClient, + CRDClient: crdClient, + DiscoveryClient: discoveryClient, + } } // SetKubeInterface sets the given kube interface // Note: To be used for writing test cases only func SetKubeInterface(i kubernetes.Interface) { - kubeClient = i + client.KubeClient = i } -// SetDiscoveryInterface sets the fake discovery interface +// NewFakeDiscovery creates a fake discovery interface // Note: To be used for writing test cases only -func SetDiscoveryInterface(groupsAndMethodsFn fakeServerGroupsAndResourcesMethod, serverVersionInfo *version.Info) { - discoveryClient = &FakeDiscovery{ - FakeDiscovery: discoveryfake.FakeDiscovery{Fake: &kubeClient.(*kubernetesfake.Clientset).Fake}, +func NewFakeDiscovery(groupsAndMethodsFn fakeServerGroupsAndResourcesMethod, serverVersionInfo *version.Info) *FakeDiscovery { + return &FakeDiscovery{ + FakeDiscovery: discoveryfake.FakeDiscovery{Fake: &client.KubeClient.(*kubernetesfake.Clientset).Fake}, fakeServerGroupsAndResourcesMethod: groupsAndMethodsFn, versionInfo: serverVersionInfo, } diff --git a/pkg/k8s/init.go b/pkg/k8s/init.go index 17a7983c..f2ed2e8f 100644 --- a/pkg/k8s/init.go +++ b/pkg/k8s/init.go @@ -17,6 +17,9 @@ package k8s import ( + "fmt" + "strconv" + "strings" "sync/atomic" apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" @@ -32,28 +35,80 @@ import ( ) // Init initializes various client interfaces. -func Init() { +func Init() error { if atomic.AddInt32(&initialized, 1) != 1 { - return + return nil } - - var err error - - if kubeConfig, err = GetKubeConfig(); err != nil { + kubeConfig, err := GetKubeConfig() + if err != nil { klog.Fatalf("unable to get kubernetes configuration; %v", err) } - kubeConfig.WarningHandler = rest.NoWarnings{} - if kubeClient, err = kubernetes.NewForConfig(kubeConfig); err != nil { + client, err = NewClient(kubeConfig) + if err != nil { klog.Fatalf("unable to create new kubernetes client interface; %v", err) } + return nil +} + +// Client represents the kubernetes client set. +type Client struct { + KubeConfig *rest.Config + KubeClient kubernetes.Interface + APIextensionsClient apiextensions.ApiextensionsV1Interface + CRDClient apiextensions.CustomResourceDefinitionInterface + DiscoveryClient discovery.DiscoveryInterface +} + +// GetKubeVersion returns the k8s version info +func (client Client) GetKubeVersion() (major, minor uint, err error) { + versionInfo, err := client.DiscoveryClient.ServerVersion() + if err != nil { + return 0, 0, err + } - if apiextensionsClient, err = apiextensions.NewForConfig(kubeConfig); err != nil { - klog.Fatalf("unable to create new API extensions client interface; %v", err) + var u64 uint64 + if u64, err = strconv.ParseUint(versionInfo.Major, 10, 64); err != nil { + return 0, 0, fmt.Errorf("unable to parse major version %v; %v", versionInfo.Major, err) } - crdClient = apiextensionsClient.CustomResourceDefinitions() + major = uint(u64) - if discoveryClient, err = discovery.NewDiscoveryClientForConfig(kubeConfig); err != nil { - klog.Fatalf("unable to create new discovery client interface; %v", err) + minorString := versionInfo.Minor + if strings.Contains(versionInfo.GitVersion, "-eks-") { + // Do trimming only for EKS. + // Refer https://github.com/aws/containers-roadmap/issues/1404 + i := strings.IndexFunc(minorString, func(r rune) bool { return r < '0' || r > '9' }) + if i > -1 { + minorString = minorString[:i] + } + } + if u64, err = strconv.ParseUint(minorString, 10, 64); err != nil { + return 0, 0, fmt.Errorf("unable to parse minor version %v; %v", minor, err) + } + minor = uint(u64) + return major, minor, nil +} + +// NewClient initializes the client with the provided kube config. +func NewClient(kubeConfig *rest.Config) (*Client, error) { + kubeClient, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, fmt.Errorf("unable to create new kubernetes client interface; %v", err) + } + apiextensionsClient, err := apiextensions.NewForConfig(kubeConfig) + if err != nil { + return nil, fmt.Errorf("unable to create new API extensions client interface; %v", err) + } + crdClient := apiextensionsClient.CustomResourceDefinitions() + discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeConfig) + if err != nil { + return nil, fmt.Errorf("unable to create new discovery client interface; %v", err) } + return &Client{ + KubeConfig: kubeConfig, + KubeClient: kubeClient, + APIextensionsClient: apiextensionsClient, + CRDClient: crdClient, + DiscoveryClient: discoveryClient, + }, nil } diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index b33d360b..d93fc874 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -17,12 +17,19 @@ package k8s import ( + "context" + "fmt" "os" "path" "strings" + "time" + "github.com/minio/directpv/pkg/consts" "github.com/minio/directpv/pkg/utils" "github.com/spf13/viper" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + storagev1beta1 "k8s.io/api/storage/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/scheme" @@ -57,8 +64,8 @@ func GetKubeConfig() (*rest.Config, error) { } // GetGroupVersionKind gets group/version/kind of given versions. -func GetGroupVersionKind(group, kind string, versions ...string) (*schema.GroupVersionKind, error) { - apiGroupResources, err := restmapper.GetAPIGroupResources(discoveryClient) +func (client Client) GetGroupVersionKind(group, kind string, versions ...string) (*schema.GroupVersionKind, error) { + apiGroupResources, err := restmapper.GetAPIGroupResources(client.DiscoveryClient) if err != nil { klog.ErrorS(err, "unable to get API group resources") return nil, err @@ -83,13 +90,13 @@ func GetGroupVersionKind(group, kind string, versions ...string) (*schema.GroupV } // GetClientForNonCoreGroupVersionKind gets client for group/kind of given versions. -func GetClientForNonCoreGroupVersionKind(group, kind string, versions ...string) (rest.Interface, *schema.GroupVersionKind, error) { - gvk, err := GetGroupVersionKind(group, kind, versions...) +func (client Client) GetClientForNonCoreGroupVersionKind(group, kind string, versions ...string) (rest.Interface, *schema.GroupVersionKind, error) { + gvk, err := client.GetGroupVersionKind(group, kind, versions...) if err != nil { return nil, nil, err } - config := *kubeConfig + config := *client.KubeConfig config.GroupVersion = &schema.GroupVersion{ Group: gvk.Group, Version: gvk.Version, @@ -100,12 +107,12 @@ func GetClientForNonCoreGroupVersionKind(group, kind string, versions ...string) config.UserAgent = rest.DefaultKubernetesUserAgent() } - client, err := rest.RESTClientFor(&config) + restClient, err := rest.RESTClientFor(&config) if err != nil { return nil, nil, err } - return client, gvk, nil + return restClient, gvk, nil } // IsCondition checks whether type/status/reason/message in conditions or not. @@ -190,3 +197,128 @@ func SanitizeResourceName(name string) string { return string(result) } + +// GetCSINodes fetches the CSI Node list +func (client Client) GetCSINodes(ctx context.Context) (nodes []string, err error) { + storageClient, gvk, err := client.GetClientForNonCoreGroupVersionKind("storage.k8s.io", "CSINode", "v1", "v1beta1", "v1alpha1") + if err != nil { + return nil, err + } + + switch gvk.Version { + case "v1apha1": + err = fmt.Errorf("unsupported CSINode storage.k8s.io/v1alpha1") + case "v1": + result := &storagev1.CSINodeList{} + if err = storageClient.Get(). + Resource("csinodes"). + VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec). + Timeout(10 * time.Second). + Do(ctx). + Into(result); err != nil { + err = fmt.Errorf("unable to get csinodes; %w", err) + break + } + for _, csiNode := range result.Items { + for _, driver := range csiNode.Spec.Drivers { + if driver.Name == consts.Identity { + nodes = append(nodes, csiNode.Name) + break + } + } + } + case "v1beta1": + result := &storagev1beta1.CSINodeList{} + if err = storageClient.Get(). + Resource(gvk.Kind). + VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec). + Timeout(10 * time.Second). + Do(ctx). + Into(result); err != nil { + err = fmt.Errorf("unable to get csinodes; %w", err) + break + } + for _, csiNode := range result.Items { + for _, driver := range csiNode.Spec.Drivers { + if driver.Name == consts.Identity { + nodes = append(nodes, csiNode.Name) + break + } + } + } + } + + return nodes, err +} + +// ParseNodeSelector parses the provided node selector values +func ParseNodeSelector(values []string) (map[string]string, error) { + nodeSelector := map[string]string{} + for _, value := range values { + tokens := strings.Split(value, "=") + if len(tokens) != 2 { + return nil, fmt.Errorf("invalid node selector value %v", value) + } + if tokens[0] == "" { + return nil, fmt.Errorf("invalid key in node selector value %v", value) + } + nodeSelector[tokens[0]] = tokens[1] + } + return nodeSelector, nil +} + +// ParseTolerations parses the provided toleration values +func ParseTolerations(values []string) ([]corev1.Toleration, error) { + var tolerations []corev1.Toleration + for _, value := range values { + var k, v, e string + tokens := strings.SplitN(value, "=", 2) + switch len(tokens) { + case 1: + k = tokens[0] + tokens = strings.Split(k, ":") + switch len(tokens) { + case 1: + case 2: + k, e = tokens[0], tokens[1] + default: + if len(tokens) != 2 { + return nil, fmt.Errorf("invalid toleration %v", value) + } + } + case 2: + k, v = tokens[0], tokens[1] + default: + if len(tokens) != 2 { + return nil, fmt.Errorf("invalid toleration %v", value) + } + } + if k == "" { + return nil, fmt.Errorf("invalid key in toleration %v", value) + } + if v != "" { + if tokens = strings.Split(v, ":"); len(tokens) != 2 { + return nil, fmt.Errorf("invalid value in toleration %v", value) + } + v, e = tokens[0], tokens[1] + } + effect := corev1.TaintEffect(e) + switch effect { + case corev1.TaintEffectNoSchedule, corev1.TaintEffectPreferNoSchedule, corev1.TaintEffectNoExecute: + default: + return nil, fmt.Errorf("invalid toleration effect in toleration %v", value) + } + operator := corev1.TolerationOpExists + if v != "" { + operator = corev1.TolerationOpEqual + } + tolerations = append(tolerations, corev1.Toleration{ + Key: k, + Operator: operator, + Value: v, + Effect: effect, + }) + } + + return tolerations, nil +} diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index d0ad8068..9dde4b14 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -21,8 +21,75 @@ import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/version" ) +func init() { + FakeInit() +} + +var ( + apiGroups = []*metav1.APIGroup{ + { + Name: "policy", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "policy/v1beta1", + Version: "v1beta1", + }, + }, + }, + { + Name: "storage.k8s.io", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "storage.k8s.io/v1", + Version: "v1", + }, + }, + }, + } + + apiResourceList = []*metav1.APIResourceList{ + { + TypeMeta: metav1.TypeMeta{ + APIVersion: "policy/v1beta1", + Kind: "PodSecurityPolicy", + }, + GroupVersion: "policy/v1beta1", + APIResources: []metav1.APIResource{ + { + Name: "policy", + Group: "policy", + Version: "v1beta1", + Namespaced: false, + Kind: "PodSecurityPolicy", + }, + }, + }, + { + TypeMeta: metav1.TypeMeta{ + APIVersion: "storage.k8s.io/v1", + Kind: "CSIDriver", + }, + GroupVersion: "storage.k8s.io/v1", + APIResources: []metav1.APIResource{ + { + Name: "CSIDriver", + Group: "storage.k8s.io", + Version: "v1", + Namespaced: false, + Kind: "CSIDriver", + }, + }, + }, + } +) + +func getDiscoveryGroupsAndMethods() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + return apiGroups, apiResourceList, nil +} + func TestVolumeStatusTransitions(t *testing.T) { statusList := []metav1.Condition{ { @@ -73,3 +140,50 @@ func TestVolumeStatusTransitions(t *testing.T) { }) } } + +func TestGetKubeVersion(t *testing.T) { + testCases := []struct { + info version.Info + major uint + minor uint + expectErr bool + }{ + {version.Info{Major: "a", Minor: "0"}, 0, 0, true}, // invalid major + {version.Info{Major: "-1", Minor: "0"}, 0, 0, true}, // invalid major + {version.Info{Major: "0", Minor: "a"}, 0, 0, true}, // invalid minor + {version.Info{Major: "0", Minor: "-1"}, 0, 0, true}, // invalid minor + {version.Info{Major: "0", Minor: "-1", GitVersion: "commit-eks-id"}, 0, 0, true}, // invalid minor for eks + {version.Info{Major: "0", Minor: "incompat", GitVersion: "commit-eks-"}, 0, 0, true}, // invalid minor for eks + {version.Info{Major: "0", Minor: "0"}, 0, 0, false}, + {version.Info{Major: "1", Minor: "0"}, 1, 0, false}, + {version.Info{Major: "0", Minor: "1"}, 0, 1, false}, + {version.Info{Major: "1", Minor: "18"}, 1, 18, false}, + {version.Info{Major: "1", Minor: "18+", GitVersion: "commit-eks-id"}, 1, 18, false}, + {version.Info{Major: "1", Minor: "18-", GitVersion: "commit-eks-id"}, 1, 18, false}, + {version.Info{Major: "1", Minor: "18incompat", GitVersion: "commit-eks-id"}, 1, 18, false}, + {version.Info{Major: "1", Minor: "18-incompat", GitVersion: "commit-eks-id"}, 1, 18, false}, + } + + for i, testCase := range testCases { + client.DiscoveryClient = NewFakeDiscovery(getDiscoveryGroupsAndMethods, &testCase.info) + major, minor, err := client.GetKubeVersion() + if testCase.expectErr { + if err == nil { + t.Fatalf("case %v: expected error, but succeeded", i+1) + } + continue + } + + if err != nil { + t.Fatalf("case %v: unexpected error: %v", i+1, err) + } + + if major != testCase.major { + t.Fatalf("case %v: major: expected: %v, got: %v", i+1, testCase.major, major) + } + + if minor != testCase.minor { + t.Fatalf("case %v: minor: expected: %v, got: %v", i+1, testCase.minor, minor) + } + } +} diff --git a/pkg/legacy/client/client.go b/pkg/legacy/client/client.go index 4eb52ece..2629ed0c 100644 --- a/pkg/legacy/client/client.go +++ b/pkg/legacy/client/client.go @@ -17,21 +17,46 @@ package client import ( + "context" + "fmt" + "os" + "github.com/minio/directpv/pkg/k8s" directcsi "github.com/minio/directpv/pkg/legacy/apis/direct.csi.min.io/v1beta5" + directv1beta5 "github.com/minio/directpv/pkg/legacy/apis/direct.csi.min.io/v1beta5" typeddirectcsi "github.com/minio/directpv/pkg/legacy/clientset/typed/direct.csi.min.io/v1beta5" + "github.com/minio/directpv/pkg/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/restmapper" - "k8s.io/klog/v2" + "k8s.io/client-go/discovery" ) var ( - initialized int32 - driveClient typeddirectcsi.DirectCSIDriveInterface - volumeClient typeddirectcsi.DirectCSIVolumeInterface + initialized int32 + client *Client ) +// Client represents the legacy client +type Client struct { + DriveClient typeddirectcsi.DirectCSIDriveInterface + VolumeClient typeddirectcsi.DirectCSIVolumeInterface + K8sClient *k8s.Client +} + +// Discovery returns the discovery client +func (client Client) Discovery() discovery.DiscoveryInterface { + return client.K8sClient.DiscoveryClient +} + +// Drive returns the legacy drive client +func (client Client) Drive() typeddirectcsi.DirectCSIDriveInterface { + return client.DriveClient +} + +// Volume returns the volume client +func (client Client) Volume() typeddirectcsi.DirectCSIVolumeInterface { + return client.VolumeClient +} + // DirectCSI group and identity names. const ( GroupName = "direct.csi.min.io" @@ -57,38 +82,99 @@ func DirectCSIVolumeTypeMeta() metav1.TypeMeta { } } -// GetGroupKindVersions gets group/version/kind of given versions. -func GetGroupKindVersions(group, kind string, versions ...string) (*schema.GroupVersionKind, error) { - apiGroupResources, err := restmapper.GetAPIGroupResources(k8s.DiscoveryClient()) - if err != nil { - klog.V(3).Infof("could not obtain API group resources: %v", err) - return nil, err +// DriveClient gets latest versioned drive interface. +func DriveClient() typeddirectcsi.DirectCSIDriveInterface { + return client.DriveClient +} + +// VolumeClient gets latest versioned volume interface. +func VolumeClient() typeddirectcsi.DirectCSIVolumeInterface { + return client.VolumeClient +} + +// GetClient returns the client +func GetClient() *Client { + return client +} + +// RemoveAllDrives removes legacy drive CRDs. +func (client Client) RemoveAllDrives(ctx context.Context, backupFile string) (backupCreated bool, err error) { + var drives []directv1beta5.DirectCSIDrive + for result := range client.ListDrives(ctx) { + if result.Err != nil { + return false, fmt.Errorf("unable to get legacy drives; %w", result.Err) + } + drives = append(drives, result.Drive) } - restMapper := restmapper.NewDiscoveryRESTMapper(apiGroupResources) - gk := schema.GroupKind{ - Group: group, - Kind: kind, + if len(drives) == 0 { + return false, nil } - mapper, err := restMapper.RESTMapping(gk, versions...) + + data, err := utils.ToYAML(directv1beta5.DirectCSIDriveList{ + TypeMeta: metav1.TypeMeta{ + Kind: "List", + APIVersion: "v1", + }, + Items: drives, + }) if err != nil { - klog.V(3).Infof("could not find valid restmapping: %v", err) - return nil, err + return false, fmt.Errorf("unable to generate legacy drives YAML; %w", err) } - gvk := &schema.GroupVersionKind{ - Group: mapper.Resource.Group, - Version: mapper.Resource.Version, - Kind: mapper.Resource.Resource, + if err = os.WriteFile(backupFile, data, os.ModePerm); err != nil { + return false, fmt.Errorf("unable to write legacy drives YAML; %w", err) } - return gvk, nil -} -// DriveClient gets latest versioned drive interface. -func DriveClient() typeddirectcsi.DirectCSIDriveInterface { - return driveClient + for _, drive := range drives { + drive.Finalizers = []string{} + if _, err := client.Drive().Update(ctx, &drive, metav1.UpdateOptions{}); err != nil { + return false, fmt.Errorf("unable to update legacy drive %v; %w", drive.Name, err) + } + if err := client.Drive().Delete(ctx, drive.Name, metav1.DeleteOptions{}); err != nil { + return false, fmt.Errorf("unable to remove legacy drive %v; %w", drive.Name, err) + } + } + + return true, nil } -// VolumeClient gets latest versioned volume interface. -func VolumeClient() typeddirectcsi.DirectCSIVolumeInterface { - return volumeClient +// RemoveAllVolumes removes legacy volume CRDs. +func (client Client) RemoveAllVolumes(ctx context.Context, backupFile string) (backupCreated bool, err error) { + var volumes []directv1beta5.DirectCSIVolume + for result := range client.ListVolumes(ctx) { + if result.Err != nil { + return false, fmt.Errorf("unable to get legacy volumes; %w", result.Err) + } + volumes = append(volumes, result.Volume) + } + if len(volumes) == 0 { + return false, nil + } + + data, err := utils.ToYAML(directv1beta5.DirectCSIVolumeList{ + TypeMeta: metav1.TypeMeta{ + Kind: "List", + APIVersion: "v1", + }, + Items: volumes, + }) + if err != nil { + return false, fmt.Errorf("unable to generate legacy volumes YAML; %w", err) + } + + if err = os.WriteFile(backupFile, data, os.ModePerm); err != nil { + return false, fmt.Errorf("unable to write legacy volumes YAML; %w", err) + } + + for _, volume := range volumes { + volume.Finalizers = nil + if _, err := client.Volume().Update(ctx, &volume, metav1.UpdateOptions{}); err != nil { + return false, fmt.Errorf("unable to update legacy volume %v; %w", volume.Name, err) + } + if err := client.Volume().Delete(ctx, volume.Name, metav1.DeleteOptions{}); err != nil { + return false, fmt.Errorf("unable to remove legacy volume %v; %w", volume.Name, err) + } + } + + return true, nil } diff --git a/pkg/legacy/client/fake.go b/pkg/legacy/client/fake.go index c2f89246..9e949930 100644 --- a/pkg/legacy/client/fake.go +++ b/pkg/legacy/client/fake.go @@ -24,20 +24,21 @@ import ( // FakeInit initializes fake clients. func FakeInit() { k8s.FakeInit() - fakeClientset := legacyclientsetfake.NewSimpleClientset() - driveClient = fakeClientset.DirectV1beta5().DirectCSIDrives() - volumeClient = fakeClientset.DirectV1beta5().DirectCSIVolumes() + client = &Client{ + DriveClient: fakeClientset.DirectV1beta5().DirectCSIDrives(), + VolumeClient: fakeClientset.DirectV1beta5().DirectCSIVolumes(), + } } // SetDriveClient sets drive interface from fake clientset. // Note: To be used for writing test cases only func SetDriveClient(clientset *legacyclientsetfake.Clientset) { - driveClient = clientset.DirectV1beta5().DirectCSIDrives() + client.DriveClient = clientset.DirectV1beta5().DirectCSIDrives() } // SetVolumeClient sets volume interface from fake clientset. // Note: To be used for writing test cases only func SetVolumeClient(clientset *legacyclientsetfake.Clientset) { - volumeClient = clientset.DirectV1beta5().DirectCSIVolumes() + client.VolumeClient = clientset.DirectV1beta5().DirectCSIVolumes() } diff --git a/pkg/legacy/client/init.go b/pkg/legacy/client/init.go index acee6ef1..3140b421 100644 --- a/pkg/legacy/client/init.go +++ b/pkg/legacy/client/init.go @@ -17,6 +17,7 @@ package client import ( + "fmt" "sync/atomic" "github.com/minio/directpv/pkg/k8s" @@ -28,15 +29,29 @@ func Init() { if atomic.AddInt32(&initialized, 1) != 1 { return } - - k8s.Init() - var err error - if driveClient, err = DirectCSIDriveInterfaceForConfig(k8s.KubeConfig()); err != nil { - klog.Fatalf("unable to create new DirectCSI drive interface; %v", err) + if err = k8s.Init(); err != nil { + klog.Fatalf("unable to initialize k8s clients; %v", err) + } + client, err = NewClient(k8s.GetClient()) + if err != nil { + klog.Fatalf("unable to create legacy client; %v", err) } +} - if volumeClient, err = DirectCSIVolumeInterfaceForConfig(k8s.KubeConfig()); err != nil { - klog.Fatalf("unable to create new DirectCSI volume interface; %v", err) +// NewClient creates a legacy client +func NewClient(k8sClient *k8s.Client) (*Client, error) { + driveClient, err := DirectCSIDriveInterfaceForConfig(k8sClient) + if err != nil { + return nil, fmt.Errorf("unable to create new DirectCSI drive interface; %v", err) + } + volumeClient, err := DirectCSIVolumeInterfaceForConfig(k8sClient) + if err != nil { + return nil, fmt.Errorf("unable to create new DirectCSI volume interface; %v", err) } + return &Client{ + DriveClient: driveClient, + VolumeClient: volumeClient, + K8sClient: k8sClient, + }, nil } diff --git a/pkg/legacy/client/interface.go b/pkg/legacy/client/interface.go index 34fae095..d35ea75e 100644 --- a/pkg/legacy/client/interface.go +++ b/pkg/legacy/client/interface.go @@ -42,8 +42,8 @@ import ( ) // GetGroupVersion probes group and version of given resource kind. -func GetGroupVersion(kind string) (version, group string, err error) { - gvk, err := GetGroupKindVersions( +func GetGroupVersion(k8sClient *k8s.Client, kind string) (version, group string, err error) { + gvk, err := k8sClient.GetGroupVersionKind( directcsi.Group, kind, directcsi.Version, @@ -56,7 +56,6 @@ func GetGroupVersion(kind string) (version, group string, err error) { if err != nil && !meta.IsNoMatchError(err) { return "", "", err } - version = directcsi.Version if gvk != nil { version = gvk.Version @@ -65,13 +64,12 @@ func GetGroupVersion(kind string) (version, group string, err error) { if gvk != nil { group = gvk.Group } - return version, group, nil } // GetLatestDirectCSIRESTClient gets REST client of the latest direct-csi. -func GetLatestDirectCSIRESTClient() rest.Interface { - directClientset, err := clientset.NewForConfig(k8s.KubeConfig()) +func GetLatestDirectCSIRESTClient(k8sClient *k8s.Client) rest.Interface { + directClientset, err := clientset.NewForConfig(k8sClient.KubeConfig) if err != nil { panic(err) } @@ -100,13 +98,13 @@ type directCSIInterface struct { groupVersion schema.GroupVersion } -func directCSIInterfaceForConfig(config *rest.Config, kind, resource string) (*directCSIInterface, error) { - version, group, err := GetGroupVersion(kind) +func directCSIInterfaceForConfig(k8sClient *k8s.Client, kind, resource string) (*directCSIInterface, error) { + version, group, err := GetGroupVersion(k8sClient, kind) if err != nil { return nil, err } - resourceInterface, err := dynamic.NewForConfig(config) + resourceInterface, err := dynamic.NewForConfig(k8sClient.KubeConfig) if err != nil { return nil, err } @@ -284,8 +282,8 @@ type DirectCSIDriveInterface struct { } // DirectCSIDriveInterfaceForConfig provides a dynamic client interface for DirectCSIDrives -func DirectCSIDriveInterfaceForConfig(config *rest.Config) (*DirectCSIDriveInterface, error) { - inter, err := directCSIInterfaceForConfig(config, "DirectCSIDrive", "directcsidrives") +func DirectCSIDriveInterfaceForConfig(k8sClient *k8s.Client) (*DirectCSIDriveInterface, error) { + inter, err := directCSIInterfaceForConfig(k8sClient, "DirectCSIDrive", "directcsidrives") if err != nil { return nil, err } @@ -398,8 +396,8 @@ type DirectCSIVolumeInterface struct { } // DirectCSIVolumeInterfaceForConfig provides a dynamic client interface for DirectCSIVolumes -func DirectCSIVolumeInterfaceForConfig(config *rest.Config) (*DirectCSIVolumeInterface, error) { - inter, err := directCSIInterfaceForConfig(config, "DirectCSIVolume", "directcsivolumes") +func DirectCSIVolumeInterfaceForConfig(k8sClient *k8s.Client) (*DirectCSIVolumeInterface, error) { + inter, err := directCSIInterfaceForConfig(k8sClient, "DirectCSIVolume", "directcsivolumes") if err != nil { return nil, err } diff --git a/pkg/legacy/client/list.go b/pkg/legacy/client/list.go index 66b6cced..76fcefaf 100644 --- a/pkg/legacy/client/list.go +++ b/pkg/legacy/client/list.go @@ -31,7 +31,7 @@ type ListDriveResult struct { } // ListDrives returns channel to loop through drive items. -func ListDrives(ctx context.Context) <-chan ListDriveResult { +func (client Client) ListDrives(ctx context.Context) <-chan ListDriveResult { resultCh := make(chan ListDriveResult) go func() { defer close(resultCh) @@ -47,7 +47,7 @@ func ListDrives(ctx context.Context) <-chan ListDriveResult { options := metav1.ListOptions{Limit: 1000} for { - result, err := DriveClient().List(ctx, options) + result, err := client.Drive().List(ctx, options) if err != nil { if !apierrors.IsNotFound(err) { send(ListDriveResult{Err: err}) @@ -79,7 +79,7 @@ type ListVolumeResult struct { } // ListVolumes returns channel to loop through volume items. -func ListVolumes(ctx context.Context) <-chan ListVolumeResult { +func (client Client) ListVolumes(ctx context.Context) <-chan ListVolumeResult { resultCh := make(chan ListVolumeResult) go func() { defer close(resultCh) @@ -95,7 +95,7 @@ func ListVolumes(ctx context.Context) <-chan ListVolumeResult { options := metav1.ListOptions{Limit: 1000} for { - result, err := VolumeClient().List(ctx, options) + result, err := client.Volume().List(ctx, options) if err != nil { if !apierrors.IsNotFound(err) { send(ListVolumeResult{Err: err}) diff --git a/pkg/metrics/collector.go b/pkg/metrics/collector.go index d717c45a..2567eabd 100644 --- a/pkg/metrics/collector.go +++ b/pkg/metrics/collector.go @@ -24,7 +24,6 @@ import ( "github.com/minio/directpv/pkg/consts" "github.com/minio/directpv/pkg/sys" "github.com/minio/directpv/pkg/types" - "github.com/minio/directpv/pkg/volume" "github.com/minio/directpv/pkg/xfs" "github.com/prometheus/client_golang/prometheus" "k8s.io/klog/v2" @@ -101,7 +100,7 @@ func (c *metricsCollector) Collect(ch chan<- prometheus.Metric) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - resultCh := volume.NewLister(). + resultCh := client.NewVolumeLister(). NodeSelector([]directpvtypes.LabelValue{directpvtypes.ToLabelValue(string(c.nodeID))}). List(ctx) for result := range resultCh { diff --git a/pkg/metrics/collector_test.go b/pkg/metrics/collector_test.go index 3dfeecba..b30a4295 100644 --- a/pkg/metrics/collector_test.go +++ b/pkg/metrics/collector_test.go @@ -55,6 +55,7 @@ func init() { volumes[0].Status.TargetPath = "/path/targetpath" volumes[1].Status.UsedCapacity = 20 * MiB volumes[1].Status.TargetPath = "/path/targetpath" + client.FakeInit() } func createFakeMetricsCollector() *metricsCollector { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 0e12f0b1..94b8ca42 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -25,6 +25,7 @@ import ( "strings" "github.com/fatih/color" + directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" "sigs.k8s.io/yaml" ) @@ -153,3 +154,11 @@ func Eprintf(quiet, asErr bool, format string, a ...any) { } fmt.Fprintf(os.Stderr, format, a...) } + +// ToLabelValues converts a string list to label values +func ToLabelValues(slice []string) (values []directpvtypes.LabelValue) { + for _, s := range slice { + values = append(values, directpvtypes.ToLabelValue(s)) + } + return +} diff --git a/pkg/volume/event_test.go b/pkg/volume/event_test.go index f11f7d0a..754f10a6 100644 --- a/pkg/volume/event_test.go +++ b/pkg/volume/event_test.go @@ -30,6 +30,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +func init() { + client.FakeInit() +} + const MiB = 1024 * 1024 func createFakeVolumeEventListener(nodeID directpvtypes.NodeID) *volumeEventHandler {