Commit 9a563b1b authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'retryable_create_or_update_kubernetes_namespace' into 'master'

Update K8s project namespace and ServiceAccount if exist

See merge request gitlab-org/gitlab-ce!23525
parents b53ebd93 c0c75c80
...@@ -12,7 +12,8 @@ module Clusters ...@@ -12,7 +12,8 @@ module Clusters
create_gitlab_service_account! create_gitlab_service_account!
configure_kubernetes configure_kubernetes
cluster.save! cluster.save!
configure_project_service_account
ClusterPlatformConfigureWorker.perform_async(cluster.id)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
...@@ -25,7 +26,7 @@ module Clusters ...@@ -25,7 +26,7 @@ module Clusters
private private
def create_gitlab_service_account! def create_gitlab_service_account!
Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator( Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator(
kube_client, kube_client,
rbac: create_rbac_cluster? rbac: create_rbac_cluster?
).execute ).execute
...@@ -55,15 +56,6 @@ module Clusters ...@@ -55,15 +56,6 @@ module Clusters
).execute ).execute
end end
def configure_project_service_account
kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: cluster,
kubernetes_namespace: kubernetes_namespace
).execute
end
def authorization_type def authorization_type
create_rbac_cluster? ? 'rbac' : 'abac' create_rbac_cluster? ? 'rbac' : 'abac'
end end
......
...@@ -27,7 +27,7 @@ module Clusters ...@@ -27,7 +27,7 @@ module Clusters
end end
def create_project_service_account def create_project_service_account
Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator( Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator(
platform.kubeclient, platform.kubeclient,
service_account_name: kubernetes_namespace.service_account_name, service_account_name: kubernetes_namespace.service_account_name,
service_account_namespace: kubernetes_namespace.namespace, service_account_namespace: kubernetes_namespace.namespace,
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Clusters module Clusters
module Gcp module Gcp
module Kubernetes module Kubernetes
class CreateServiceAccountService class CreateOrUpdateServiceAccountService
def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil) def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
@kubeclient = kubeclient @kubeclient = kubeclient
@service_account_name = service_account_name @service_account_name = service_account_name
...@@ -38,8 +38,9 @@ module Clusters ...@@ -38,8 +38,9 @@ module Clusters
def execute def execute
ensure_project_namespace_exists if namespace_creator ensure_project_namespace_exists if namespace_creator
kubeclient.create_service_account(service_account_resource)
kubeclient.create_secret(service_account_token_resource) kubeclient.create_or_update_service_account(service_account_resource)
kubeclient.create_or_update_secret(service_account_token_resource)
create_role_or_cluster_role_binding if rbac create_role_or_cluster_role_binding if rbac
end end
...@@ -56,9 +57,9 @@ module Clusters ...@@ -56,9 +57,9 @@ module Clusters
def create_role_or_cluster_role_binding def create_role_or_cluster_role_binding
if namespace_creator if namespace_creator
kubeclient.create_role_binding(role_binding_resource) kubeclient.create_or_update_role_binding(role_binding_resource)
else else
kubeclient.create_cluster_role_binding(cluster_role_binding_resource) kubeclient.create_or_update_cluster_role_binding(cluster_role_binding_resource)
end end
end end
......
---
title: Updates service to update Kubernetes project namespaces and restricted service
account if present
merge_request: 23525
author:
type: changed
...@@ -46,6 +46,7 @@ module Gitlab ...@@ -46,6 +46,7 @@ module Gitlab
:create_secret, :create_secret,
:create_service_account, :create_service_account,
:update_config_map, :update_config_map,
:update_secret,
:update_service_account, :update_service_account,
to: :core_client to: :core_client
...@@ -80,8 +81,64 @@ module Gitlab ...@@ -80,8 +81,64 @@ module Gitlab
@kubeclient_options = kubeclient_options @kubeclient_options = kubeclient_options
end end
def create_or_update_cluster_role_binding(resource)
if cluster_role_binding_exists?(resource)
update_cluster_role_binding(resource)
else
create_cluster_role_binding(resource)
end
end
def create_or_update_role_binding(resource)
if role_binding_exists?(resource)
update_role_binding(resource)
else
create_role_binding(resource)
end
end
def create_or_update_service_account(resource)
if service_account_exists?(resource)
update_service_account(resource)
else
create_service_account(resource)
end
end
def create_or_update_secret(resource)
if secret_exists?(resource)
update_secret(resource)
else
create_secret(resource)
end
end
private private
def cluster_role_binding_exists?(resource)
get_cluster_role_binding(resource.metadata.name)
rescue ::Kubeclient::ResourceNotFoundError
false
end
def role_binding_exists?(resource)
get_role_binding(resource.metadata.name, resource.metadata.namespace)
rescue ::Kubeclient::ResourceNotFoundError
false
end
def service_account_exists?(resource)
get_service_account(resource.metadata.name, resource.metadata.namespace)
rescue ::Kubeclient::ResourceNotFoundError
false
end
def secret_exists?(resource)
get_secret(resource.metadata.name, resource.metadata.namespace)
rescue ::Kubeclient::ResourceNotFoundError
false
end
def build_kubeclient(api_group, api_version) def build_kubeclient(api_group, api_version)
::Kubeclient::Client.new( ::Kubeclient::Client.new(
join_api_url(api_prefix, api_group), join_api_url(api_prefix, api_group),
......
...@@ -99,6 +99,7 @@ describe Gitlab::Kubernetes::KubeClient do ...@@ -99,6 +99,7 @@ describe Gitlab::Kubernetes::KubeClient do
:create_secret, :create_secret,
:create_service_account, :create_service_account,
:update_config_map, :update_config_map,
:update_secret,
:update_service_account :update_service_account
].each do |method| ].each do |method|
describe "##{method}" do describe "##{method}" do
...@@ -174,6 +175,84 @@ describe Gitlab::Kubernetes::KubeClient do ...@@ -174,6 +175,84 @@ describe Gitlab::Kubernetes::KubeClient do
end end
end end
shared_examples 'create_or_update method' do
let(:get_method) { "get_#{resource_type}" }
let(:update_method) { "update_#{resource_type}" }
let(:create_method) { "create_#{resource_type}" }
context 'resource exists' do
before do
expect(client).to receive(get_method).and_return(resource)
end
it 'calls the update method' do
expect(client).to receive(update_method).with(resource)
subject
end
end
context 'resource does not exist' do
before do
expect(client).to receive(get_method).and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
end
it 'calls the create method' do
expect(client).to receive(create_method).with(resource)
subject
end
end
end
describe '#create_or_update_cluster_role_binding' do
let(:resource_type) { 'cluster_role_binding' }
let(:resource) do
::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
end
subject { client.create_or_update_cluster_role_binding(resource) }
it_behaves_like 'create_or_update method'
end
describe '#create_or_update_role_binding' do
let(:resource_type) { 'role_binding' }
let(:resource) do
::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
end
subject { client.create_or_update_role_binding(resource) }
it_behaves_like 'create_or_update method'
end
describe '#create_or_update_service_account' do
let(:resource_type) { 'service_account' }
let(:resource) do
::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
end
subject { client.create_or_update_service_account(resource) }
it_behaves_like 'create_or_update method'
end
describe '#create_or_update_secret' do
let(:resource_type) { 'secret' }
let(:resource) do
::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
end
subject { client.create_or_update_secret(resource) }
it_behaves_like 'create_or_update method'
end
describe 'methods that do not exist on any client' do describe 'methods that do not exist on any client' do
it 'throws an error' do it 'throws an error' do
expect { client.non_existent_method }.to raise_error(NoMethodError) expect { client.non_existent_method }.to raise_error(NoMethodError)
......
...@@ -19,6 +19,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do ...@@ -19,6 +19,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
subject { described_class.new.execute(provider) } subject { described_class.new.execute(provider) }
before do
allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
end
shared_examples 'success' do shared_examples 'success' do
it 'configures provider and kubernetes' do it 'configures provider and kubernetes' do
subject subject
...@@ -39,14 +43,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do ...@@ -39,14 +43,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
expect(platform.token).to eq(token) expect(platform.token).to eq(token)
end end
it 'creates kubernetes namespace model' do it 'calls ClusterPlatformConfigureWorker in a ascync fashion' do
subject expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id)
kubernetes_namespace = cluster.reload.kubernetes_namespace subject
expect(kubernetes_namespace).to be_persisted
expect(kubernetes_namespace.namespace).to eq(namespace)
expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
expect(kubernetes_namespace.service_account_token).to be_present
end end
end end
...@@ -104,8 +104,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do ...@@ -104,8 +104,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
stub_kubeclient_discover(api_url) stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url) stub_kubeclient_get_namespace(api_url)
stub_kubeclient_create_namespace(api_url) stub_kubeclient_create_namespace(api_url)
stub_kubeclient_get_service_account_error(api_url, 'gitlab')
stub_kubeclient_create_service_account(api_url) stub_kubeclient_create_service_account(api_url)
stub_kubeclient_create_secret(api_url) stub_kubeclient_create_secret(api_url)
stub_kubeclient_put_secret(api_url, 'gitlab-token')
stub_kubeclient_get_secret( stub_kubeclient_get_secret(
api_url, api_url,
...@@ -115,19 +117,6 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do ...@@ -115,19 +117,6 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
namespace: 'default' namespace: 'default'
} }
) )
stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_create_service_account(api_url, namespace: namespace)
stub_kubeclient_create_secret(api_url, namespace: namespace)
stub_kubeclient_get_secret(
api_url,
{
metadata_name: "#{namespace}-token",
token: Base64.encode64(token),
namespace: namespace
}
)
end end
end end
...@@ -155,8 +144,8 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do ...@@ -155,8 +144,8 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
before do before do
provider.legacy_abac = false provider.legacy_abac = false
stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin')
stub_kubeclient_create_cluster_role_binding(api_url) stub_kubeclient_create_cluster_role_binding(api_url)
stub_kubeclient_create_role_binding(api_url, namespace: namespace)
end end
include_context 'kubernetes information successfully fetched' include_context 'kubernetes information successfully fetched'
......
...@@ -10,6 +10,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -10,6 +10,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
let(:api_url) { 'https://kubernetes.example.com' } let(:api_url) { 'https://kubernetes.example.com' }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:cluster_project) { cluster.cluster_project } let(:cluster_project) { cluster.cluster_project }
let(:namespace) { "#{project.path}-#{project.id}" }
subject do subject do
described_class.new( described_class.new(
...@@ -18,16 +19,19 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -18,16 +19,19 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
).execute ).execute
end end
shared_context 'kubernetes requests' do
before do before do
stub_kubeclient_discover(api_url) stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url) stub_kubeclient_get_namespace(api_url)
stub_kubeclient_get_service_account_error(api_url, 'gitlab')
stub_kubeclient_create_service_account(api_url) stub_kubeclient_create_service_account(api_url)
stub_kubeclient_get_secret_error(api_url, 'gitlab-token')
stub_kubeclient_create_secret(api_url) stub_kubeclient_create_secret(api_url)
stub_kubeclient_get_namespace(api_url, namespace: namespace) stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_get_service_account_error(api_url, "#{namespace}-service-account", namespace: namespace)
stub_kubeclient_create_service_account(api_url, namespace: namespace) stub_kubeclient_create_service_account(api_url, namespace: namespace)
stub_kubeclient_create_secret(api_url, namespace: namespace) stub_kubeclient_create_secret(api_url, namespace: namespace)
stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace)
stub_kubeclient_get_secret( stub_kubeclient_get_secret(
api_url, api_url,
...@@ -38,20 +42,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -38,20 +42,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
} }
) )
end end
end
context 'when kubernetes namespace is not persisted' do
let(:namespace) { "#{project.path}-#{project.id}" }
let(:kubernetes_namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster,
project: cluster_project.project,
cluster_project: cluster_project)
end
include_context 'kubernetes requests'
shared_examples 'successful creation of kubernetes namespace' do
it 'creates a Clusters::KubernetesNamespace' do it 'creates a Clusters::KubernetesNamespace' do
expect do expect do
subject subject
...@@ -59,7 +51,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -59,7 +51,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
end end
it 'creates project service account' do it 'creates project service account' do
expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once
subject subject
end end
...@@ -74,6 +66,34 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -74,6 +66,34 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
end end
end end
context 'group clusters' do
let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
let(:group) { cluster.group }
let(:project) { create(:project, group: group) }
context 'when kubernetes namespace is not persisted' do
let(:kubernetes_namespace) do
build(:cluster_kubernetes_namespace,
cluster: cluster,
project: project)
end
it_behaves_like 'successful creation of kubernetes namespace'
end
end
context 'project clusters' do
context 'when kubernetes namespace is not persisted' do
let(:kubernetes_namespace) do
build(:cluster_kubernetes_namespace,
cluster: cluster,
project: cluster_project.project,
cluster_project: cluster_project)
end
it_behaves_like 'successful creation of kubernetes namespace'
end
context 'when there is a Kubernetes Namespace associated' do context 'when there is a Kubernetes Namespace associated' do
let(:namespace) { 'new-namespace' } let(:namespace) { 'new-namespace' }
...@@ -84,8 +104,6 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -84,8 +104,6 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
cluster_project: cluster_project) cluster_project: cluster_project)
end end
include_context 'kubernetes requests'
before do before do
platform.update_column(:namespace, 'new-namespace') platform.update_column(:namespace, 'new-namespace')
end end
...@@ -97,7 +115,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -97,7 +115,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
end end
it 'creates project service account' do it 'creates project service account' do
expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once
subject subject
end end
...@@ -112,4 +130,5 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -112,4 +130,5 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
expect(kubernetes_namespace.encrypted_service_account_token).to be_present expect(kubernetes_namespace.encrypted_service_account_token).to be_present
end end
end end
end
end end
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do
include KubernetesHelpers include KubernetesHelpers
let(:api_url) { 'http://111.111.111.111' } let(:api_url) { 'http://111.111.111.111' }
...@@ -55,7 +55,11 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do ...@@ -55,7 +55,11 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
before do before do
stub_kubeclient_discover(api_url) stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url, namespace: namespace) stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_create_service_account(api_url, namespace: namespace )
stub_kubeclient_get_service_account_error(api_url, service_account_name, namespace: namespace)
stub_kubeclient_create_service_account(api_url, namespace: namespace)
stub_kubeclient_get_secret_error(api_url, token_name, namespace: namespace)
stub_kubeclient_create_secret(api_url, namespace: namespace) stub_kubeclient_create_secret(api_url, namespace: namespace)
end end
...@@ -74,10 +78,12 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do ...@@ -74,10 +78,12 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
context 'with RBAC cluster' do context 'with RBAC cluster' do
let(:rbac) { true } let(:rbac) { true }
let(:cluster_role_binding_name) { 'gitlab-admin' }
before do before do
cluster.platform_kubernetes.rbac! cluster.platform_kubernetes.rbac!
stub_kubeclient_get_cluster_role_binding_error(api_url, cluster_role_binding_name)
stub_kubeclient_create_cluster_role_binding(api_url) stub_kubeclient_create_cluster_role_binding(api_url)
end end
...@@ -130,10 +136,12 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do ...@@ -130,10 +136,12 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
context 'With RBAC enabled cluster' do context 'With RBAC enabled cluster' do
let(:rbac) { true } let(:rbac) { true }
let(:role_binding_name) { "gitlab-#{namespace}"}
before do before do
cluster.platform_kubernetes.rbac! cluster.platform_kubernetes.rbac!
stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace)
stub_kubeclient_create_role_binding(api_url, namespace: namespace) stub_kubeclient_create_role_binding(api_url, namespace: namespace)
end end
......
...@@ -47,6 +47,11 @@ module KubernetesHelpers ...@@ -47,6 +47,11 @@ module KubernetesHelpers
.to_return(status: [status, "Internal Server Error"]) .to_return(status: [status, "Internal Server Error"])
end end
def stub_kubeclient_get_service_account_error(api_url, name, namespace: 'default', status: 404)
WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts/#{name}")
.to_return(status: [status, "Internal Server Error"])
end
def stub_kubeclient_create_service_account(api_url, namespace: 'default') def stub_kubeclient_create_service_account(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts") WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts")
.to_return(kube_response({})) .to_return(kube_response({}))
...@@ -62,11 +67,26 @@ module KubernetesHelpers ...@@ -62,11 +67,26 @@ module KubernetesHelpers
.to_return(kube_response({})) .to_return(kube_response({}))
end end
def stub_kubeclient_put_secret(api_url, name, namespace: 'default')
WebMock.stub_request(:put, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{name}")
.to_return(kube_response({}))
end
def stub_kubeclient_get_cluster_role_binding_error(api_url, name, status: 404)
WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
.to_return(status: [status, "Internal Server Error"])
end
def stub_kubeclient_create_cluster_role_binding(api_url) def stub_kubeclient_create_cluster_role_binding(api_url)
WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings') WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings')
.to_return(kube_response({})) .to_return(kube_response({}))
end end
def stub_kubeclient_get_role_binding_error(api_url, name, namespace: 'default', status: 404)
WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
.to_return(status: [status, "Internal Server Error"])
end
def stub_kubeclient_create_role_binding(api_url, namespace: 'default') def stub_kubeclient_create_role_binding(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings") WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings")
.to_return(kube_response({})) .to_return(kube_response({}))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment