Skip to content

Commit 8894a27

Browse files
committed
add a client to support token request
Signed-off-by: Wei Liu <liuweixa@redhat.com>
1 parent b6c1a66 commit 8894a27

12 files changed

Lines changed: 1646 additions & 3 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package serviceaccount
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/google/uuid"
8+
authenticationv1 "k8s.io/api/authentication/v1"
9+
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/errors"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/types"
13+
"k8s.io/apimachinery/pkg/watch"
14+
applyconfigurationscorev1 "k8s.io/client-go/applyconfigurations/core/v1"
15+
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
16+
"k8s.io/klog/v2"
17+
18+
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/clients"
19+
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/builder"
20+
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/grpc"
21+
cetypes "open-cluster-management.io/sdk-go/pkg/cloudevents/generic/types"
22+
)
23+
24+
var (
25+
// TokenRequestTimeout is the timeout for CreateToken requests
26+
TokenRequestTimeout = 10 * time.Second
27+
)
28+
29+
type ServiceAccountClient struct {
30+
grpcOptions *grpc.GRPCOptions
31+
clusterName string
32+
}
33+
34+
var _ corev1client.ServiceAccountInterface = &ServiceAccountClient{}
35+
36+
// NewServiceAccountClient returns a ServiceAccountInterface
37+
// This client only supports creating token via gRPC in cluster namespace on the hub.
38+
func NewServiceAccountClient(clusterName string, opt *grpc.GRPCOptions) *ServiceAccountClient {
39+
return &ServiceAccountClient{
40+
grpcOptions: opt,
41+
clusterName: clusterName,
42+
}
43+
}
44+
45+
func (sa *ServiceAccountClient) Create(ctx context.Context, serviceAccount *corev1.ServiceAccount, opts metav1.CreateOptions) (*corev1.ServiceAccount, error) {
46+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "create")
47+
}
48+
49+
func (sa *ServiceAccountClient) Update(ctx context.Context, serviceAccount *corev1.ServiceAccount, opts metav1.UpdateOptions) (*corev1.ServiceAccount, error) {
50+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "update")
51+
}
52+
53+
func (sa *ServiceAccountClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
54+
return errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "delete")
55+
}
56+
57+
func (sa *ServiceAccountClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
58+
return errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "delete")
59+
}
60+
61+
func (sa *ServiceAccountClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.ServiceAccount, error) {
62+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "get")
63+
}
64+
65+
func (sa *ServiceAccountClient) List(ctx context.Context, opts metav1.ListOptions) (*corev1.ServiceAccountList, error) {
66+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "list")
67+
}
68+
69+
func (sa *ServiceAccountClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
70+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "watch")
71+
}
72+
73+
func (sa *ServiceAccountClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *corev1.ServiceAccount, err error) {
74+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "patch")
75+
}
76+
77+
func (sa *ServiceAccountClient) Apply(ctx context.Context, serviceAccount *applyconfigurationscorev1.ServiceAccountApplyConfiguration, opts metav1.ApplyOptions) (result *corev1.ServiceAccount, err error) {
78+
return nil, errors.NewMethodNotSupported(corev1.Resource("serviceaccounts"), "apply")
79+
}
80+
81+
func (sa *ServiceAccountClient) CreateToken(ctx context.Context, serviceAccountName string, tokenRequest *authenticationv1.TokenRequest, opts metav1.CreateOptions) (*authenticationv1.TokenRequest, error) {
82+
tokenRequestCtx, cancel := context.WithTimeout(ctx, TokenRequestTimeout)
83+
defer cancel()
84+
85+
// Create a cancellable context for the client lifecycle
86+
clientCtx, clientCancel := context.WithCancel(ctx)
87+
defer clientCancel() // Ensure client resources are cleaned up
88+
89+
responseChan := make(chan *authenticationv1.TokenRequest, 1)
90+
91+
options, err := builder.BuildCloudEventsAgentOptions(
92+
sa.grpcOptions,
93+
sa.clusterName,
94+
sa.clusterName,
95+
TokenRequestDataType,
96+
)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
cloudEventsClient, err := clients.NewCloudEventAgentClient(
102+
clientCtx,
103+
options,
104+
nil, // resync is disabled, so lister is not required
105+
nil, // resync is disabled, so statusHashGetter is not required
106+
&TokenRequestCodec{},
107+
)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
requestID := types.UID(uuid.New().String())
113+
114+
// subscribe before publish to avoid missing the response
115+
cloudEventsClient.Subscribe(clientCtx, func(handlerCtx context.Context, resp *authenticationv1.TokenRequest) error {
116+
if resp.UID != requestID {
117+
return nil
118+
}
119+
120+
logger := klog.FromContext(handlerCtx)
121+
logger.V(4).Info("response token", "requestID", resp.UID, "serviceAccountName", resp.Name)
122+
responseChan <- resp
123+
return nil
124+
})
125+
126+
eventType := cetypes.CloudEventsType{
127+
CloudEventsDataType: TokenRequestDataType,
128+
SubResource: cetypes.SubResourceSpec,
129+
Action: cetypes.CreateRequestAction,
130+
}
131+
132+
newTokenRequest := tokenRequest.DeepCopy()
133+
newTokenRequest.UID = requestID
134+
newTokenRequest.Name = serviceAccountName
135+
newTokenRequest.Namespace = sa.clusterName // the serviceaccount should locate in cluster namespace on hub
136+
137+
logger := klog.FromContext(tokenRequestCtx)
138+
logger.V(4).Info("request token", "requestID", requestID, "serviceAccountName", serviceAccountName)
139+
if err := cloudEventsClient.Publish(tokenRequestCtx, eventType, newTokenRequest); err != nil {
140+
return nil, errors.NewInternalError(err)
141+
}
142+
143+
// wait until the tokenRequestResponse is received or timeout
144+
select {
145+
case tokenRequestResponse := <-responseChan:
146+
return tokenRequestResponse, nil
147+
case <-tokenRequestCtx.Done():
148+
return nil, errors.NewInternalError(tokenRequestCtx.Err())
149+
}
150+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package serviceaccount
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/api/errors"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
12+
)
13+
14+
func TestNewServiceAccountClient(t *testing.T) {
15+
clusterName := "test-cluster"
16+
saClient := NewServiceAccountClient(clusterName, nil)
17+
18+
if saClient == nil {
19+
t.Fatal("expected non-nil ServiceAccountClient")
20+
}
21+
22+
if saClient.clusterName != clusterName {
23+
t.Errorf("expected clusterName %s, got %s", clusterName, saClient.clusterName)
24+
}
25+
26+
// Verify it implements the interface
27+
var _ corev1client.ServiceAccountInterface = saClient
28+
}
29+
30+
func TestUnsupportedMethods(t *testing.T) {
31+
saClient := &ServiceAccountClient{
32+
clusterName: "test-cluster",
33+
}
34+
35+
ctx := context.Background()
36+
sa := &corev1.ServiceAccount{
37+
ObjectMeta: metav1.ObjectMeta{
38+
Name: "test-sa",
39+
Namespace: "test-ns",
40+
},
41+
}
42+
43+
tests := []struct {
44+
name string
45+
fn func() error
46+
}{
47+
{
48+
name: "Create",
49+
fn: func() error {
50+
_, err := saClient.Create(ctx, sa, metav1.CreateOptions{})
51+
return err
52+
},
53+
},
54+
{
55+
name: "Update",
56+
fn: func() error {
57+
_, err := saClient.Update(ctx, sa, metav1.UpdateOptions{})
58+
return err
59+
},
60+
},
61+
{
62+
name: "Delete",
63+
fn: func() error {
64+
return saClient.Delete(ctx, "test-sa", metav1.DeleteOptions{})
65+
},
66+
},
67+
{
68+
name: "DeleteCollection",
69+
fn: func() error {
70+
return saClient.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})
71+
},
72+
},
73+
{
74+
name: "Get",
75+
fn: func() error {
76+
_, err := saClient.Get(ctx, "test-sa", metav1.GetOptions{})
77+
return err
78+
},
79+
},
80+
{
81+
name: "List",
82+
fn: func() error {
83+
_, err := saClient.List(ctx, metav1.ListOptions{})
84+
return err
85+
},
86+
},
87+
{
88+
name: "Watch",
89+
fn: func() error {
90+
_, err := saClient.Watch(ctx, metav1.ListOptions{})
91+
return err
92+
},
93+
},
94+
{
95+
name: "Patch",
96+
fn: func() error {
97+
_, err := saClient.Patch(ctx, "test-sa", types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{})
98+
return err
99+
},
100+
},
101+
{
102+
name: "Apply",
103+
fn: func() error {
104+
_, err := saClient.Apply(ctx, nil, metav1.ApplyOptions{})
105+
return err
106+
},
107+
},
108+
}
109+
110+
for _, tt := range tests {
111+
t.Run(tt.name, func(t *testing.T) {
112+
err := tt.fn()
113+
if !errors.IsMethodNotSupported(err) {
114+
t.Errorf("expected MethodNotSupported error, got: %v", err)
115+
}
116+
})
117+
}
118+
}
119+
120+
// TestCreateToken would require a full gRPC infrastructure to test properly.
121+
// Integration tests for CreateToken should be implemented separately with proper
122+
// gRPC server mocking or test infrastructure.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package serviceaccount
2+
3+
import (
4+
"fmt"
5+
6+
authenticationv1 "k8s.io/api/authentication/v1"
7+
8+
cloudevents "github.com/cloudevents/sdk-go/v2"
9+
10+
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/types"
11+
)
12+
13+
var TokenRequestDataType = types.CloudEventsDataType{
14+
Group: authenticationv1.GroupName,
15+
Version: "v1",
16+
Resource: "tokenrequests",
17+
}
18+
19+
// TokenRequestCodec is a codec to encode/decode a event/cloudevent for an agent.
20+
type TokenRequestCodec struct{}
21+
22+
func NewTokenRequestCodec() *TokenRequestCodec {
23+
return &TokenRequestCodec{}
24+
}
25+
26+
// EventDataType always returns the event data type `authentication.k8s.io.v1.tokenrequests`.
27+
func (c *TokenRequestCodec) EventDataType() types.CloudEventsDataType {
28+
return TokenRequestDataType
29+
}
30+
31+
// Encode the event to a cloudevent
32+
func (c *TokenRequestCodec) Encode(source string, eventType types.CloudEventsType, tokenRequest *authenticationv1.TokenRequest) (*cloudevents.Event, error) {
33+
if eventType.CloudEventsDataType != TokenRequestDataType {
34+
return nil, fmt.Errorf("unsupported cloudevents data type %s", eventType.CloudEventsDataType)
35+
}
36+
37+
evt := types.NewEventBuilder(source, eventType).
38+
WithResourceID(string(tokenRequest.UID)).
39+
WithClusterName(tokenRequest.Namespace).
40+
NewEvent()
41+
42+
if err := evt.SetData(cloudevents.ApplicationJSON, tokenRequest); err != nil {
43+
return nil, fmt.Errorf("failed to encode event to a cloudevent: %v", err)
44+
}
45+
46+
return &evt, nil
47+
}
48+
49+
// Decode a cloudevent to an event object
50+
func (c *TokenRequestCodec) Decode(evt *cloudevents.Event) (*authenticationv1.TokenRequest, error) {
51+
tokenRequest := &authenticationv1.TokenRequest{}
52+
if err := evt.DataAs(tokenRequest); err != nil {
53+
return nil, fmt.Errorf("failed to unmarshal event data %s, %v", string(evt.Data()), err)
54+
}
55+
56+
return tokenRequest, nil
57+
}

0 commit comments

Comments
 (0)