Implement client.ExchangeToken.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
1a349bb609
commit
b0d9db1bcc
@ -7,11 +7,76 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/pkg/apis/clientauthentication"
|
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
|
||||||
|
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
|
||||||
|
placeholderclientset "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/clientset/versioned"
|
||||||
|
"github.com/suzerain-io/placeholder-name/internal/constable"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrLoginFailed is returned by ExchangeToken when the server rejects the login request.
|
||||||
|
const ErrLoginFailed = constable.Error("login failed")
|
||||||
|
|
||||||
func ExchangeToken(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) {
|
func ExchangeToken(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) {
|
||||||
_, _, _, _ = ctx, token, caBundle, apiEndpoint
|
clientset, err := getClient(apiEndpoint, caBundle)
|
||||||
return nil, nil
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := clientset.PlaceholderV1alpha1().LoginRequests().Create(ctx, &placeholderv1alpha1.LoginRequest{
|
||||||
|
Spec: placeholderv1alpha1.LoginRequestSpec{
|
||||||
|
Type: placeholderv1alpha1.TokenLoginCredentialType,
|
||||||
|
Token: &placeholderv1alpha1.LoginRequestTokenCredential{
|
||||||
|
Value: token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not login: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Status.Credential == nil || resp.Status.Message != "" {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrLoginFailed, resp.Status.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientauthentication.ExecCredential{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ExecCredential",
|
||||||
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||||
|
},
|
||||||
|
Status: &clientauthentication.ExecCredentialStatus{
|
||||||
|
ExpirationTimestamp: resp.Status.Credential.ExpirationTimestamp,
|
||||||
|
ClientCertificateData: resp.Status.Credential.ClientCertificateData,
|
||||||
|
ClientKeyData: resp.Status.Credential.ClientKeyData,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClient returns an anonymous clientset for the placeholder-name API at the provided endpoint/CA bundle.
|
||||||
|
func getClient(apiEndpoint string, caBundle string) (placeholderclientset.Interface, error) {
|
||||||
|
cfg, err := clientcmd.NewNonInteractiveClientConfig(clientcmdapi.Config{
|
||||||
|
Clusters: map[string]*clientcmdapi.Cluster{
|
||||||
|
"cluster": {
|
||||||
|
Server: apiEndpoint,
|
||||||
|
CertificateAuthorityData: []byte(caBundle),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Contexts: map[string]*clientcmdapi.Context{
|
||||||
|
"current": {
|
||||||
|
Cluster: "cluster",
|
||||||
|
AuthInfo: "client",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||||
|
"client": {},
|
||||||
|
},
|
||||||
|
}, "current", nil, nil).ClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return placeholderclientset.NewForConfig(cfg)
|
||||||
}
|
}
|
||||||
|
121
pkg/client/client_test.go
Normal file
121
pkg/client/client_test.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||||
|
|
||||||
|
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startTestServer(t *testing.T, handler http.HandlerFunc) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
server := httptest.NewTLSServer(handler)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
caBundle := string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: server.TLS.Certificates[0].Certificate[0],
|
||||||
|
}))
|
||||||
|
return caBundle, server.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeToken(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("invalid configuration", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got, err := ExchangeToken(ctx, "", "", "")
|
||||||
|
require.EqualError(t, err, "could not get API client: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
|
||||||
|
require.Nil(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("server error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Start a test server that returns only 500 errors.
|
||||||
|
caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte("some server error"))
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
||||||
|
require.EqualError(t, err, `could not login: an error on the server ("some server error") has prevented the request from succeeding (post loginrequests.placeholder.suzerain-io.github.io)`)
|
||||||
|
require.Nil(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("login failure", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Start a test server that returns success but with an error message
|
||||||
|
caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&placeholderv1alpha1.LoginRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "placeholder.suzerain-io.github.io/v1alpha1", Kind: "LoginRequest"},
|
||||||
|
Status: placeholderv1alpha1.LoginRequestStatus{Message: "some login failure"},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
||||||
|
require.EqualError(t, err, `login failed: some login failure`)
|
||||||
|
require.Nil(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Start a test server that returns successfully and asserts various properties of the request.
|
||||||
|
caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "application/json", r.Header.Get("content-type"))
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t,
|
||||||
|
`{
|
||||||
|
"kind": "LoginRequest",
|
||||||
|
"apiVersion": "placeholder.suzerain-io.github.io/v1alpha1",
|
||||||
|
"metadata": {
|
||||||
|
"creationTimestamp": null
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"type": "token",
|
||||||
|
"token": {}
|
||||||
|
},
|
||||||
|
"status": {}
|
||||||
|
}`,
|
||||||
|
string(body),
|
||||||
|
)
|
||||||
|
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&placeholderv1alpha1.LoginRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "placeholder.suzerain-io.github.io/v1alpha1", Kind: "LoginRequest"},
|
||||||
|
Status: placeholderv1alpha1.LoginRequestStatus{
|
||||||
|
Credential: &placeholderv1alpha1.LoginRequestCredential{
|
||||||
|
ClientCertificateData: "test-certificate",
|
||||||
|
ClientKeyData: "test-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &clientauthentication.ExecCredential{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ExecCredential",
|
||||||
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||||
|
},
|
||||||
|
Status: &clientauthentication.ExecCredentialStatus{
|
||||||
|
ClientCertificateData: "test-certificate",
|
||||||
|
ClientKeyData: "test-key",
|
||||||
|
},
|
||||||
|
}, got)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user