github.com/oam-dev/cluster-gateway@v1.9.0/pkg/util/exec/exec.go (about)

     1  package exec
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"sync"
    10  	"time"
    11  
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/runtime"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/apimachinery/pkg/runtime/serializer"
    16  
    17  	"k8s.io/client-go/pkg/apis/clientauthentication"
    18  	"k8s.io/client-go/pkg/apis/clientauthentication/install"
    19  	clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
    20  	clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
    21  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    22  )
    23  
    24  var (
    25  	scheme = runtime.NewScheme()
    26  
    27  	codecs = serializer.NewCodecFactory(scheme)
    28  
    29  	apiVersions = map[string]schema.GroupVersion{
    30  		clientauthenticationv1beta1.SchemeGroupVersion.String(): clientauthenticationv1beta1.SchemeGroupVersion,
    31  		clientauthenticationv1.SchemeGroupVersion.String():      clientauthenticationv1.SchemeGroupVersion,
    32  	}
    33  
    34  	credentials sync.Map
    35  )
    36  
    37  func init() {
    38  	install.Install(scheme)
    39  }
    40  
    41  func IssueClusterCredential(name string, ec *clientcmdapi.ExecConfig) (*clientauthentication.ExecCredential, error) {
    42  	if name == "" {
    43  		return nil, errors.New("cluster name not provided")
    44  	}
    45  
    46  	value, found := credentials.Load(name)
    47  	if found {
    48  		cred, ok := value.(*clientauthentication.ExecCredential)
    49  		if !ok {
    50  			return nil, errors.New("failed to convert item in cache to ExecCredential")
    51  		}
    52  
    53  		now := &metav1.Time{Time: time.Now().Add(time.Minute)} // expires a minute early
    54  
    55  		if cred.Status != nil && cred.Status.ExpirationTimestamp.Before(now) {
    56  			credentials.Delete(name)
    57  			return IssueClusterCredential(name, ec) // credential expired, calling function again
    58  		}
    59  
    60  		return cred, nil // credential on cache still valid
    61  	}
    62  
    63  	cred, err := issueClusterCredential(ec)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	if cred.Status != nil && !cred.Status.ExpirationTimestamp.IsZero() {
    69  		credentials.Store(name, cred) // storing credential in cache
    70  	}
    71  
    72  	return cred, nil
    73  }
    74  
    75  func issueClusterCredential(ec *clientcmdapi.ExecConfig) (*clientauthentication.ExecCredential, error) {
    76  	if ec == nil {
    77  		return nil, errors.New("exec config not provided")
    78  	}
    79  
    80  	if ec.Command == "" {
    81  		return nil, errors.New("missing \"command\" property on exec config object")
    82  	}
    83  
    84  	command, err := exec.LookPath(ec.Command)
    85  	if err != nil {
    86  		return nil, unwrapExecCommandError(ec.Command, err)
    87  	}
    88  
    89  	cmd := exec.Command(command, ec.Args...)
    90  	cmd.Env = os.Environ()
    91  
    92  	for _, env := range ec.Env {
    93  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", env.Name, env.Value))
    94  	}
    95  
    96  	var stderr, stdout bytes.Buffer
    97  	cmd.Stderr = &stderr
    98  	cmd.Stdout = &stdout
    99  
   100  	if err := cmd.Run(); err != nil {
   101  		return nil, unwrapExecCommandError(command, err)
   102  	}
   103  
   104  	ecgv, err := schema.ParseGroupVersion(ec.APIVersion)
   105  	if err != nil {
   106  		return nil, fmt.Errorf("failed to parse exec config API version: %v", err)
   107  	}
   108  
   109  	cred := &clientauthentication.ExecCredential{
   110  		TypeMeta: metav1.TypeMeta{
   111  			APIVersion: ec.APIVersion,
   112  			Kind:       "ExecCredential",
   113  		},
   114  		Spec: clientauthentication.ExecCredentialSpec{},
   115  	}
   116  
   117  	gv, ok := apiVersions[ec.APIVersion]
   118  	if !ok {
   119  		return nil, fmt.Errorf("exec plugin: invalid apiVersion %q", ec.APIVersion)
   120  	}
   121  
   122  	_, gvk, err := codecs.UniversalDecoder(gv).Decode(stdout.Bytes(), nil, cred)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("decoding stdout: %v", err)
   125  	}
   126  
   127  	if gvk.Group != ecgv.Group || gvk.Version != ecgv.Version {
   128  		return nil, fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s", ecgv, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version})
   129  	}
   130  
   131  	if cred.Status == nil {
   132  		return nil, fmt.Errorf("exec plugin didn't return a status field")
   133  	}
   134  
   135  	if cred.Status.Token == "" && cred.Status.ClientCertificateData == "" && cred.Status.ClientKeyData == "" {
   136  		return nil, fmt.Errorf("exec plugin didn't return a token or cert/key pair")
   137  	}
   138  
   139  	if (cred.Status.ClientCertificateData == "") != (cred.Status.ClientKeyData == "") {
   140  		return nil, fmt.Errorf("exec plugin returned only certificate or key, not both")
   141  	}
   142  
   143  	return cred, nil
   144  }
   145  
   146  func unwrapExecCommandError(path string, err error) error {
   147  	switch err.(type) {
   148  	case *exec.Error: // Binary does not exist (see exec.Error).
   149  		return fmt.Errorf("exec: executable %s not found", path)
   150  
   151  	case *exec.ExitError: // Binary execution failed (see exec.Cmd.Run()).
   152  		e := err.(*exec.ExitError)
   153  		return fmt.Errorf("exec: executable %s failed with exit code %d", path, e.ProcessState.ExitCode())
   154  
   155  	default:
   156  		return fmt.Errorf("exec: %v", err)
   157  	}
   158  }