diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 3e74f9f..aab6643 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -43,6 +43,7 @@ requiring access to the MapR cluster.`, rootCmd.AddCommand( newListCmd(rootOpts), newVersionCmd(rootOpts), + newUsedByCmd(rootOpts), ) return rootCmd diff --git a/cmd/cli/usedby.go b/cmd/cli/usedby.go new file mode 100644 index 0000000..e49d084 --- /dev/null +++ b/cmd/cli/usedby.go @@ -0,0 +1,105 @@ +package cli + +import ( + "fmt" + + "github.com/nobbs/kubectl-mapr-ticket/internal/util" + "github.com/nobbs/kubectl-mapr-ticket/internal/volumes" + "github.com/spf13/cobra" +) + +type UsedByOptions struct { + *rootCmdOptions + + // Args are the arguments passed to the command + args []string + + // SecretName is the name of the secret to find persistent volumes for + SecretName string +} + +func NewUsedByOptions(rootOpts *rootCmdOptions) *UsedByOptions { + return &UsedByOptions{ + rootCmdOptions: rootOpts, + } +} + +func newUsedByCmd(rootOpts *rootCmdOptions) *cobra.Command { + o := NewUsedByOptions(rootOpts) + + cmd := &cobra.Command{ + Use: "used-by secret", + Short: "List all persistent volumes that use the specified MapR ticket secret", + Long: `List all persistent volumes that use the specified MapR ticket secret and print +some information about them.`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := o.Complete(cmd, args); err != nil { + return err + } + + if err := o.Validate(); err != nil { + return err + } + + if err := o.Run(cmd, args); err != nil { + return err + } + + return nil + }, + } + + // set IOStreams for the command + cmd.SetIn(o.IOStreams.In) + cmd.SetOut(o.IOStreams.Out) + cmd.SetErr(o.IOStreams.ErrOut) + + return cmd +} + +func (o *UsedByOptions) Complete(cmd *cobra.Command, args []string) error { + o.args = args + + if len(args) > 0 { + o.SecretName = args[0] + } + + return nil +} + +func (o *UsedByOptions) Validate() error { + // ensure that the secret name was provided + if o.SecretName == "" { + return fmt.Errorf("secret name must be provided") + } + + return nil +} + +func (o *UsedByOptions) Run(cmd *cobra.Command, args []string) error { + client, err := util.ClientFromFlags(o.kubernetesConfigFlags) + if err != nil { + return err + } + + // create list options + opts := []volumes.ListerOption{} + + // create lister + lister := volumes.NewLister(client, o.SecretName, *o.kubernetesConfigFlags.Namespace, opts...) + + // run the lister + volumes, err := lister.Run() + if err != nil { + return err + } + + // print the volumes + // if err := volumes.Print(cmd); err != nil { + // return err + // } + + fmt.Printf("%+v\n", volumes) + + return nil +} diff --git a/internal/volumes/volumes.go b/internal/volumes/volumes.go new file mode 100644 index 0000000..d42294c --- /dev/null +++ b/internal/volumes/volumes.go @@ -0,0 +1,122 @@ +package volumes + +import ( + "context" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + typedV1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +var ( + // maprCSIProvisioners is a list of the default MapR CSI provisioners + // that we support. + maprCSIProvisioners = []string{ + "com.mapr.csi-kdf", + "com.mapr.csi-nfskdf", + } +) + +type Lister struct { + client typedV1.PersistentVolumeInterface + secretName string + namespace string +} + +type ListerOption func(*Lister) + +func NewLister(client kubernetes.Interface, secretName string, namespace string, opts ...ListerOption) *Lister { + l := &Lister{ + client: client.CoreV1().PersistentVolumes(), + secretName: secretName, + namespace: namespace, + } + + for _, opt := range opts { + opt(l) + } + + return l +} + +func (l *Lister) Run() ([]coreV1.PersistentVolume, error) { + // Unfortunately, we have to list all persistent volumes and filter them + // ourselves, because there is no way to filter them by label selector. + volumes, err := l.client.List(context.TODO(), metaV1.ListOptions{}) + if err != nil { + return nil, err + } + + // Filter the volumes to only MapR CSI-based ones + filtered := l.filterVolumesToMaprCSI(volumes.Items) + + // Filter the volumes to only ones that use the specified secret + filtered = l.filterVolumeUsesTicket(filtered) + + return filtered, nil +} + +func (l *Lister) filterVolumesToMaprCSI(volumes []coreV1.PersistentVolume) []coreV1.PersistentVolume { + var filtered []coreV1.PersistentVolume + + for _, volume := range volumes { + if l.volumeIsMaprCSIBased(&volume) { + filtered = append(filtered, volume) + } + } + + return filtered +} + +func (l *Lister) filterVolumeUsesTicket(volumes []coreV1.PersistentVolume) []coreV1.PersistentVolume { + var filtered []coreV1.PersistentVolume + + for _, volume := range volumes { + if l.volumeUsesTicket(&volume) { + filtered = append(filtered, volume) + } + } + + return filtered +} + +func (l *Lister) volumeUsesTicket(volume *coreV1.PersistentVolume) bool { + // Check if the volume uses a CSI driver + if volume.Spec.CSI == nil { + return false + } + + // Check if the volume uses a NodePublishSecretRef + if volume.Spec.CSI.NodePublishSecretRef == nil { + return false + } + + // Check if the volume uses the specified secret + if volume.Spec.CSI.NodePublishSecretRef.Name != l.secretName { + return false + } + + // Check if the volume uses the specified namespace + if volume.Spec.CSI.NodePublishSecretRef.Namespace != l.namespace { + return false + } + + return true +} + +func (l *Lister) volumeIsMaprCSIBased(volume *coreV1.PersistentVolume) bool { + // Check if the volume is MapR CSI-based + if volume.Spec.CSI == nil { + return false + } + + // Check if the volume is provisioned by one of the MapR CSI provisioners + for _, provisioner := range maprCSIProvisioners { + if volume.Spec.CSI.Driver == provisioner { + return true + } + } + + return false +}