ContainerImage.Pinniped/cmd/pinniped/cmd/whoami.go

192 lines
5.8 KiB
Go

// Copyright 2021-2022 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()
}