Merge pull request #4636 from wallrj/remove-conversion-webhook

Refactor the webhook testing code so that alternative CRDs and conversion handlers can be loaded in tests
This commit is contained in:
jetstack-bot 2021-12-15 12:29:33 +00:00 committed by GitHub
commit 1b3adf3b96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 241 additions and 223 deletions

View File

@ -8,7 +8,7 @@ go_library(
deps = [
"//cmd/webhook/app:go_default_library",
"//cmd/webhook/app/options:go_default_library",
"//pkg/logs:go_default_library",
"//pkg/logs/testing:go_default_library",
"//pkg/util/pki:go_default_library",
"//pkg/webhook/server:go_default_library",
"@com_github_spf13_pflag//:go_default_library",

View File

@ -37,13 +37,11 @@ import (
"github.com/jetstack/cert-manager/cmd/webhook/app"
"github.com/jetstack/cert-manager/cmd/webhook/app/options"
logf "github.com/jetstack/cert-manager/pkg/logs"
logtesting "github.com/jetstack/cert-manager/pkg/logs/testing"
"github.com/jetstack/cert-manager/pkg/util/pki"
"github.com/jetstack/cert-manager/pkg/webhook/server"
)
var log = logf.Log.WithName("webhook-server-test")
type StopFunc func()
type ServerOptions struct {
@ -58,7 +56,9 @@ type ServerOptions struct {
CAPEM []byte
}
func StartWebhookServer(t *testing.T, ctx context.Context, args []string) (ServerOptions, StopFunc) {
func StartWebhookServer(t *testing.T, ctx context.Context, args []string, argumentsForNewServerWithOptions ...app.ServerOption) (ServerOptions, StopFunc) {
log := logtesting.TestLogger{T: t}
fs := pflag.NewFlagSet("testset", pflag.ExitOnError)
webhookFlags := options.NewWebhookFlags()
webhookConfig, err := options.NewWebhookConfiguration()
@ -100,7 +100,7 @@ func StartWebhookServer(t *testing.T, ctx context.Context, args []string) (Serve
stopCh := make(chan struct{})
errCh := make(chan error)
srv, err := app.NewServerWithOptions(log, *webhookFlags, *webhookConfig)
srv, err := app.NewServerWithOptions(log, *webhookFlags, *webhookConfig, argumentsForNewServerWithOptions...)
if err != nil {
t.Fatal(err)
}

View File

@ -46,7 +46,17 @@ var validationHook handlers.ValidatingAdmissionHook = handlers.NewRegistryBacked
var mutationHook handlers.MutatingAdmissionHook = handlers.NewRegistryBackedMutator(logf.Log, webhook.Scheme, webhook.MutationRegistry)
var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logf.Log, webhook.Scheme)
func NewServerWithOptions(log logr.Logger, _ options.WebhookFlags, opts config.WebhookConfiguration) (*server.Server, error) {
type ServerOption func(*server.Server)
// WithConversionHandler allows you to override the handler for the `/convert`
// endpoint in tests.
func WithConversionHandler(handler handlers.ConversionHook) ServerOption {
return func(s *server.Server) {
s.ConversionWebhook = handler
}
}
func NewServerWithOptions(log logr.Logger, _ options.WebhookFlags, opts config.WebhookConfiguration, optionFunctions ...ServerOption) (*server.Server, error) {
restcfg, err := clientcmd.BuildConfigFromFlags(opts.APIServerHost, opts.KubeConfig)
if err != nil {
return nil, err
@ -88,7 +98,7 @@ func NewServerWithOptions(log logr.Logger, _ options.WebhookFlags, opts config.W
log.V(logf.WarnLevel).Info("serving insecurely as tls certificate data not provided")
}
return &server.Server{
s := &server.Server{
ListenAddr: fmt.Sprintf(":%d", *opts.SecurePort),
HealthzAddr: fmt.Sprintf(":%d", *opts.HealthzPort),
EnablePprof: opts.EnablePprof,
@ -100,7 +110,11 @@ func NewServerWithOptions(log logr.Logger, _ options.WebhookFlags, opts config.W
MutationWebhook: mutationHook,
ConversionWebhook: conversionHook,
Log: log,
}, nil
}
for _, f := range optionFunctions {
f(s)
}
return s, nil
}
const componentWebhook = "webhook"

View File

@ -39,6 +39,11 @@ export PATH=$(dirname "$go"):$PATH
REPO_ROOT=${BUILD_WORKSPACE_DIRECTORY}
cd "${REPO_ROOT}"
"$controllergen" \
crd \
paths=./pkg/webhook/handlers/testdata/apis/testgroup/v{1,2}/... \
output:crd:dir=./pkg/webhook/handlers/testdata/apis/testgroup/crds
"$controllergen" \
schemapatch:manifests=./deploy/crds \
output:dir=./deploy/crds \

View File

@ -16,7 +16,9 @@ limitations under the License.
package webhook
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -28,6 +28,7 @@ filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/webhook/handlers/testdata/apis/testgroup/crds:all-srcs",
"//pkg/webhook/handlers/testdata/apis/testgroup/fuzzer:all-srcs",
"//pkg/webhook/handlers/testdata/apis/testgroup/install:all-srcs",
"//pkg/webhook/handlers/testdata/apis/testgroup/v1:all-srcs",

View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,3 @@
# README
These CRDs are auto generated by `hack/update-crds.sh`.

View File

@ -0,0 +1,92 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: (unknown)
creationTimestamp: null
name: testtypes.testgroup.testing.cert-manager.io
spec:
group: testgroup.testing.cert-manager.io
names:
kind: TestType
listKind: TestTypeList
plural: testtypes
singular: testtype
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
testField:
description: TestField is used in tests. Validation doesn't allow this
to be set to the value of TestFieldValueNotAllowed.
type: string
testFieldImmutable:
description: TestFieldImmutable cannot be changed after being set to a
non-zero value
type: string
testFieldPtr:
type: string
required:
- metadata
- testField
- testFieldImmutable
type: object
served: true
storage: false
- name: v2
schema:
openAPIV3Schema:
description: TestType in v2 is identical to v1, except TestFieldPtr has been
renamed to TestFieldPtrAlt
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
testField:
description: TestField is used in tests. Validation doesn't allow this
to be set to the value of TestFieldValueNotAllowed.
type: string
testFieldImmutable:
description: TestFieldImmutable cannot be changed after being set to a
non-zero value
type: string
testFieldPtrAlt:
type: string
required:
- metadata
- testField
- testFieldImmutable
type: object
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -22,6 +22,7 @@ import (
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:storageversion
// TestType in v2 is identical to v1, except TestFieldPtr has been renamed to TestFieldPtrAlt
type TestType struct {
metav1.TypeMeta `json:",inline"`

View File

@ -3,20 +3,23 @@ load("@io_bazel_rules_go//go:def.bzl", "go_test")
go_test(
name = "go_default_test",
srcs = ["conversion_test.go"],
data = [
"//pkg/webhook/handlers/testdata/apis/testgroup/crds:all-srcs",
],
deps = [
"//pkg/api:go_default_library",
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/certmanager/v1alpha2:go_default_library",
"//pkg/apis/certmanager/v1alpha3:go_default_library",
"//pkg/apis/certmanager/v1beta1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/util/pki:go_default_library",
"//pkg/logs/testing:go_default_library",
"//pkg/webhook/handlers:go_default_library",
"//pkg/webhook/handlers/testdata/apis/testgroup/install:go_default_library",
"//pkg/webhook/handlers/testdata/apis/testgroup/v1:go_default_library",
"//pkg/webhook/handlers/testdata/apis/testgroup/v2:go_default_library",
"//test/integration/framework:go_default_library",
"@io_k8s_apimachinery//pkg/api/equality:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
"@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client:go_default_library",
"@io_k8s_utils//diff:go_default_library",
"@io_k8s_utils//pointer:go_default_library",
],
)

View File

@ -18,231 +18,83 @@ package conversion
import (
"context"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"testing"
"time"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/diff"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/jetstack/cert-manager/pkg/api"
v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha3"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1beta1"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
"github.com/jetstack/cert-manager/pkg/util/pki"
logtesting "github.com/jetstack/cert-manager/pkg/logs/testing"
"github.com/jetstack/cert-manager/pkg/webhook/handlers"
testapi "github.com/jetstack/cert-manager/pkg/webhook/handlers/testdata/apis/testgroup/install"
testv1 "github.com/jetstack/cert-manager/pkg/webhook/handlers/testdata/apis/testgroup/v1"
testv2 "github.com/jetstack/cert-manager/pkg/webhook/handlers/testdata/apis/testgroup/v2"
"github.com/jetstack/cert-manager/test/integration/framework"
)
func generateCSR(t *testing.T) []byte {
skRSA, err := pki.GenerateRSAPrivateKey(2048)
if err != nil {
t.Fatal(err)
}
asn1Subj, _ := asn1.Marshal(pkix.Name{
CommonName: "test",
}.ToRDNSequence())
template := x509.CertificateRequest{
RawSubject: asn1Subj,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, skRSA)
if err != nil {
t.Fatal(err)
}
csr := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
return csr
}
func TestConversion(t *testing.T) {
testCSR := generateCSR(t)
tests := map[string]struct {
input client.Object
targetGVK schema.GroupVersionKind
output client.Object
}{
"should convert Certificates from v1alpha2 to v1alpha3": {
input: &v1alpha2.Certificate{
"should convert from v1 to v2": {
input: &testv1.TestType{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha2.CertificateSpec{
SecretName: "something",
CommonName: "test",
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
TestFieldPtr: pointer.StringPtr("test1"),
},
targetGVK: v1alpha3.SchemeGroupVersion.WithKind("Certificate"),
output: &v1alpha3.Certificate{
targetGVK: testv2.SchemeGroupVersion.WithKind("TestType"),
output: &testv2.TestType{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha3.CertificateSpec{
SecretName: "something",
CommonName: "test",
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
TestFieldPtrAlt: pointer.StringPtr("test1"),
},
},
"should convert CertificateRequest from v1alpha2 to v1alpha3": {
input: &v1alpha2.CertificateRequest{
"should convert from v2 to v1": {
input: &testv2.TestType{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha2.CertificateRequestSpec{
CSRPEM: testCSR,
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
TestFieldPtrAlt: pointer.StringPtr("test1"),
},
targetGVK: v1alpha3.SchemeGroupVersion.WithKind("CertificateRequest"),
output: &v1alpha3.CertificateRequest{
targetGVK: testv1.SchemeGroupVersion.WithKind("TestType"),
output: &testv1.TestType{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha3.CertificateRequestSpec{
CSRPEM: testCSR,
Username: "admin",
Groups: []string{"system:masters", "system:authenticated"},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
},
},
"should convert CertificateRequest from v1alpha2 to v1": {
input: &v1alpha2.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha2.CertificateRequestSpec{
CSRPEM: testCSR,
Username: "some-user",
Groups: []string{"some-group"},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
},
targetGVK: v1.SchemeGroupVersion.WithKind("CertificateRequest"),
output: &v1.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1.CertificateRequestSpec{
Request: testCSR,
Username: "admin",
Groups: []string{"system:masters", "system:authenticated"},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
},
},
"should convert Certificate from v1alpha2 to v1beta1": {
input: &v1alpha2.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha2.CertificateSpec{
SecretName: "abc",
CommonName: "test",
Organization: []string{"test"},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
},
targetGVK: v1beta1.SchemeGroupVersion.WithKind("Certificate"),
output: &v1beta1.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1beta1.CertificateSpec{
SecretName: "abc",
CommonName: "test",
Subject: &v1beta1.X509Subject{
Organizations: []string{"test"},
},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
},
},
"should convert Certificate from v1beta1 to v1": {
input: &v1beta1.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1beta1.CertificateSpec{
SecretName: "abc",
CommonName: "test",
Subject: &v1beta1.X509Subject{
Organizations: []string{"test"},
},
URISANs: []string{"spiffe://foo.foo.example.net"},
EmailSANs: []string{"alice@example.com"},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
},
targetGVK: v1.SchemeGroupVersion.WithKind("Certificate"),
output: &v1.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1.CertificateSpec{
SecretName: "abc",
CommonName: "test",
Subject: &v1.X509Subject{
Organizations: []string{"test"},
},
URIs: []string{"spiffe://foo.foo.example.net"},
EmailAddresses: []string{"alice@example.com"},
IssuerRef: cmmeta.ObjectReference{
Name: "issuername",
},
},
TestFieldPtr: pointer.StringPtr("test1"),
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
log := logtesting.TestLogger{T: t}
scheme := runtime.NewScheme()
testapi.Install(scheme)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*40)
defer cancel()
config, stop := framework.RunControlPlane(t, ctx)
config, stop := framework.RunControlPlane(
t, ctx,
framework.WithCRDDirectory("../../../pkg/webhook/handlers/testdata/apis/testgroup/crds"),
framework.WithWebhookConversionHandler(handlers.NewSchemeBackedConverter(log, scheme)),
)
defer stop()
cl, err := client.New(config, client.Options{Scheme: api.Scheme})
cl, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
t.Fatal(err)
}
@ -252,7 +104,7 @@ func TestConversion(t *testing.T) {
}
meta := test.input.(metav1.ObjectMetaAccessor)
convertedObj, err := api.Scheme.New(test.targetGVK)
convertedObj, err := scheme.New(test.targetGVK)
if err != nil {
t.Fatal(err)
}

View File

@ -9,12 +9,14 @@ go_library(
importpath = "github.com/jetstack/cert-manager/test/integration/framework",
visibility = ["//visibility:public"],
deps = [
"//cmd/webhook/app:go_default_library",
"//cmd/webhook/app/testing:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/testing:go_default_library",
"//pkg/client/clientset/versioned:go_default_library",
"//pkg/client/informers/externalversions:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/webhook/handlers:go_default_library",
"//test/internal/apiserver:go_default_library",
"@io_k8s_api//admissionregistration/v1:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
@ -37,6 +39,7 @@ go_library(
"@io_k8s_kubectl//pkg/util/openapi:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/envtest:go_default_library",
"@io_k8s_utils//pointer:go_default_library",
],
)

View File

@ -34,25 +34,63 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/jetstack/cert-manager/cmd/webhook/app"
webhooktesting "github.com/jetstack/cert-manager/cmd/webhook/app/testing"
"github.com/jetstack/cert-manager/pkg/api"
apitesting "github.com/jetstack/cert-manager/pkg/api/testing"
"github.com/jetstack/cert-manager/pkg/webhook/handlers"
"github.com/jetstack/cert-manager/test/internal/apiserver"
)
type StopFunc func()
func RunControlPlane(t *testing.T, ctx context.Context) (*rest.Config, StopFunc) {
// controlPlaneOptions has parameters for the control plane of the integration
// test framework which can be overridden in tests.
type controlPlaneOptions struct {
crdsDir *string
webhookConversionHandler handlers.ConversionHook
}
type RunControlPlaneOption func(*controlPlaneOptions)
// WithCRDDirectory allows alternative CRDs to be loaded into the test API
// server in tests.
func WithCRDDirectory(directory string) RunControlPlaneOption {
return func(o *controlPlaneOptions) {
o.crdsDir = pointer.StringPtr(directory)
}
}
// WithWebhookConversionHandler allows the webhook handler for the `/convert`
// endpoint to be overridden in tests.
func WithWebhookConversionHandler(handler handlers.ConversionHook) RunControlPlaneOption {
return func(o *controlPlaneOptions) {
o.webhookConversionHandler = handler
}
}
func RunControlPlane(t *testing.T, ctx context.Context, optionFunctions ...RunControlPlaneOption) (*rest.Config, StopFunc) {
options := &controlPlaneOptions{
crdsDir: pointer.StringPtr(apitesting.CRDDirectory(t)),
}
for _, f := range optionFunctions {
f(options)
}
env, stopControlPlane := apiserver.RunBareControlPlane(t)
config := env.Config
webhookOpts, stopWebhook := webhooktesting.StartWebhookServer(t, ctx, []string{"--api-server-host=" + config.Host})
webhookOpts, stopWebhook := webhooktesting.StartWebhookServer(
t, ctx, []string{"--api-server-host=" + config.Host},
app.WithConversionHandler(options.webhookConversionHandler),
)
crdsDir := apitesting.CRDDirectory(t)
crds := readCustomResourcesAtPath(t, crdsDir)
crds := readCustomResourcesAtPath(t, *options.crdsDir)
for _, crd := range crds {
t.Logf("Found CRD with name %q", crd.Name)
}
@ -96,31 +134,22 @@ func init() {
apiextensionsinstall.Install(internalScheme)
}
// patchCRDConversion overrides the conversion configuration of the CRDs that
// are loaded in to the integration test API server,
// configuring the conversion to be handled by the local webhook server.
func patchCRDConversion(crds []apiextensionsv1.CustomResourceDefinition, url string, caPEM []byte) {
for _, crd := range crds {
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Served = true
for i := range crds {
url := fmt.Sprintf("%s%s", url, "/convert")
crds[i].Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
Strategy: apiextensionsv1.WebhookConverter,
Webhook: &apiextensionsv1.WebhookConversion{
ClientConfig: &apiextensionsv1.WebhookClientConfig{
URL: &url,
CABundle: caPEM,
},
ConversionReviewVersions: []string{"v1"},
},
}
if crd.Spec.Conversion == nil {
continue
}
if crd.Spec.Conversion.Webhook == nil {
continue
}
if crd.Spec.Conversion.Webhook.ClientConfig == nil {
continue
}
if crd.Spec.Conversion.Webhook.ClientConfig.Service == nil {
continue
}
path := ""
if crd.Spec.Conversion.Webhook.ClientConfig.Service.Path != nil {
path = *crd.Spec.Conversion.Webhook.ClientConfig.Service.Path
}
url := fmt.Sprintf("%s%s", url, path)
crd.Spec.Conversion.Webhook.ClientConfig.URL = &url
crd.Spec.Conversion.Webhook.ClientConfig.CABundle = caPEM
crd.Spec.Conversion.Webhook.ClientConfig.Service = nil
}
}