mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
Add status subresource to HorizontalPodAutoscaler
This commit is contained in:
parent
41a7f579ea
commit
3c012db30f
@ -51,6 +51,19 @@ func ValidateHorizontalPodAutoscalerName(name string, prefix bool) (bool, string
|
|||||||
return apivalidation.ValidateReplicationControllerName(name, prefix)
|
return apivalidation.ValidateReplicationControllerName(name, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateResourceConsumption(consumption *extensions.ResourceConsumption, fieldName string) errs.ValidationErrorList {
|
||||||
|
allErrs := errs.ValidationErrorList{}
|
||||||
|
resource := consumption.Resource.String()
|
||||||
|
if resource != string(api.ResourceMemory) && resource != string(api.ResourceCPU) {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid(fieldName+".resource", resource, "resource not supported by autoscaler"))
|
||||||
|
}
|
||||||
|
quantity := consumption.Quantity.Value()
|
||||||
|
if quantity < 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid(fieldName+".quantity", quantity, "must be non-negative"))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
func validateHorizontalPodAutoscalerSpec(autoscaler extensions.HorizontalPodAutoscalerSpec) errs.ValidationErrorList {
|
func validateHorizontalPodAutoscalerSpec(autoscaler extensions.HorizontalPodAutoscalerSpec) errs.ValidationErrorList {
|
||||||
allErrs := errs.ValidationErrorList{}
|
allErrs := errs.ValidationErrorList{}
|
||||||
if autoscaler.MinReplicas < 0 {
|
if autoscaler.MinReplicas < 0 {
|
||||||
@ -87,6 +100,19 @@ func ValidateHorizontalPodAutoscalerUpdate(newAutoscler, oldAutoscaler *extensio
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidateHorizontalPodAutoscalerStatusUpdate(controller, oldController *extensions.HorizontalPodAutoscaler) errs.ValidationErrorList {
|
||||||
|
allErrs := errs.ValidationErrorList{}
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...)
|
||||||
|
|
||||||
|
status := controller.Status
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.CurrentReplicas), "currentReplicas")...)
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.DesiredReplicas), "desiredReplicas")...)
|
||||||
|
if status.CurrentConsumption != nil {
|
||||||
|
allErrs = append(allErrs, validateResourceConsumption(status.CurrentConsumption, "currentConsumption")...)
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
func ValidateThirdPartyResourceUpdate(old, update *extensions.ThirdPartyResource) errs.ValidationErrorList {
|
func ValidateThirdPartyResourceUpdate(old, update *extensions.ThirdPartyResource) errs.ValidationErrorList {
|
||||||
return ValidateThirdPartyResource(update)
|
return ValidateThirdPartyResource(update)
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ type HorizontalPodAutoscalerInterface interface {
|
|||||||
Delete(name string, options *api.DeleteOptions) error
|
Delete(name string, options *api.DeleteOptions) error
|
||||||
Create(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error)
|
Create(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error)
|
||||||
Update(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error)
|
Update(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error)
|
||||||
|
UpdateStatus(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error)
|
||||||
Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
|
Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +95,13 @@ func (c *horizontalPodAutoscalers) Update(horizontalPodAutoscaler *extensions.Ho
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateStatus takes the representation of a horizontalPodAutoscaler and updates it. Returns the server's representation of the horizontalPodAutoscaler, and an error, if it occurs.
|
||||||
|
func (c *horizontalPodAutoscalers) UpdateStatus(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (result *extensions.HorizontalPodAutoscaler, err error) {
|
||||||
|
result = &extensions.HorizontalPodAutoscaler{}
|
||||||
|
err = c.client.Put().Namespace(c.ns).Resource("horizontalPodAutoscalers").Name(horizontalPodAutoscaler.Name).SubResource("status").Body(horizontalPodAutoscaler).Do().Into(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Watch returns a watch.Interface that watches the requested horizontalPodAutoscalers.
|
// Watch returns a watch.Interface that watches the requested horizontalPodAutoscalers.
|
||||||
func (c *horizontalPodAutoscalers) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
|
func (c *horizontalPodAutoscalers) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
|
||||||
return c.client.Get().
|
return c.client.Get().
|
||||||
|
@ -120,6 +120,23 @@ func TestHorizontalPodAutoscalerUpdate(t *testing.T) {
|
|||||||
c.Validate(t, response, err)
|
c.Validate(t, response, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHorizontalPodAutoscalerUpdateStatus(t *testing.T) {
|
||||||
|
ns := api.NamespaceDefault
|
||||||
|
horizontalPodAutoscaler := &extensions.HorizontalPodAutoscaler{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc",
|
||||||
|
Namespace: ns,
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c := &testClient{
|
||||||
|
Request: testRequest{Method: "PUT", Path: testapi.Extensions.ResourcePath(getHorizontalPodAutoscalersResoureName(), ns, "abc") + "/status", Query: buildQueryValues(nil)},
|
||||||
|
Response: Response{StatusCode: 200, Body: horizontalPodAutoscaler},
|
||||||
|
}
|
||||||
|
response, err := c.Setup(t).Experimental().HorizontalPodAutoscalers(ns).UpdateStatus(horizontalPodAutoscaler)
|
||||||
|
c.Validate(t, response, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHorizontalPodAutoscalerDelete(t *testing.T) {
|
func TestHorizontalPodAutoscalerDelete(t *testing.T) {
|
||||||
ns := api.NamespaceDefault
|
ns := api.NamespaceDefault
|
||||||
c := &testClient{
|
c := &testClient{
|
||||||
|
@ -72,6 +72,14 @@ func (c *FakeHorizontalPodAutoscalers) Update(a *extensions.HorizontalPodAutosca
|
|||||||
return obj.(*extensions.HorizontalPodAutoscaler), err
|
return obj.(*extensions.HorizontalPodAutoscaler), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *FakeHorizontalPodAutoscalers) UpdateStatus(a *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error) {
|
||||||
|
obj, err := c.Fake.Invokes(NewUpdateSubresourceAction("horizontalpodautoscalers", "status", c.Namespace, a), &extensions.HorizontalPodAutoscaler{})
|
||||||
|
if obj == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return obj.(*extensions.HorizontalPodAutoscaler), err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *FakeHorizontalPodAutoscalers) Delete(name string, options *api.DeleteOptions) error {
|
func (c *FakeHorizontalPodAutoscalers) Delete(name string, options *api.DeleteOptions) error {
|
||||||
_, err := c.Fake.Invokes(NewDeleteAction("horizontalpodautoscalers", c.Namespace, name), &extensions.HorizontalPodAutoscaler{})
|
_, err := c.Fake.Invokes(NewDeleteAction("horizontalpodautoscalers", c.Namespace, name), &extensions.HorizontalPodAutoscaler{})
|
||||||
return err
|
return err
|
||||||
|
@ -147,7 +147,7 @@ func (a *HorizontalController) reconcileAutoscaler(hpa extensions.HorizontalPodA
|
|||||||
hpa.Status.LastScaleTimestamp = &now
|
hpa.Status.LastScaleTimestamp = &now
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = a.client.Experimental().HorizontalPodAutoscalers(hpa.Namespace).Update(&hpa)
|
_, err = a.client.Experimental().HorizontalPodAutoscalers(hpa.Namespace).UpdateStatus(&hpa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.eventRecorder.Event(&hpa, "FailedUpdateStatus", err.Error())
|
a.eventRecorder.Event(&hpa, "FailedUpdateStatus", err.Error())
|
||||||
return fmt.Errorf("failed to update status for %s: %v", hpa.Name, err)
|
return fmt.Errorf("failed to update status for %s: %v", hpa.Name, err)
|
||||||
|
@ -1042,7 +1042,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion {
|
|||||||
dbClient := func(resource string) storage.Interface {
|
dbClient := func(resource string) storage.Interface {
|
||||||
return c.StorageDestinations.get("extensions", resource)
|
return c.StorageDestinations.get("extensions", resource)
|
||||||
}
|
}
|
||||||
autoscalerStorage := horizontalpodautoscaleretcd.NewREST(dbClient("horizonalpodautoscalers"))
|
autoscalerStorage, autoscalerStatusStorage := horizontalpodautoscaleretcd.NewREST(dbClient("horizonalpodautoscalers"))
|
||||||
thirdPartyResourceStorage := thirdpartyresourceetcd.NewREST(dbClient("thirdpartyresources"))
|
thirdPartyResourceStorage := thirdpartyresourceetcd.NewREST(dbClient("thirdpartyresources"))
|
||||||
daemonSetStorage, daemonSetStatusStorage := daemonetcd.NewREST(dbClient("daemonsets"))
|
daemonSetStorage, daemonSetStatusStorage := daemonetcd.NewREST(dbClient("daemonsets"))
|
||||||
deploymentStorage := deploymentetcd.NewStorage(dbClient("deployments"))
|
deploymentStorage := deploymentetcd.NewStorage(dbClient("deployments"))
|
||||||
@ -1061,17 +1061,18 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion {
|
|||||||
}, 10*time.Second)
|
}, 10*time.Second)
|
||||||
}()
|
}()
|
||||||
storage := map[string]rest.Storage{
|
storage := map[string]rest.Storage{
|
||||||
strings.ToLower("replicationControllers"): controllerStorage.ReplicationController,
|
strings.ToLower("replicationControllers"): controllerStorage.ReplicationController,
|
||||||
strings.ToLower("replicationControllers/scale"): controllerStorage.Scale,
|
strings.ToLower("replicationControllers/scale"): controllerStorage.Scale,
|
||||||
strings.ToLower("horizontalpodautoscalers"): autoscalerStorage,
|
strings.ToLower("horizontalpodautoscalers"): autoscalerStorage,
|
||||||
strings.ToLower("thirdpartyresources"): thirdPartyResourceStorage,
|
strings.ToLower("horizontalpodautoscalers/status"): autoscalerStatusStorage,
|
||||||
strings.ToLower("daemonsets"): daemonSetStorage,
|
strings.ToLower("thirdpartyresources"): thirdPartyResourceStorage,
|
||||||
strings.ToLower("daemonsets/status"): daemonSetStatusStorage,
|
strings.ToLower("daemonsets"): daemonSetStorage,
|
||||||
strings.ToLower("deployments"): deploymentStorage.Deployment,
|
strings.ToLower("daemonsets/status"): daemonSetStatusStorage,
|
||||||
strings.ToLower("deployments/scale"): deploymentStorage.Scale,
|
strings.ToLower("deployments"): deploymentStorage.Deployment,
|
||||||
strings.ToLower("jobs"): jobStorage,
|
strings.ToLower("deployments/scale"): deploymentStorage.Scale,
|
||||||
strings.ToLower("jobs/status"): jobStatusStorage,
|
strings.ToLower("jobs"): jobStorage,
|
||||||
strings.ToLower("ingress"): ingressStorage,
|
strings.ToLower("jobs/status"): jobStatusStorage,
|
||||||
|
strings.ToLower("ingress"): ingressStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
expMeta := latest.GroupOrDie("extensions")
|
expMeta := latest.GroupOrDie("extensions")
|
||||||
|
@ -33,7 +33,7 @@ type REST struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewREST returns a RESTStorage object that will work against horizontal pod autoscalers.
|
// NewREST returns a RESTStorage object that will work against horizontal pod autoscalers.
|
||||||
func NewREST(s storage.Interface) *REST {
|
func NewREST(s storage.Interface) (*REST, *StatusREST) {
|
||||||
prefix := "/horizontalpodautoscalers"
|
prefix := "/horizontalpodautoscalers"
|
||||||
store := &etcdgeneric.Etcd{
|
store := &etcdgeneric.Etcd{
|
||||||
NewFunc: func() runtime.Object { return &extensions.HorizontalPodAutoscaler{} },
|
NewFunc: func() runtime.Object { return &extensions.HorizontalPodAutoscaler{} },
|
||||||
@ -67,5 +67,21 @@ func NewREST(s storage.Interface) *REST {
|
|||||||
|
|
||||||
Storage: s,
|
Storage: s,
|
||||||
}
|
}
|
||||||
return &REST{store}
|
statusStore := *store
|
||||||
|
statusStore.UpdateStrategy = horizontalpodautoscaler.StatusStrategy
|
||||||
|
return &REST{store}, &StatusREST{store: &statusStore}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusREST implements the REST endpoint for changing the status of a daemonset
|
||||||
|
type StatusREST struct {
|
||||||
|
store *etcdgeneric.Etcd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusREST) New() runtime.Object {
|
||||||
|
return &extensions.HorizontalPodAutoscaler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update alters the status subset of an object.
|
||||||
|
func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) {
|
||||||
|
return r.store.Update(ctx, obj)
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,10 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/tools"
|
"k8s.io/kubernetes/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient) {
|
func newStorage(t *testing.T) (*REST, *StatusREST, *tools.FakeEtcdClient) {
|
||||||
etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "extensions")
|
etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "extensions")
|
||||||
return NewREST(etcdStorage), fakeClient
|
storage, statusStorage := NewREST(etcdStorage)
|
||||||
|
return storage, statusStorage, fakeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func validNewHorizontalPodAutoscaler(name string) *extensions.HorizontalPodAutoscaler {
|
func validNewHorizontalPodAutoscaler(name string) *extensions.HorizontalPodAutoscaler {
|
||||||
@ -54,7 +55,7 @@ func validNewHorizontalPodAutoscaler(name string) *extensions.HorizontalPodAutos
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCreate(t *testing.T) {
|
func TestCreate(t *testing.T) {
|
||||||
storage, fakeClient := newStorage(t)
|
storage, _, fakeClient := newStorage(t)
|
||||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||||
autoscaler := validNewHorizontalPodAutoscaler("foo")
|
autoscaler := validNewHorizontalPodAutoscaler("foo")
|
||||||
autoscaler.ObjectMeta = api.ObjectMeta{}
|
autoscaler.ObjectMeta = api.ObjectMeta{}
|
||||||
@ -67,7 +68,7 @@ func TestCreate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdate(t *testing.T) {
|
func TestUpdate(t *testing.T) {
|
||||||
storage, fakeClient := newStorage(t)
|
storage, _, fakeClient := newStorage(t)
|
||||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||||
test.TestUpdate(
|
test.TestUpdate(
|
||||||
// valid
|
// valid
|
||||||
@ -82,25 +83,25 @@ func TestUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDelete(t *testing.T) {
|
func TestDelete(t *testing.T) {
|
||||||
storage, fakeClient := newStorage(t)
|
storage, _, fakeClient := newStorage(t)
|
||||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||||
test.TestDelete(validNewHorizontalPodAutoscaler("foo"))
|
test.TestDelete(validNewHorizontalPodAutoscaler("foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
storage, fakeClient := newStorage(t)
|
storage, _, fakeClient := newStorage(t)
|
||||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||||
test.TestGet(validNewHorizontalPodAutoscaler("foo"))
|
test.TestGet(validNewHorizontalPodAutoscaler("foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestList(t *testing.T) {
|
func TestList(t *testing.T) {
|
||||||
storage, fakeClient := newStorage(t)
|
storage, _, fakeClient := newStorage(t)
|
||||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||||
test.TestList(validNewHorizontalPodAutoscaler("foo"))
|
test.TestList(validNewHorizontalPodAutoscaler("foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatch(t *testing.T) {
|
func TestWatch(t *testing.T) {
|
||||||
storage, fakeClient := newStorage(t)
|
storage, _, fakeClient := newStorage(t)
|
||||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||||
test.TestWatch(
|
test.TestWatch(
|
||||||
validNewHorizontalPodAutoscaler("foo"),
|
validNewHorizontalPodAutoscaler("foo"),
|
||||||
@ -119,3 +120,5 @@ func TestWatch(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO TestUpdateStatus
|
||||||
|
@ -46,7 +46,10 @@ func (autoscalerStrategy) NamespaceScoped() bool {
|
|||||||
|
|
||||||
// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
|
// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
|
||||||
func (autoscalerStrategy) PrepareForCreate(obj runtime.Object) {
|
func (autoscalerStrategy) PrepareForCreate(obj runtime.Object) {
|
||||||
_ = obj.(*extensions.HorizontalPodAutoscaler)
|
newHPA := obj.(*extensions.HorizontalPodAutoscaler)
|
||||||
|
|
||||||
|
// create cannot set status
|
||||||
|
newHPA.Status = extensions.HorizontalPodAutoscalerStatus{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates a new autoscaler.
|
// Validate validates a new autoscaler.
|
||||||
@ -62,7 +65,10 @@ func (autoscalerStrategy) AllowCreateOnUpdate() bool {
|
|||||||
|
|
||||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||||
func (autoscalerStrategy) PrepareForUpdate(obj, old runtime.Object) {
|
func (autoscalerStrategy) PrepareForUpdate(obj, old runtime.Object) {
|
||||||
_ = obj.(*extensions.HorizontalPodAutoscaler)
|
newHPA := obj.(*extensions.HorizontalPodAutoscaler)
|
||||||
|
oldHPA := obj.(*extensions.HorizontalPodAutoscaler)
|
||||||
|
// Update is not allowed to set status
|
||||||
|
newHPA.Status = oldHPA.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateUpdate is the default update validation for an end user.
|
// ValidateUpdate is the default update validation for an end user.
|
||||||
@ -91,3 +97,20 @@ func MatchAutoscaler(label labels.Selector, field fields.Selector) generic.Match
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type autoscalerStatusStrategy struct {
|
||||||
|
autoscalerStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
var StatusStrategy = autoscalerStatusStrategy{Strategy}
|
||||||
|
|
||||||
|
func (autoscalerStatusStrategy) PrepareForUpdate(obj, old runtime.Object) {
|
||||||
|
newAutoscaler := obj.(*extensions.HorizontalPodAutoscaler)
|
||||||
|
oldAutoscaler := old.(*extensions.HorizontalPodAutoscaler)
|
||||||
|
// status changes are not allowed to update spec
|
||||||
|
newAutoscaler.Spec = oldAutoscaler.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (autoscalerStatusStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) errs.ValidationErrorList {
|
||||||
|
return validation.ValidateHorizontalPodAutoscalerStatusUpdate(obj.(*extensions.HorizontalPodAutoscaler), old.(*extensions.HorizontalPodAutoscaler))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user