Fix verify error and address review comments

Signed-off-by: Wu Qiang <qiang.q.wu@oracle.com>
This commit is contained in:
Wu Qiang 2017-11-15 11:20:12 +08:00 committed by Wu Qiang
parent 31fb539f17
commit 16b04d68b1
9 changed files with 117 additions and 80 deletions

View File

@ -605,6 +605,7 @@ staging/src/k8s.io/apiserver/pkg/storage/storagebackend
staging/src/k8s.io/apiserver/pkg/storage/testing
staging/src/k8s.io/apiserver/pkg/storage/tests
staging/src/k8s.io/apiserver/pkg/storage/value
staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1
staging/src/k8s.io/apiserver/pkg/util/feature
staging/src/k8s.io/apiserver/pkg/util/flag
staging/src/k8s.io/apiserver/pkg/util/proxy

View File

@ -170,7 +170,7 @@ func GetPrefixTransformers(config *ResourceConfig) ([]value.PrefixTransformer, e
}
} else {
// Get gRPC client service with remote config
envelopeService, err = envelope.NewEnvelopeService(
envelopeService, err = envelope.NewGRPCService(
remoteConfig.Endpoint,
remoteConfig.ServerCACert,
remoteConfig.ClientCert,

View File

@ -11,16 +11,15 @@ go_library(
srcs = [
"envelope.go",
"grpc_service.go",
"service.pb.go",
],
importpath = "k8s.io/apiserver/pkg/storage/value/encrypt/envelope",
deps = [
"//vendor/github.com/golang/protobuf/proto:go_default_library",
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/google.golang.org/grpc:go_default_library",
"//vendor/google.golang.org/grpc/credentials:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1:go_default_library",
],
)
@ -41,6 +40,7 @@ go_test(
"//vendor/google.golang.org/grpc/credentials:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1:go_default_library",
"//vendor/k8s.io/client-go/util/cert:go_default_library",
],
)
@ -54,12 +54,9 @@ filegroup(
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
srcs = [
":package-srcs",
"//staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1:all-srcs",
],
tags = ["automanaged"],
)
filegroup(
name = "go_default_library_protos",
srcs = ["service.proto"],
visibility = ["//visibility:public"],
)

View File

@ -30,6 +30,8 @@ import (
"google.golang.org/grpc/credentials"
"golang.org/x/net/context"
kmsapi "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1"
)
const (
@ -43,12 +45,12 @@ const (
type gRPCService struct {
// gRPC client instance
kmsClient KmsServiceClient
kmsClient kmsapi.KMSServiceClient
connection *grpc.ClientConn
}
// NewEnvelopeService returns an envelope.Service which use gRPC to communicate the remote KMS provider.
func NewEnvelopeService(endpoint, serverCert, clientCert, clientKey string) (Service, error) {
// NewGRPCService returns an envelope.Service which use gRPC to communicate the remote KMS provider.
func NewGRPCService(endpoint, serverCert, clientCert, clientKey string) (Service, error) {
protocol, addr, err := parseEndpoint(endpoint)
if err != nil {
return nil, err
@ -59,7 +61,7 @@ func NewEnvelopeService(endpoint, serverCert, clientCert, clientKey string) (Ser
}
// With or without TLS/SSL support
tlsOption, err := getTlsDialOption(serverCert, clientCert, clientKey)
tlsOption, err := getTLSDialOption(serverCert, clientCert, clientKey)
if err != nil {
return nil, err
}
@ -69,11 +71,15 @@ func NewEnvelopeService(endpoint, serverCert, clientCert, clientKey string) (Ser
return nil, fmt.Errorf("connect remote image service %s failed, error: %v", addr, err)
}
return &gRPCService{kmsClient: NewKmsServiceClient(conn), connection: conn}, nil
return &gRPCService{kmsClient: kmsapi.NewKMSServiceClient(conn), connection: conn}, nil
}
// Parse the endpoint to extract schema, host or path.
func parseEndpoint(endpoint string) (string, string, error) {
if len(endpoint) == 0 {
return "", "", fmt.Errorf("remote KMS provider can't use empty string as endpoint")
}
u, err := url.Parse(endpoint)
if err != nil {
return "", "", fmt.Errorf("invalid kms provider endpoint %q, error: %v", endpoint, err)
@ -90,7 +96,7 @@ func parseEndpoint(endpoint string) (string, string, error) {
}
// Build the TLS/SSL options for gRPC client.
func getTlsDialOption(serverCert, clientCert, clientKey string) (grpc.DialOption, error) {
func getTLSDialOption(serverCert, clientCert, clientKey string) (grpc.DialOption, error) {
// No TLS/SSL support.
if len(serverCert) == 0 && len(clientCert) == 0 && len(clientKey) == 0 {
return grpc.WithInsecure(), nil
@ -132,7 +138,7 @@ func getTlsDialOption(serverCert, clientCert, clientKey string) (grpc.DialOption
// Decrypt a given data string to obtain the original byte data.
func (g *gRPCService) Decrypt(cipher string) ([]byte, error) {
request := &DecryptRequest{Cipher: []byte(cipher), Version: version}
request := &kmsapi.DecryptRequest{Cipher: []byte(cipher), Version: version}
response, err := g.kmsClient.Decrypt(context.Background(), request)
if err != nil {
return nil, err
@ -142,7 +148,7 @@ func (g *gRPCService) Decrypt(cipher string) ([]byte, error) {
// Encrypt bytes to a string ciphertext.
func (g *gRPCService) Encrypt(plain []byte) (string, error) {
request := &EncryptRequest{Plain: plain, Version: version}
request := &kmsapi.EncryptRequest{Plain: plain, Version: version}
response, err := g.kmsClient.Encrypt(context.Background(), request)
if err != nil {
return "", err

View File

@ -29,6 +29,7 @@ import (
"google.golang.org/grpc/credentials"
"golang.org/x/net/context"
kmsapi "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1"
"k8s.io/client-go/util/cert"
)
@ -43,21 +44,21 @@ const (
clientKey = "testdata/client.key"
)
func TestTcpEndpoint(t *testing.T) {
func TestTCPEndpoint(t *testing.T) {
// Start the gRPC server that listens on tcp socket.
listener, err := tcpListner()
if err != nil {
t.Fatal(err)
}
server := startTestKmsProvider(listener)
server := startTestKMSProvider(listener)
defer server.Stop()
endpoint := tcpProtocol + "://" + listener.Addr().String()
verifyService(t, endpoint, "", "", "")
}
func TestTlsEndpoint(t *testing.T) {
func TestTLSEndpoint(t *testing.T) {
// Start the gRPC server that listens on tcp socket.
listener, err := tcpListner()
if err != nil {
@ -69,7 +70,7 @@ func TestTlsEndpoint(t *testing.T) {
t.Fatal(err)
}
server := startTestKmsProvider(listener, tlsOption)
server := startTestKMSProvider(listener, tlsOption)
defer server.Stop()
// There are 2 TLS case: no auth and client auth.
@ -102,7 +103,7 @@ func TestInvalidConfiguration(t *testing.T) {
t.Fatal(err)
}
server := startTestKmsProvider(listener, tlsOption)
server := startTestKMSProvider(listener, tlsOption)
defer server.Stop()
endpoint := tcpProtocol + "://" + listener.Addr().String()
@ -122,7 +123,7 @@ func TestInvalidConfiguration(t *testing.T) {
for _, testCase := range invalidConfigs {
t.Run(testCase.name, func(t *testing.T) {
_, err := NewEnvelopeService(
_, err := NewGRPCService(
testCase.endpoint,
testCase.serverCACert,
testCase.clientCert,
@ -136,7 +137,7 @@ func TestInvalidConfiguration(t *testing.T) {
}
func verifyService(t *testing.T, endpoint, serverCACert, clientCert, clientKey string) {
service, err := NewEnvelopeService(endpoint, serverCACert, clientCert, clientKey)
service, err := NewGRPCService(endpoint, serverCACert, clientCert, clientKey)
if err != nil {
t.Fatalf("failed to create envelope service, error: %v", err)
}
@ -172,9 +173,9 @@ func tcpListner() (net.Listener, error) {
return listener, nil
}
func startTestKmsProvider(listener net.Listener, options ...grpc.ServerOption) *grpc.Server {
func startTestKMSProvider(listener net.Listener, options ...grpc.ServerOption) *grpc.Server {
server := grpc.NewServer(options...)
RegisterKmsServiceServer(server, &base64Server{})
kmsapi.RegisterKMSServiceServer(server, &base64Server{})
go server.Serve(listener)
return server
}
@ -201,18 +202,18 @@ func tlsServerOption() (grpc.ServerOption, error) {
// Fake gRPC sever for remote KMS provider.
type base64Server struct{}
func (b *base64Server) Decrypt(ctx context.Context, request *DecryptRequest) (*DecryptResponse, error) {
func (b *base64Server) Decrypt(ctx context.Context, request *kmsapi.DecryptRequest) (*kmsapi.DecryptResponse, error) {
buf := make([]byte, base64.StdEncoding.DecodedLen(len(request.Cipher)))
n, err := base64.StdEncoding.Decode(buf, request.Cipher)
if err != nil {
return nil, err
}
return &DecryptResponse{Plain: buf[:n]}, nil
return &kmsapi.DecryptResponse{Plain: buf[:n]}, nil
}
func (b *base64Server) Encrypt(ctx context.Context, request *EncryptRequest) (*EncryptResponse, error) {
func (b *base64Server) Encrypt(ctx context.Context, request *kmsapi.EncryptRequest) (*kmsapi.EncryptResponse, error) {
buf := make([]byte, base64.StdEncoding.EncodedLen(len(request.Plain)))
base64.StdEncoding.Encode(buf, request.Plain)
return &EncryptResponse{Cipher: buf}, nil
return &kmsapi.EncryptResponse{Cipher: buf}, nil
}

View File

@ -39,7 +39,7 @@ func TestUnixSockEndpoint(t *testing.T) {
t.Fatal(err)
}
server := startTestKmsProvider(listener)
server := startTestKMSProvider(listener)
defer func() {
server.Stop()
if err := cleanSockFile(); err != nil {

View File

@ -0,0 +1,33 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
filegroup(
name = "go_default_library_protos",
srcs = ["service.proto"],
visibility = ["//visibility:public"],
)
go_library(
name = "go_default_library",
srcs = ["service.pb.go"],
importpath = "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/golang/protobuf/proto:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/google.golang.org/grpc:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -19,7 +19,7 @@ limitations under the License.
// DO NOT EDIT!
/*
Package envelope is a generated protocol buffer package.
Package v1beta1 is a generated protocol buffer package.
It is generated from these files:
service.proto
@ -30,7 +30,7 @@ It has these top-level messages:
EncryptRequest
EncryptResponse
*/
package envelope
package v1beta1
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
@ -93,10 +93,10 @@ func (*EncryptResponse) ProtoMessage() {}
func (*EncryptResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func init() {
proto.RegisterType((*DecryptRequest)(nil), "envelope.DecryptRequest")
proto.RegisterType((*DecryptResponse)(nil), "envelope.DecryptResponse")
proto.RegisterType((*EncryptRequest)(nil), "envelope.EncryptRequest")
proto.RegisterType((*EncryptResponse)(nil), "envelope.EncryptResponse")
proto.RegisterType((*DecryptRequest)(nil), "v1beta1.DecryptRequest")
proto.RegisterType((*DecryptResponse)(nil), "v1beta1.DecryptResponse")
proto.RegisterType((*EncryptRequest)(nil), "v1beta1.EncryptRequest")
proto.RegisterType((*EncryptResponse)(nil), "v1beta1.EncryptResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
@ -107,97 +107,97 @@ var _ grpc.ClientConn
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for KmsService service
// Client API for KMSService service
type KmsServiceClient interface {
type KMSServiceClient interface {
Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error)
Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error)
}
type kmsServiceClient struct {
type kMSServiceClient struct {
cc *grpc.ClientConn
}
func NewKmsServiceClient(cc *grpc.ClientConn) KmsServiceClient {
return &kmsServiceClient{cc}
func NewKMSServiceClient(cc *grpc.ClientConn) KMSServiceClient {
return &kMSServiceClient{cc}
}
func (c *kmsServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) {
func (c *kMSServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) {
out := new(DecryptResponse)
err := grpc.Invoke(ctx, "/envelope.KmsService/Decrypt", in, out, c.cc, opts...)
err := grpc.Invoke(ctx, "/v1beta1.KMSService/Decrypt", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kmsServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) {
func (c *kMSServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) {
out := new(EncryptResponse)
err := grpc.Invoke(ctx, "/envelope.KmsService/Encrypt", in, out, c.cc, opts...)
err := grpc.Invoke(ctx, "/v1beta1.KMSService/Encrypt", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for KmsService service
// Server API for KMSService service
type KmsServiceServer interface {
type KMSServiceServer interface {
Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error)
Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error)
}
func RegisterKmsServiceServer(s *grpc.Server, srv KmsServiceServer) {
s.RegisterService(&_KmsService_serviceDesc, srv)
func RegisterKMSServiceServer(s *grpc.Server, srv KMSServiceServer) {
s.RegisterService(&_KMSService_serviceDesc, srv)
}
func _KmsService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
func _KMSService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DecryptRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KmsServiceServer).Decrypt(ctx, in)
return srv.(KMSServiceServer).Decrypt(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/envelope.KmsService/Decrypt",
FullMethod: "/v1beta1.KMSService/Decrypt",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KmsServiceServer).Decrypt(ctx, req.(*DecryptRequest))
return srv.(KMSServiceServer).Decrypt(ctx, req.(*DecryptRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KmsService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
func _KMSService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EncryptRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KmsServiceServer).Encrypt(ctx, in)
return srv.(KMSServiceServer).Encrypt(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/envelope.KmsService/Encrypt",
FullMethod: "/v1beta1.KMSService/Encrypt",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KmsServiceServer).Encrypt(ctx, req.(*EncryptRequest))
return srv.(KMSServiceServer).Encrypt(ctx, req.(*EncryptRequest))
}
return interceptor(ctx, in, info, handler)
}
var _KmsService_serviceDesc = grpc.ServiceDesc{
ServiceName: "envelope.KmsService",
HandlerType: (*KmsServiceServer)(nil),
var _KMSService_serviceDesc = grpc.ServiceDesc{
ServiceName: "v1beta1.KMSService",
HandlerType: (*KMSServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Decrypt",
Handler: _KmsService_Decrypt_Handler,
Handler: _KMSService_Decrypt_Handler,
},
{
MethodName: "Encrypt",
Handler: _KmsService_Encrypt_Handler,
Handler: _KMSService_Encrypt_Handler,
},
},
Streams: []grpc.StreamDesc{},
@ -207,19 +207,18 @@ var _KmsService_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("service.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 209 bytes of a gzipped FileDescriptorProto
// 207 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x4e, 0x2d, 0x2a,
0xcb, 0x4c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x48, 0xcd, 0x2b, 0x4b, 0xcd,
0xc9, 0x2f, 0x48, 0x55, 0x72, 0xe2, 0xe2, 0x73, 0x49, 0x4d, 0x2e, 0xaa, 0x2c, 0x28, 0x09, 0x4a,
0x2d, 0x2c, 0x4d, 0x2d, 0x2e, 0x11, 0x12, 0xe3, 0x62, 0x4b, 0xce, 0x2c, 0xc8, 0x48, 0x2d, 0x92,
0x60, 0x54, 0x60, 0xd4, 0xe0, 0x09, 0x82, 0xf2, 0x84, 0x24, 0xb8, 0xd8, 0xcb, 0x52, 0x8b, 0x8a,
0x33, 0xf3, 0xf3, 0x24, 0x98, 0x14, 0x18, 0x35, 0x38, 0x83, 0x60, 0x5c, 0x25, 0x75, 0x2e, 0x7e,
0xb8, 0x19, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x22, 0x5c, 0xac, 0x05, 0x39, 0x89, 0x99,
0x79, 0x50, 0x33, 0x20, 0x1c, 0x25, 0x07, 0x2e, 0x3e, 0xd7, 0x3c, 0x14, 0xcb, 0xb0, 0xaa, 0xc3,
0x63, 0x95, 0x26, 0x17, 0x3f, 0xdc, 0x04, 0xa8, 0x55, 0x38, 0xdc, 0x6b, 0x34, 0x81, 0x91, 0x8b,
0xcb, 0x3b, 0xb7, 0x38, 0x18, 0xe2, 0x71, 0x21, 0x07, 0x2e, 0x76, 0xa8, 0x23, 0x85, 0x24, 0xf4,
0x60, 0xde, 0xd7, 0x43, 0xf5, 0xbb, 0x94, 0x24, 0x16, 0x19, 0x88, 0x35, 0x4a, 0x0c, 0x20, 0x13,
0xa0, 0x76, 0x23, 0x9b, 0x80, 0xea, 0x21, 0x64, 0x13, 0xd0, 0x1c, 0xaa, 0xc4, 0x90, 0xc4, 0x06,
0x0e, 0x7d, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb0, 0xc2, 0xcf, 0x84, 0x8e, 0x01, 0x00,
0x00,
0xcb, 0x4c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x2f, 0x33, 0x4c, 0x4a, 0x2d,
0x49, 0x34, 0x54, 0x72, 0xe2, 0xe2, 0x73, 0x49, 0x4d, 0x2e, 0xaa, 0x2c, 0x28, 0x09, 0x4a, 0x2d,
0x2c, 0x4d, 0x2d, 0x2e, 0x11, 0x12, 0xe3, 0x62, 0x4b, 0xce, 0x2c, 0xc8, 0x48, 0x2d, 0x92, 0x60,
0x54, 0x60, 0xd4, 0xe0, 0x09, 0x82, 0xf2, 0x84, 0x24, 0xb8, 0xd8, 0xcb, 0x52, 0x8b, 0x8a, 0x33,
0xf3, 0xf3, 0x24, 0x98, 0x14, 0x18, 0x35, 0x38, 0x83, 0x60, 0x5c, 0x25, 0x75, 0x2e, 0x7e, 0xb8,
0x19, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x22, 0x5c, 0xac, 0x05, 0x39, 0x89, 0x99, 0x79,
0x50, 0x33, 0x20, 0x1c, 0x25, 0x07, 0x2e, 0x3e, 0xd7, 0x3c, 0x14, 0xcb, 0xb0, 0xaa, 0xc3, 0x63,
0x95, 0x26, 0x17, 0x3f, 0xdc, 0x04, 0xa8, 0x55, 0x38, 0xdc, 0x6b, 0xd4, 0xc3, 0xc8, 0xc5, 0xe5,
0xed, 0x1b, 0x1c, 0x0c, 0xf1, 0xb7, 0x90, 0x1d, 0x17, 0x3b, 0xd4, 0x91, 0x42, 0xe2, 0x7a, 0x50,
0xdf, 0xeb, 0xa1, 0x7a, 0x5d, 0x4a, 0x02, 0x53, 0x02, 0x62, 0x89, 0x12, 0x03, 0x48, 0x3f, 0xd4,
0x66, 0x24, 0xfd, 0xa8, 0xbe, 0x41, 0xd2, 0x8f, 0xe6, 0x48, 0x25, 0x86, 0x24, 0x36, 0x70, 0xc0,
0x1b, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xd2, 0x95, 0x8d, 0x5c, 0x89, 0x01, 0x00, 0x00,
}

View File

@ -1,8 +1,8 @@
syntax = "proto3";
package envelope;
package v1beta1;
service KmsService {
service KMSService {
rpc Decrypt(DecryptRequest) returns (DecryptResponse) {}
rpc Encrypt(EncryptRequest) returns (EncryptResponse) {}
}