192 lines
5.8 KiB
Go
192 lines
5.8 KiB
Go
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
|
|
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
|
conciergescheme "go.pinniped.dev/internal/concierge/scheme"
|
|
"go.pinniped.dev/internal/groupsuffix"
|
|
"go.pinniped.dev/internal/here"
|
|
)
|
|
|
|
//nolint: gochecknoinits
|
|
func init() {
|
|
rootCmd.AddCommand(newWhoamiCommand(getRealConciergeClientset))
|
|
}
|
|
|
|
type whoamiFlags struct {
|
|
outputFormat string // e.g., yaml, json, text
|
|
|
|
kubeconfigPath string
|
|
kubeconfigContextOverride string
|
|
|
|
apiGroupSuffix string
|
|
}
|
|
|
|
type clusterInfo struct {
|
|
name string
|
|
url string
|
|
}
|
|
|
|
func newWhoamiCommand(getClientset getConciergeClientsetFunc) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
|
Use: "whoami",
|
|
Short: "Print information about the current user",
|
|
SilenceUsage: true,
|
|
}
|
|
flags := &whoamiFlags{}
|
|
|
|
// flags
|
|
f := cmd.Flags()
|
|
f.StringVarP(&flags.outputFormat, "output", "o", "text", "Output format (e.g., 'yaml', 'json', 'text')")
|
|
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
|
|
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
|
|
f.StringVar(&flags.apiGroupSuffix, "api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
|
|
|
cmd.RunE = func(cmd *cobra.Command, _ []string) error {
|
|
return runWhoami(cmd.OutOrStdout(), getClientset, flags)
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runWhoami(output io.Writer, getClientset getConciergeClientsetFunc, flags *whoamiFlags) error {
|
|
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
|
|
clientset, err := getClientset(clientConfig, flags.apiGroupSuffix)
|
|
if err != nil {
|
|
return fmt.Errorf("could not configure Kubernetes client: %w", err)
|
|
}
|
|
|
|
clusterInfo, err := getCurrentCluster(clientConfig, flags.kubeconfigContextOverride)
|
|
if err != nil {
|
|
return fmt.Errorf("could not get current cluster info: %w", err)
|
|
}
|
|
|
|
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
|
defer cancelFunc()
|
|
whoAmI, err := clientset.IdentityV1alpha1().WhoAmIRequests().Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
|
if err != nil {
|
|
hint := ""
|
|
if errors.IsNotFound(err) {
|
|
hint = " (is the Pinniped WhoAmI API running and healthy?)"
|
|
}
|
|
return fmt.Errorf("could not complete WhoAmIRequest%s: %w", hint, err)
|
|
}
|
|
|
|
if err := writeWhoamiOutput(output, flags, clusterInfo, whoAmI); err != nil {
|
|
return fmt.Errorf("could not write output: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getCurrentCluster(clientConfig clientcmd.ClientConfig, currentContextNameOverride string) (*clusterInfo, error) {
|
|
currentKubeConfig, err := clientConfig.RawConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contextName := currentKubeConfig.CurrentContext
|
|
if len(currentContextNameOverride) > 0 {
|
|
contextName = currentContextNameOverride
|
|
}
|
|
|
|
unknownClusterInfo := &clusterInfo{name: "???", url: "???"}
|
|
ctx, ok := currentKubeConfig.Contexts[contextName]
|
|
if !ok {
|
|
return unknownClusterInfo, nil
|
|
}
|
|
|
|
cluster, ok := currentKubeConfig.Clusters[ctx.Cluster]
|
|
if !ok {
|
|
return unknownClusterInfo, nil
|
|
}
|
|
|
|
return &clusterInfo{name: ctx.Cluster, url: cluster.Server}, nil
|
|
}
|
|
|
|
func writeWhoamiOutput(output io.Writer, flags *whoamiFlags, cInfo *clusterInfo, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
|
switch flags.outputFormat {
|
|
case "text":
|
|
return writeWhoamiOutputText(output, cInfo, whoAmI)
|
|
case "json":
|
|
return writeWhoamiOutputJSON(output, flags.apiGroupSuffix, whoAmI)
|
|
case "yaml":
|
|
return writeWhoamiOutputYAML(output, flags.apiGroupSuffix, whoAmI)
|
|
default:
|
|
return fmt.Errorf("unknown output format: %q", flags.outputFormat)
|
|
}
|
|
}
|
|
|
|
func writeWhoamiOutputText(output io.Writer, clusterInfo *clusterInfo, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
|
fmt.Fprint(output, here.Docf(`
|
|
Current cluster info:
|
|
|
|
Name: %s
|
|
URL: %s
|
|
|
|
Current user info:
|
|
|
|
Username: %s
|
|
Groups: %s
|
|
`, clusterInfo.name, clusterInfo.url, whoAmI.Status.KubernetesUserInfo.User.Username, prettyStrings(whoAmI.Status.KubernetesUserInfo.User.Groups)))
|
|
return nil
|
|
}
|
|
|
|
func writeWhoamiOutputJSON(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
|
return serialize(output, apiGroupSuffix, whoAmI, runtime.ContentTypeJSON)
|
|
}
|
|
|
|
func writeWhoamiOutputYAML(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
|
return serialize(output, apiGroupSuffix, whoAmI, runtime.ContentTypeYAML)
|
|
}
|
|
|
|
func serialize(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest, contentType string) error {
|
|
scheme, _, identityGV := conciergescheme.New(apiGroupSuffix)
|
|
codecs := serializer.NewCodecFactory(scheme)
|
|
respInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), contentType)
|
|
if !ok {
|
|
return fmt.Errorf("unknown content type: %q", contentType)
|
|
}
|
|
|
|
// I have seen the pretty serializer be nil before, so this will hopefully protect against that
|
|
// corner.
|
|
serializer := respInfo.PrettySerializer
|
|
if serializer == nil {
|
|
serializer = respInfo.Serializer
|
|
}
|
|
|
|
// Ensure that these fields are set so that the JSON/YAML output tells the full story.
|
|
whoAmI.APIVersion = identityGV.String()
|
|
whoAmI.Kind = "WhoAmIRequest"
|
|
|
|
return serializer.Encode(whoAmI, output)
|
|
}
|
|
|
|
func prettyStrings(ss []string) string {
|
|
b := &strings.Builder{}
|
|
for i, s := range ss {
|
|
if i != 0 {
|
|
b.WriteString(", ")
|
|
}
|
|
b.WriteString(s)
|
|
}
|
|
return b.String()
|
|
}
|