Commit b17b4a71 authored by James Fargher's avatar James Fargher

Merge branch 'network-policy-management' into 'master'

Network policy management endpoints

See merge request gitlab-org/gitlab!30911
parents f8d107e7 cd600060
......@@ -5,7 +5,7 @@ module Projects
class NetworkPoliciesController < Projects::ApplicationController
POLLING_INTERVAL = 5_000
before_action :authorize_read_network_policies!
before_action :authorize_read_threat_monitoring!
before_action :set_polling_interval, only: [:summary]
def summary
......@@ -33,6 +33,41 @@ module Projects
end
end
def index
response = NetworkPolicies::ResourcesService.new(environment: environment).execute
respond_with_service_response(response)
end
def create
policy = Gitlab::Kubernetes::NetworkPolicy.from_yaml(params[:manifest])
response = NetworkPolicies::DeployResourceService.new(
policy: policy,
environment: environment
).execute
respond_with_service_response(response)
end
def update
policy = Gitlab::Kubernetes::NetworkPolicy.from_yaml(params[:manifest])
response = NetworkPolicies::DeployResourceService.new(
resource_name: params[:id],
policy: policy,
environment: environment
).execute
respond_with_service_response(response)
end
def destroy
response = NetworkPolicies::DeleteResourceService.new(
resource_name: params[:id],
environment: environment
).execute
respond_with_service_response(response)
end
private
def parse_time(params, fallback)
......@@ -49,9 +84,18 @@ module Projects
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
end
def authorize_read_network_policies!
def authorize_read_threat_monitoring!
render_403 unless can?(current_user, :read_threat_monitoring, project)
end
def respond_with_service_response(response)
payload = response.success? ? response.payload : { error: response.message }
respond_to do |format|
format.json do
render status: response.http_status, json: payload
end
end
end
end
end
end
# frozen_string_literal: true
module NetworkPolicies
class DeleteResourceService
include NetworkPolicies::Responses
def initialize(resource_name:, environment:)
@resource_name = resource_name
@platform = environment.deployment_platform
@kubernetes_namespace = environment.deployment_namespace
end
def execute
return no_platform_response unless @platform
@platform.kubeclient.delete_network_policy(@resource_name, @kubernetes_namespace)
ServiceResponse.success
rescue Kubeclient::HttpError => e
kubernetes_error_response(e)
end
end
end
# frozen_string_literal: true
module NetworkPolicies
class DeployResourceService
include NetworkPolicies::Responses
def initialize(policy:, environment:, resource_name: nil)
@policy = policy
@platform = environment.deployment_platform
@kubernetes_namespace = environment.deployment_namespace
@resource_name = resource_name
end
def execute
return empty_resource_response unless policy
return no_platform_response unless platform
setup_resource
resource = deploy_resource
policy = Gitlab::Kubernetes::NetworkPolicy.from_resource(resource)
ServiceResponse.success(payload: policy)
rescue Kubeclient::HttpError => e
kubernetes_error_response(e)
end
private
attr_reader :platform, :policy, :resource_name, :resource, :kubernetes_namespace
def setup_resource
@resource = policy.generate
resource[:metadata][:namespace] = kubernetes_namespace
resource[:metadata][:name] = resource_name if resource_name
end
def deploy_resource
if resource_name
platform.kubeclient.update_network_policy(resource)
else
platform.kubeclient.create_network_policy(resource)
end
end
end
end
# frozen_string_literal: true
module NetworkPolicies
class ResourcesService
include NetworkPolicies::Responses
def initialize(environment:)
@platform = environment.deployment_platform
@kubernetes_namespace = environment.deployment_namespace
end
def execute
return no_platform_response unless @platform
policies = @platform.kubeclient
.get_network_policies(namespace: @kubernetes_namespace)
.map { |resource| Gitlab::Kubernetes::NetworkPolicy.from_resource(resource) }
ServiceResponse.success(payload: policies)
rescue Kubeclient::HttpError => e
kubernetes_error_response(e)
end
end
end
# frozen_string_literal: true
module NetworkPolicies
module Responses
def kubernetes_error_response(error)
ServiceResponse.error(
http_status: :bad_request,
message: s_('NetworkPolicies|Kubernetes error: %{error}') % { error: error }
)
end
def empty_resource_response
ServiceResponse.error(
http_status: :bad_request,
message: s_('NetworkPolicies|Invalid or empty policy')
)
end
def no_platform_response
ServiceResponse.error(
http_status: :bad_request,
message: s_('NetworkPolicies|Environment does not have deployment platform')
)
end
end
end
......@@ -64,7 +64,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :summary, on: :collection
end
resource :network_policies, only: [] do
resources :network_policies, only: [:index, :create, :update, :destroy] do
get :summary, on: :collection
end
......
......@@ -11,17 +11,32 @@ describe Projects::Security::NetworkPoliciesController do
let_it_be(:action_params) { { project_id: project, namespace_id: project.namespace, environment_id: environment } }
shared_examples 'CRUD service errors' do
context 'with a error service response' do
before do
allow(service).to receive(:execute) { ServiceResponse.error(http_status: :bad_request, message: 'error') }
end
it 'responds with bad_request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to eq('{"error":"error"}')
end
end
end
before do
stub_licensed_features(threat_monitoring: true)
sign_in(user)
end
describe 'GET #summary' do
subject { get :summary, params: action_params, format: :json }
let_it_be(:kubernetes_namespace) { environment.deployment_namespace }
before do
stub_licensed_features(threat_monitoring: true)
sign_in(user)
end
context 'with authorized user' do
before do
group.add_developer(user)
......@@ -122,4 +137,199 @@ describe Projects::Security::NetworkPoliciesController do
end
end
end
describe 'GET #index' do
subject { get :index, params: action_params, format: :json }
context 'with authorized user' do
let(:service) { instance_double('NetworkPolicies::ResourcesService', execute: ServiceResponse.success(payload: [policy])) }
let(:policy) do
Gitlab::Kubernetes::NetworkPolicy.new(
name: 'policy',
namespace: 'another',
pod_selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
)
end
before do
group.add_developer(user)
allow(NetworkPolicies::ResourcesService).to receive(:new).with(environment: environment) { service }
end
it 'responds with policies' do
subject
expect(response).to have_gitlab_http_status(:success)
expect(response.body).to eq([policy].to_json)
end
include_examples 'CRUD service errors'
end
context 'with unauthorized user' do
it 'returns unauthorized' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'POST #create' do
subject { post :create, params: action_params.merge(manifest: manifest), format: :json }
let(:service) { instance_double('NetworkPolicies::DeployResourceService', execute: ServiceResponse.success(payload: policy)) }
let(:policy) do
Gitlab::Kubernetes::NetworkPolicy.new(
name: 'policy',
namespace: 'another',
pod_selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
)
end
let(:manifest) do
<<~POLICY
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: example-name
namespace: example-namespace
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
project: myproject
POLICY
end
context 'with authorized user' do
before do
group.add_developer(user)
allow(NetworkPolicies::DeployResourceService).to(
receive(:new)
.with(policy: kind_of(Gitlab::Kubernetes::NetworkPolicy), environment: environment)
.and_return(service)
)
end
it 'responds with success' do
subject
expect(response).to have_gitlab_http_status(:success)
expect(response.body).to eq(policy.to_json)
end
include_examples 'CRUD service errors'
end
context 'with unauthorized user' do
it 'returns unauthorized' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'PUT #update' do
subject { put :update, params: action_params.merge(id: 'example-policy', manifest: manifest), format: :json }
let(:service) { instance_double('NetworkPolicies::DeployResourceService', execute: ServiceResponse.success(payload: policy)) }
let(:policy) do
Gitlab::Kubernetes::NetworkPolicy.new(
name: 'policy',
namespace: 'another',
pod_selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
)
end
let(:manifest) do
<<~POLICY
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: example-name
namespace: example-namespace
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
project: myproject
POLICY
end
context 'with authorized user' do
before do
group.add_developer(user)
allow(NetworkPolicies::DeployResourceService).to(
receive(:new)
.with(policy: kind_of(Gitlab::Kubernetes::NetworkPolicy), environment: environment, resource_name: 'example-policy')
.and_return(service)
)
end
it 'responds with success' do
subject
expect(response).to have_gitlab_http_status(:success)
expect(response.body).to eq(policy.to_json)
end
include_examples 'CRUD service errors'
end
context 'with unauthorized user' do
it 'returns unauthorized' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'DELETE #destroy' do
subject { delete :destroy, params: action_params.merge(id: 'example-policy'), format: :json }
let(:service) { instance_double('NetworkPolicies::DeleteResourceService', execute: ServiceResponse.success) }
context 'with authorized user' do
before do
group.add_developer(user)
allow(NetworkPolicies::DeleteResourceService).to(
receive(:new)
.with(environment: environment, resource_name: 'example-policy')
.and_return(service)
)
end
it 'responds with success' do
subject
expect(response).to have_gitlab_http_status(:success)
end
include_examples 'CRUD service errors'
end
context 'with unauthorized user' do
it 'returns unauthorized' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe NetworkPolicies::DeleteResourceService do
let(:service) { NetworkPolicies::DeleteResourceService.new(resource_name: 'policy', environment: environment) }
let(:environment) { instance_double('Environment', deployment_platform: platform, deployment_namespace: 'namespace') }
let(:platform) { instance_double('Clusters::Platforms::Kubernetes', kubeclient: kubeclient) }
let(:kubeclient) { double('Kubeclient::Client') }
describe '#execute' do
subject { service.execute }
it 'deletes resource from the deployment namespace and returns success response' do
expect(kubeclient).to receive(:delete_network_policy).with('policy', environment.deployment_namespace)
expect(subject).to be_success
end
context 'without deployment_platform' do
let(:platform) { nil }
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
context 'with Kubeclient::HttpError' do
before do
allow(kubeclient).to receive(:delete_network_policy).and_raise(Kubeclient::HttpError.new(500, 'system failure', nil))
end
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe NetworkPolicies::DeployResourceService do
let(:service) { NetworkPolicies::DeployResourceService.new(policy: policy, environment: environment) }
let(:environment) { instance_double('Environment', deployment_platform: platform, deployment_namespace: 'namespace') }
let(:platform) { instance_double('Clusters::Platforms::Kubernetes', kubeclient: kubeclient) }
let(:kubeclient) { double('Kubeclient::Client') }
let(:policy) do
Gitlab::Kubernetes::NetworkPolicy.new(
name: 'policy',
namespace: 'another',
pod_selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
)
end
describe '#execute' do
subject { service.execute }
it 'creates resource in the deployment namespace and return success response with a policy' do
namespaced_policy = policy.generate
namespaced_policy[:metadata][:namespace] = environment.deployment_namespace
expect(kubeclient).to receive(:create_network_policy).with(namespaced_policy) { policy.generate }
expect(subject).to be_success
expect(subject.payload.as_json).to eq(policy.as_json)
end
context 'with resource_name' do
let(:service) { NetworkPolicies::DeployResourceService.new(policy: policy, environment: environment, resource_name: 'policy2') }
it 'updates resource in the deployment namespace and returns success response with a policy' do
namespaced_policy = policy.generate
namespaced_policy[:metadata][:namespace] = environment.deployment_namespace
namespaced_policy[:metadata][:name] = 'policy2'
expect(kubeclient).to receive(:update_network_policy).with(namespaced_policy) { policy.generate }
expect(subject).to be_success
expect(subject.payload.as_json).to eq(policy.as_json)
end
end
context 'without policy' do
let(:policy) { nil }
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
context 'without deployment_platform' do
let(:platform) { nil }
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
context 'with Kubeclient::HttpError' do
before do
allow(kubeclient).to receive(:create_network_policy).and_raise(Kubeclient::HttpError.new(500, 'system failure', nil))
end
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe NetworkPolicies::ResourcesService do
let(:service) { NetworkPolicies::ResourcesService.new(environment: environment) }
let(:environment) { instance_double('Environment', deployment_platform: platform, deployment_namespace: 'namespace') }
let(:platform) { instance_double('Clusters::Platforms::Kubernetes', kubeclient: kubeclient) }
let(:kubeclient) { double('Kubeclient::Client') }
let(:policy) do
Gitlab::Kubernetes::NetworkPolicy.new(
name: 'policy',
namespace: 'another',
pod_selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
)
end
describe '#execute' do
subject { service.execute }
it 'returns success response with policies from the deployment namespace' do
expect(kubeclient).to receive(:get_network_policies).with(namespace: environment.deployment_namespace) { [policy.generate] }
expect(subject).to be_success
expect(subject.payload.count).to eq(1)
expect(subject.payload.first.as_json).to eq(policy.as_json)
end
context 'without deployment_platform' do
let(:platform) { nil }
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
context 'with Kubeclient::HttpError' do
before do
allow(kubeclient).to receive(:get_network_policies).and_raise(Kubeclient::HttpError.new(500, 'system failure', nil))
end
it 'returns error response' do
expect(subject).to be_error
expect(subject.http_status).to eq(:bad_request)
expect(subject.message).not_to be_nil
end
end
end
end
......@@ -84,7 +84,7 @@ module Gitlab
end
def manifest
YAML.dump(metadata: metadata, spec: spec)
YAML.dump({ metadata: metadata, spec: spec }.deep_stringify_keys)
end
end
end
......
......@@ -14049,6 +14049,15 @@ msgstr ""
msgid "Network"
msgstr ""
msgid "NetworkPolicies|Environment does not have deployment platform"
msgstr ""
msgid "NetworkPolicies|Invalid or empty policy"
msgstr ""
msgid "NetworkPolicies|Kubernetes error: %{error}"
msgstr ""
msgid "NetworkPolicies|Something went wrong, unable to fetch policies"
msgstr ""
......
......@@ -212,7 +212,7 @@ spec:
{
metadata: { name: name, namespace: namespace },
spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
}
}.deep_stringify_keys
)
}
end
......
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