![Ryan Richard](/assets/img/avatar_default.png)
Also fix some tests that were broken by bumping golang and dependencies in the previous commits. Note that in addition to changes made to satisfy the linter which do not impact the behavior of the code, this commit also adds ReadHeaderTimeout to all usages of http.Server to satisfy the linter (and because it seemed like a good suggestion).
192 lines
5.8 KiB
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()
|
|
}
|