diff --git a/api/v1beta2/cryostat_types.go b/api/v1beta2/cryostat_types.go index ed006a98e..d17766a32 100644 --- a/api/v1beta2/cryostat_types.go +++ b/api/v1beta2/cryostat_types.go @@ -53,6 +53,10 @@ type CryostatSpec struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec ServiceOptions *ServiceConfigList `json:"serviceOptions,omitempty"` + // Options to customize the NetworkPolicy objects created for Cryostat's various Services. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + NetworkPolicies *NetworkPoliciesList `json:"networkPolicies,omitempty"` // Options to control how the operator exposes the application outside of the cluster, // such as using an Ingress or Route. // +optional @@ -372,6 +376,30 @@ type ServiceConfigList struct { AgentConfig *AgentServiceConfig `json:"agentConfig,omitempty"` } +// NetworkPoliciesList holds the configurations for NetworkPolicy +// objects for each service created by the operator. +type NetworkPoliciesList struct { + // NetworkPolicy configuration for the Cryostat application service. + // +optional + CoreConfig *NetworkPolicyConfig `json:"coreConfig,omitempty"` + // NetworkPolicy configuration for the cryostat-reports service. + // +optional + ReportsConfig *NetworkPolicyConfig `json:"reportsConfig,omitempty"` + // NetworkPolicy configuration for the database service. + // +optional + DatabaseConfig *NetworkPolicyConfig `json:"databaseConfig,omitempty"` + // NetworkPolicy configuration for the storage service. + // +optional + StorageConfig *NetworkPolicyConfig `json:"storageConfig,omitempty"` +} + +type NetworkPolicyConfig struct { + // Disable the NetworkPolicy for a given service. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Disable NetworkPolicy creation",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + Disabled *bool `json:"disabled,omitempty"` +} + // NetworkConfiguration provides customization for how to expose a Cryostat // service, so that it can be reached from outside the cluster. // On OpenShift, a Route is created by default. On Kubernetes, an Ingress will diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index fc15eab0b..d15733b34 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -252,6 +252,11 @@ func (in *CryostatSpec) DeepCopyInto(out *CryostatSpec) { *out = new(ServiceConfigList) (*in).DeepCopyInto(*out) } + if in.NetworkPolicies != nil { + in, out := &in.NetworkPolicies, &out.NetworkPolicies + *out = new(NetworkPoliciesList) + (*in).DeepCopyInto(*out) + } if in.NetworkOptions != nil { in, out := &in.NetworkOptions, &out.NetworkOptions *out = new(NetworkConfigurationList) @@ -456,6 +461,61 @@ func (in *NetworkConfigurationList) DeepCopy() *NetworkConfigurationList { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkPoliciesList) DeepCopyInto(out *NetworkPoliciesList) { + *out = *in + if in.CoreConfig != nil { + in, out := &in.CoreConfig, &out.CoreConfig + *out = new(NetworkPolicyConfig) + (*in).DeepCopyInto(*out) + } + if in.ReportsConfig != nil { + in, out := &in.ReportsConfig, &out.ReportsConfig + *out = new(NetworkPolicyConfig) + (*in).DeepCopyInto(*out) + } + if in.DatabaseConfig != nil { + in, out := &in.DatabaseConfig, &out.DatabaseConfig + *out = new(NetworkPolicyConfig) + (*in).DeepCopyInto(*out) + } + if in.StorageConfig != nil { + in, out := &in.StorageConfig, &out.StorageConfig + *out = new(NetworkPolicyConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPoliciesList. +func (in *NetworkPoliciesList) DeepCopy() *NetworkPoliciesList { + if in == nil { + return nil + } + out := new(NetworkPoliciesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkPolicyConfig) DeepCopyInto(out *NetworkPolicyConfig) { + *out = *in + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicyConfig. +func (in *NetworkPolicyConfig) DeepCopy() *NetworkPolicyConfig { + if in == nil { + return nil + } + out := new(NetworkPolicyConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenShiftSSOConfig) DeepCopyInto(out *OpenShiftSSOConfig) { *out = *in diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 87649dca7..a02688f75 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -13,6 +13,7 @@ metadata: "spec": { "enableCertManager": true, "eventTemplates": [], + "networkPolicies": {}, "reportOptions": { "replicas": 0 }, @@ -30,7 +31,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:4.0.0-dev - createdAt: "2025-01-14T19:21:36Z" + createdAt: "2025-01-16T17:07:46Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { @@ -160,6 +161,29 @@ spec: - description: Labels to add to the Ingress or Route during its creation. The label with key "app" is reserved for use by the operator. displayName: Labels path: networkOptions.coreConfig.labels + - description: Options to customize the NetworkPolicy objects created for Cryostat's various Services. + displayName: Network Policies + path: networkPolicies + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.coreConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.databaseConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.reportsConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.storageConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch - description: Options to configure the Cryostat deployments and pods metadata displayName: Operand metadata path: operandMetadata @@ -916,6 +940,7 @@ spec: - networking.k8s.io resources: - ingresses + - networkpolicies verbs: - '*' - apiGroups: diff --git a/bundle/manifests/operator.cryostat.io_cryostats.yaml b/bundle/manifests/operator.cryostat.io_cryostats.yaml index a62d2e5e2..a6108d58d 100644 --- a/bundle/manifests/operator.cryostat.io_cryostats.yaml +++ b/bundle/manifests/operator.cryostat.io_cryostats.yaml @@ -5794,6 +5794,41 @@ spec: type: object type: object type: object + networkPolicies: + description: Options to customize the NetworkPolicy objects created + for Cryostat's various Services. + properties: + coreConfig: + description: NetworkPolicy configuration for the Cryostat application + service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + databaseConfig: + description: NetworkPolicy configuration for the database service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + reportsConfig: + description: NetworkPolicy configuration for the cryostat-reports + service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + storageConfig: + description: NetworkPolicy configuration for the storage service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + type: object operandMetadata: description: Options to configure the Cryostat deployments and pods metadata diff --git a/config/crd/bases/operator.cryostat.io_cryostats.yaml b/config/crd/bases/operator.cryostat.io_cryostats.yaml index 6337f8bb3..1b4bf12d4 100644 --- a/config/crd/bases/operator.cryostat.io_cryostats.yaml +++ b/config/crd/bases/operator.cryostat.io_cryostats.yaml @@ -5781,6 +5781,41 @@ spec: type: object type: object type: object + networkPolicies: + description: Options to customize the NetworkPolicy objects created + for Cryostat's various Services. + properties: + coreConfig: + description: NetworkPolicy configuration for the Cryostat application + service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + databaseConfig: + description: NetworkPolicy configuration for the database service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + reportsConfig: + description: NetworkPolicy configuration for the cryostat-reports + service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + storageConfig: + description: NetworkPolicy configuration for the storage service. + properties: + disabled: + description: Disable the NetworkPolicy for a given service. + type: boolean + type: object + type: object operandMetadata: description: Options to configure the Cryostat deployments and pods metadata diff --git a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml index ec137f4b8..c96298d60 100644 --- a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml @@ -165,6 +165,30 @@ spec: label with key "app" is reserved for use by the operator. displayName: Labels path: networkOptions.coreConfig.labels + - description: Options to customize the NetworkPolicy objects created for Cryostat's + various Services. + displayName: Network Policies + path: networkPolicies + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.coreConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.databaseConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.reportsConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch + - description: Disable the NetworkPolicy for a given service. + displayName: Disable NetworkPolicy creation + path: networkPolicies.storageConfig.disabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch - description: Options to configure the Cryostat deployments and pods metadata displayName: Operand metadata path: operandMetadata diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b629f3e36..70fdeed65 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -118,6 +118,7 @@ rules: - networking.k8s.io resources: - ingresses + - networkpolicies verbs: - '*' - apiGroups: diff --git a/config/samples/operator_v1beta2_cryostat.yaml b/config/samples/operator_v1beta2_cryostat.yaml index 8f014f31d..eeb956e58 100644 --- a/config/samples/operator_v1beta2_cryostat.yaml +++ b/config/samples/operator_v1beta2_cryostat.yaml @@ -13,3 +13,4 @@ spec: spec: {} reportOptions: replicas: 0 + networkPolicies: {} diff --git a/internal/controllers/common/resource_definitions/resource_definitions.go b/internal/controllers/common/resource_definitions/resource_definitions.go index 1eb7a757f..e0ff1648b 100644 --- a/internal/controllers/common/resource_definitions/resource_definitions.go +++ b/internal/controllers/common/resource_definitions/resource_definitions.go @@ -95,6 +95,14 @@ const ( DatabaseName string = "cryostat" ) +func CorePodLabels(cr *model.CryostatInstance) map[string]string { + return map[string]string{ + "app": cr.Name, + "kind": "cryostat", + "component": "cryostat", + } +} + func NewDeploymentForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTags *ImageTags, tls *TLSConfig, fsGroup int64, openshift bool) (*appsv1.Deployment, error) { // Force one replica to avoid lock file and PVC contention @@ -109,11 +117,7 @@ func NewDeploymentForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTa defaultDeploymentAnnotations := map[string]string{ "app.openshift.io/connects-to": constants.OperatorDeploymentName, } - defaultPodLabels := map[string]string{ - "app": cr.Name, - "kind": "cryostat", - "component": "cryostat", - } + defaultPodLabels := CorePodLabels(cr) userDefinedDeploymentLabels := make(map[string]string) userDefinedDeploymentAnnotations := make(map[string]string) userDefinedPodTemplateLabels := make(map[string]string) @@ -199,6 +203,14 @@ func createMetadataCopy(in *operatorv1beta2.ResourceMetadata) operatorv1beta2.Re } } +func DatabasePodLabels(cr *model.CryostatInstance) map[string]string { + return map[string]string{ + "app": cr.Name, + "kind": "cryostat", + "component": "database", + } +} + func NewDeploymentForDatabase(cr *model.CryostatInstance, imageTags *ImageTags, tls *TLSConfig, openshift bool, fsGroup int64) *appsv1.Deployment { replicas := int32(1) @@ -212,11 +224,7 @@ func NewDeploymentForDatabase(cr *model.CryostatInstance, imageTags *ImageTags, defaultDeploymentAnnotations := map[string]string{ "app.openshift.io/connects-to": cr.Name, } - defaultPodLabels := map[string]string{ - "app": cr.Name, - "kind": "cryostat", - "component": "database", - } + defaultPodLabels := DatabasePodLabels(cr) operandMeta := operatorv1beta2.OperandMetadata{ DeploymentMetadata: &operatorv1beta2.ResourceMetadata{}, PodMetadata: &operatorv1beta2.ResourceMetadata{}, @@ -270,6 +278,14 @@ func NewDeploymentForDatabase(cr *model.CryostatInstance, imageTags *ImageTags, } } +func StoragePodLabels(cr *model.CryostatInstance) map[string]string { + return map[string]string{ + "app": cr.Name, + "kind": "cryostat", + "component": "storage", + } +} + func NewDeploymentForStorage(cr *model.CryostatInstance, imageTags *ImageTags, tls *TLSConfig, openshift bool, fsGroup int64) *appsv1.Deployment { replicas := int32(1) @@ -282,11 +298,7 @@ func NewDeploymentForStorage(cr *model.CryostatInstance, imageTags *ImageTags, t defaultDeploymentAnnotations := map[string]string{ "app.openshift.io/connects-to": cr.Name, } - defaultPodLabels := map[string]string{ - "app": cr.Name, - "kind": "cryostat", - "component": "storage", - } + defaultPodLabels := StoragePodLabels(cr) userDefinedDeploymentLabels := make(map[string]string) userDefinedDeploymentAnnotations := make(map[string]string) userDefinedPodTemplateLabels := make(map[string]string) @@ -350,6 +362,14 @@ func NewDeploymentForStorage(cr *model.CryostatInstance, imageTags *ImageTags, t } } +func ReportsPodLabels(cr *model.CryostatInstance) map[string]string { + return map[string]string{ + "app": cr.Name, + "kind": "cryostat", + "component": "reports", + } +} + func NewDeploymentForReports(cr *model.CryostatInstance, imageTags *ImageTags, tls *TLSConfig, openshift bool) *appsv1.Deployment { replicas := int32(0) @@ -366,11 +386,7 @@ func NewDeploymentForReports(cr *model.CryostatInstance, imageTags *ImageTags, t defaultDeploymentAnnotations := map[string]string{ "app.openshift.io/connects-to": cr.Name, } - defaultPodLabels := map[string]string{ - "app": cr.Name, - "kind": "cryostat", - "component": "reports", - } + defaultPodLabels := ReportsPodLabels(cr) userDefinedDeploymentLabels := make(map[string]string) userDefinedDeploymentAnnotations := make(map[string]string) userDefinedPodTemplateLabels := make(map[string]string) diff --git a/internal/controllers/cryostat_controller.go b/internal/controllers/cryostat_controller.go index 5d1a00b12..86142f0fe 100644 --- a/internal/controllers/cryostat_controller.go +++ b/internal/controllers/cryostat_controller.go @@ -64,7 +64,7 @@ func NewCryostatReconciler(config *ReconcilerConfig) (*CryostatReconciler, error // +kubebuilder:rbac:namespace=system,groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create // +kubebuilder:rbac:groups=cert-manager.io,resources=issuers;certificates,verbs=create;get;list;update;watch;delete // +kubebuilder:rbac:groups=console.openshift.io,resources=consolelinks,verbs=get;create;list;update;delete -// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=* +// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses;networkpolicies,verbs=* // RBAC for Insights controller, remove these when moving to a separate container // +kubebuilder:rbac:namespace=system,groups=apps,resources=deployments;deployments/finalizers,verbs=create;update;get;list;watch diff --git a/internal/controllers/networkpolicy.go b/internal/controllers/networkpolicy.go new file mode 100644 index 000000000..5e371a12f --- /dev/null +++ b/internal/controllers/networkpolicy.go @@ -0,0 +1,229 @@ +// Copyright The Cryostat Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "context" + "fmt" + + resources "github.com/cryostatio/cryostat-operator/internal/controllers/common/resource_definitions" + "github.com/cryostatio/cryostat-operator/internal/controllers/constants" + "github.com/cryostatio/cryostat-operator/internal/controllers/model" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var AllNamespacesSelector = networkingv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{}, +} + +var RouteSelector = networkingv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "policy-group.network.openshift.io/ingress": "", + }, + }, +} + +func installationNamespaceSelector(cr *model.CryostatInstance) *metav1.LabelSelector { + return &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": cr.InstallNamespace, + }, + } +} + +func (r *Reconciler) reconcileCoreNetworkPolicy(ctx context.Context, cr *model.CryostatInstance) error { + networkPolicy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-internal-ingress", cr.Name), + Namespace: cr.InstallNamespace, + }, + } + + if cr.Spec.NetworkPolicies != nil && cr.Spec.NetworkPolicies.CoreConfig != nil && cr.Spec.NetworkPolicies.CoreConfig.Disabled != nil && *cr.Spec.NetworkPolicies.CoreConfig.Disabled { + return r.deletePolicy(ctx, networkPolicy) + } + + return r.createOrUpdatePolicy(ctx, networkPolicy, cr.Object, func() error { + networkPolicy.Spec = networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: resources.CorePodLabels(cr), + }, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + AllNamespacesSelector, + RouteSelector, + }, + Ports: []networkingv1.NetworkPolicyPort{ + { + Port: &intstr.IntOrString{IntVal: constants.AuthProxyHttpContainerPort}, + }, + }, + }, + }, + } + return nil + }) +} + +func (r *Reconciler) reconcileDatabaseNetworkPolicy(ctx context.Context, cr *model.CryostatInstance) error { + networkPolicy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-db-internal-ingress", cr.Name), + Namespace: cr.InstallNamespace, + }, + } + if cr.Spec.NetworkPolicies != nil && cr.Spec.NetworkPolicies.DatabaseConfig != nil && cr.Spec.NetworkPolicies.DatabaseConfig.Disabled != nil && *cr.Spec.NetworkPolicies.DatabaseConfig.Disabled { + return r.deletePolicy(ctx, networkPolicy) + } + + return r.createOrUpdatePolicy(ctx, networkPolicy, cr.Object, func() error { + networkPolicy.Spec = networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: resources.DatabasePodLabels(cr), + }, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: installationNamespaceSelector(cr), + PodSelector: &metav1.LabelSelector{ + MatchLabels: resources.CorePodLabels(cr), + }, + }, + }, + Ports: []networkingv1.NetworkPolicyPort{ + { + Port: &intstr.IntOrString{IntVal: constants.DatabasePort}, + }, + }, + }, + }, + } + return nil + }) +} + +func (r *Reconciler) reconcileStorageNetworkPolicy(ctx context.Context, cr *model.CryostatInstance) error { + networkPolicy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-storage-internal-ingress", cr.Name), + Namespace: cr.InstallNamespace, + }, + } + + if cr.Spec.NetworkPolicies != nil && cr.Spec.NetworkPolicies.StorageConfig != nil && cr.Spec.NetworkPolicies.StorageConfig.Disabled != nil && *cr.Spec.NetworkPolicies.StorageConfig.Disabled { + return r.deletePolicy(ctx, networkPolicy) + } + + return r.createOrUpdatePolicy(ctx, networkPolicy, cr.Object, func() error { + networkPolicy.Spec = networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: resources.StoragePodLabels(cr), + }, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: installationNamespaceSelector(cr), + PodSelector: &metav1.LabelSelector{ + MatchLabels: resources.CorePodLabels(cr), + }, + }, + }, + Ports: []networkingv1.NetworkPolicyPort{ + { + Port: &intstr.IntOrString{IntVal: constants.StoragePort}, + }, + }, + }, + }, + } + return nil + }) +} + +func (r *Reconciler) reconcileReportsNetworkPolicy(ctx context.Context, cr *model.CryostatInstance) error { + networkPolicy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-reports-internal-ingress", cr.Name), + Namespace: cr.InstallNamespace, + }, + } + + if cr.Spec.NetworkPolicies != nil && cr.Spec.NetworkPolicies.ReportsConfig != nil && cr.Spec.NetworkPolicies.ReportsConfig.Disabled != nil && *cr.Spec.NetworkPolicies.ReportsConfig.Disabled { + return r.deletePolicy(ctx, networkPolicy) + } + + return r.createOrUpdatePolicy(ctx, networkPolicy, cr.Object, func() error { + networkPolicy.Spec = networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: resources.ReportsPodLabels(cr), + }, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: installationNamespaceSelector(cr), + PodSelector: &metav1.LabelSelector{ + MatchLabels: resources.CorePodLabels(cr), + }, + }, + }, + Ports: []networkingv1.NetworkPolicyPort{ + { + Port: &intstr.IntOrString{IntVal: constants.ReportsContainerPort}, + }, + }, + }, + }, + } + return nil + }) +} + +func (r *Reconciler) createOrUpdatePolicy(ctx context.Context, networkPolicy *networkingv1.NetworkPolicy, owner metav1.Object, + delegate controllerutil.MutateFn) error { + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, networkPolicy, func() error { + // Set the Cryostat CR as controller + if owner != nil { + if err := controllerutil.SetControllerReference(owner, networkPolicy, r.Scheme); err != nil { + return err + } + } + // Call the delegate for specific mutations + return delegate() + }) + if err != nil { + return err + } + r.Log.Info(fmt.Sprintf("Network policy %s", op), "name", networkPolicy.Name, "namespace", networkPolicy.Namespace) + return nil +} + +func (r *Reconciler) deletePolicy(ctx context.Context, networkPolicy *networkingv1.NetworkPolicy) error { + err := r.Client.Delete(ctx, networkPolicy) + if err != nil && !errors.IsNotFound(err) { + r.Log.Error(err, "Could not delete network policy", "name", networkPolicy.Name, "namespace", networkPolicy.Namespace) + return err + } + r.Log.Info("Network policy deleted", "name", networkPolicy.Name, "namespace", networkPolicy.Namespace) + return nil +} diff --git a/internal/controllers/reconciler.go b/internal/controllers/reconciler.go index 4ac270330..cbc8f83bc 100644 --- a/internal/controllers/reconciler.go +++ b/internal/controllers/reconciler.go @@ -289,6 +289,10 @@ func (r *Reconciler) reconcileCryostat(ctx context.Context, cr *model.CryostatIn if err != nil { return requeueIfIngressNotReady(reqLogger, err) } + err = r.reconcileCoreNetworkPolicy(ctx, cr) + if err != nil { + return reconcile.Result{}, err + } err = r.reconcileAgentService(ctx, cr) if err != nil { return reconcile.Result{}, err @@ -395,6 +399,10 @@ func (r *Reconciler) reconcileReports(ctx context.Context, reqLogger logr.Logger if err != nil { return reconcile.Result{}, err } + err = r.reconcileReportsNetworkPolicy(ctx, cr) + if err != nil { + return reconcile.Result{}, err + } deployment := resources.NewDeploymentForReports(cr, imageTags, tls, r.IsOpenShift) if desired == 0 { if err := r.Client.Delete(ctx, deployment); err != nil && !kerrors.IsNotFound(err) { @@ -438,6 +446,10 @@ func (r *Reconciler) reconcileDatabase(ctx context.Context, reqLogger logr.Logge if err != nil { return reconcile.Result{}, err } + err = r.reconcileDatabaseNetworkPolicy(ctx, cr) + if err != nil { + return reconcile.Result{}, err + } deployment := resources.NewDeploymentForDatabase(cr, imageTags, tls, r.IsOpenShift, fsGroup) err = r.createOrUpdateDeployment(ctx, deployment, cr.Object) @@ -466,6 +478,10 @@ func (r *Reconciler) reconcileStorage(ctx context.Context, reqLogger logr.Logger if err != nil { return reconcile.Result{}, err } + err = r.reconcileStorageNetworkPolicy(ctx, cr) + if err != nil { + return reconcile.Result{}, err + } deployment := resources.NewDeploymentForStorage(cr, imageTags, tls, r.IsOpenShift, fsGroup) err = r.createOrUpdateDeployment(ctx, deployment, cr.Object) diff --git a/internal/controllers/reconciler_test.go b/internal/controllers/reconciler_test.go index 421c27b63..2a50fa183 100644 --- a/internal/controllers/reconciler_test.go +++ b/internal/controllers/reconciler_test.go @@ -162,9 +162,12 @@ func resourceChecks() []resourceCheck { {(*cryostatTestInput).expectDatabaseSecret, "database secret"}, {(*cryostatTestInput).expectStorageSecret, "object storage secret"}, {(*cryostatTestInput).expectCoreService, "core service"}, + {(*cryostatTestInput).expectCoreNetworkPolicy, "core networkpolicy"}, {(*cryostatTestInput).expectMainDeployment, "main deployment"}, {(*cryostatTestInput).expectDatabaseDeployment, "database deployment"}, + {(*cryostatTestInput).expectDatabaseNetworkPolicy, "database networkpolicy"}, {(*cryostatTestInput).expectStorageDeployment, "storage deployment"}, + {(*cryostatTestInput).expectStorageNetworkPolicy, "storage networkpolicy"}, {(*cryostatTestInput).expectLockConfigMap, "lock config map"}, {(*cryostatTestInput).expectAgentProxyConfigMap, "agent proxy config map"}, {(*cryostatTestInput).expectAgentProxyService, "agent proxy service"}, @@ -537,6 +540,42 @@ func (c *controllerTest) commonTests() { t.checkRoute(expected) }) }) + Context("with networkpolicies disabled", func() { + var cr *model.CryostatInstance + BeforeEach(func() { + cr = t.NewCryostat() + disabled := true + cr.Spec.NetworkPolicies = &operatorv1beta2.NetworkPoliciesList{ + CoreConfig: &operatorv1beta2.NetworkPolicyConfig{ + Disabled: &disabled, + }, + DatabaseConfig: &operatorv1beta2.NetworkPolicyConfig{ + Disabled: &disabled, + }, + StorageConfig: &operatorv1beta2.NetworkPolicyConfig{ + Disabled: &disabled, + }, + ReportsConfig: &operatorv1beta2.NetworkPolicyConfig{ + Disabled: &disabled, + }, + } + }) + JustBeforeEach(func() { + t.reconcileCryostatFully() + }) + It("should not create cryostat networkpolicy", func() { + t.expectNoNetworkPolicy(t.NewCryostatNetworkPolicy().Name) + }) + It("should not create database networkpolicy", func() { + t.expectNoNetworkPolicy(t.NewDatabaseNetworkPolicy().Name) + }) + It("should not create storage networkpolicy", func() { + t.expectNoNetworkPolicy(t.NewStorageNetworkPolicy().Name) + }) + It("should not create reports networkpolicy", func() { + t.expectNoNetworkPolicy(t.NewReportsNetworkPolicy().Name) + }) + }) Context("with report generator service", func() { var cr *model.CryostatInstance BeforeEach(func() { @@ -559,6 +598,7 @@ func (c *controllerTest) commonTests() { t.expectStorageDeployment() t.checkReportsDeployment() t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) Context("with Scheduling options", func() { @@ -580,6 +620,7 @@ func (c *controllerTest) commonTests() { t.expectStorageDeployment() t.checkReportsDeployment() t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) Context("with low limits", func() { @@ -590,6 +631,7 @@ func (c *controllerTest) commonTests() { t.expectMainDeployment() t.checkReportsDeployment() t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) }) @@ -652,6 +694,7 @@ func (c *controllerTest) commonTests() { t.expectMainDeployment() t.checkReportsDeployment() t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) Context("Switching from 1 report sidecar to 2", func() { @@ -674,6 +717,7 @@ func (c *controllerTest) commonTests() { t.expectMainDeployment() t.checkReportsDeployment() t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) Context("Switching from 2 report sidecars to 1", func() { @@ -696,6 +740,7 @@ func (c *controllerTest) commonTests() { t.expectMainDeployment() t.checkReportsDeployment() t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) Context("Switching from 1 report sidecar to 0", func() { @@ -2211,6 +2256,7 @@ func (c *controllerTest) commonTests() { }) It("should create the reports service", func() { t.checkService(t.NewReportsService()) + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) }) }) Context("with security options", func() { @@ -2964,6 +3010,22 @@ func (t *cryostatTestInput) expectCoreService() { t.checkService(t.NewCryostatService()) } +func (t *cryostatTestInput) expectCoreNetworkPolicy() { + t.checkNetworkPolicy(t.NewCryostatNetworkPolicy()) +} + +func (t *cryostatTestInput) expectDatabaseNetworkPolicy() { + t.checkNetworkPolicy(t.NewDatabaseNetworkPolicy()) +} + +func (t *cryostatTestInput) expectStorageNetworkPolicy() { + t.checkNetworkPolicy(t.NewStorageNetworkPolicy()) +} + +func (t *cryostatTestInput) expectReportsNetworkPolicy() { + t.checkNetworkPolicy(t.NewReportsNetworkPolicy()) +} + func (t *cryostatTestInput) expectAgentProxyService() { t.checkService(t.NewAgentProxyService()) } @@ -3026,6 +3088,15 @@ func (t *cryostatTestInput) checkService(expected *corev1.Service) { t.checkServiceSpec(service, expected) } +func (t *cryostatTestInput) checkNetworkPolicy(expected *netv1.NetworkPolicy) { + policy := &netv1.NetworkPolicy{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: expected.Name, Namespace: expected.Namespace}, policy) + Expect(err).ToNot(HaveOccurred()) + + t.checkMetadata(policy, expected) + t.checkNetworkPolicySpec(policy, expected) +} + func (t *cryostatTestInput) checkServiceNoOwner(expected *corev1.Service) { service := &corev1.Service{} err := t.Client.Get(context.Background(), types.NamespacedName{Name: expected.Name, Namespace: expected.Namespace}, service) @@ -3042,12 +3113,23 @@ func (t *cryostatTestInput) checkServiceSpec(service *corev1.Service, expected * Expect(service.Spec.ClusterIP).To(Equal(expected.Spec.ClusterIP)) } +func (t *cryostatTestInput) checkNetworkPolicySpec(policy *netv1.NetworkPolicy, expected *netv1.NetworkPolicy) { + Expect(policy.Spec.PodSelector).To(Equal(expected.Spec.PodSelector)) + Expect(policy.Spec.Ingress).To(Equal(expected.Spec.Ingress)) +} + func (t *cryostatTestInput) expectNoService(svcName string) { service := &corev1.Service{} err := t.Client.Get(context.Background(), types.NamespacedName{Name: svcName, Namespace: t.Namespace}, service) Expect(kerrors.IsNotFound(err)).To(BeTrue()) } +func (t *cryostatTestInput) expectNoNetworkPolicy(policyName string) { + policy := &netv1.NetworkPolicy{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: policyName, Namespace: t.Namespace}, policy) + Expect(kerrors.IsNotFound(err)).To(BeTrue()) +} + func (t *cryostatTestInput) expectNoReportsDeployment() { deployment := &appsv1.Deployment{} err := t.Client.Get(context.Background(), types.NamespacedName{Name: t.Name + "-reports", Namespace: t.Namespace}, deployment) diff --git a/internal/test/resources.go b/internal/test/resources.go index 05a922c83..becb7f84c 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -793,6 +793,174 @@ func (r *TestResources) NewCryostatService() *corev1.Service { } } +func (r *TestResources) NewCryostatNetworkPolicy() *netv1.NetworkPolicy { + return &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-internal-ingress", r.Name), + Namespace: r.Namespace, + }, + Spec: netv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "cryostat", + "kind": "cryostat", + }, + }, + Ingress: []netv1.NetworkPolicyIngressRule{ + netv1.NetworkPolicyIngressRule{ + From: []netv1.NetworkPolicyPeer{ + netv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{}, + }, + netv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "policy-group.network.openshift.io/ingress": "", + }, + }, + }, + }, + Ports: []netv1.NetworkPolicyPort{ + netv1.NetworkPolicyPort{ + Port: &intstr.IntOrString{IntVal: 4180}, + }, + }, + }, + }, + }, + } +} + +func (r *TestResources) NewDatabaseNetworkPolicy() *netv1.NetworkPolicy { + return &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-db-internal-ingress", r.Name), + Namespace: r.Namespace, + }, + Spec: netv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "database", + "kind": "cryostat", + }, + }, + Ingress: []netv1.NetworkPolicyIngressRule{ + netv1.NetworkPolicyIngressRule{ + From: []netv1.NetworkPolicyPeer{ + netv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": r.Namespace, + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "cryostat", + "kind": "cryostat", + }, + }, + }, + }, + Ports: []netv1.NetworkPolicyPort{ + netv1.NetworkPolicyPort{ + Port: &intstr.IntOrString{IntVal: 5432}, + }, + }, + }, + }, + }, + } +} + +func (r *TestResources) NewStorageNetworkPolicy() *netv1.NetworkPolicy { + return &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-storage-internal-ingress", r.Name), + Namespace: r.Namespace, + }, + Spec: netv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "storage", + "kind": "cryostat", + }, + }, + Ingress: []netv1.NetworkPolicyIngressRule{ + netv1.NetworkPolicyIngressRule{ + From: []netv1.NetworkPolicyPeer{ + netv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": r.Namespace, + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "cryostat", + "kind": "cryostat", + }, + }, + }, + }, + Ports: []netv1.NetworkPolicyPort{ + netv1.NetworkPolicyPort{ + Port: &intstr.IntOrString{IntVal: 8333}, + }, + }, + }, + }, + }, + } +} + +func (r *TestResources) NewReportsNetworkPolicy() *netv1.NetworkPolicy { + return &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-reports-internal-ingress", r.Name), + Namespace: r.Namespace, + }, + Spec: netv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "reports", + "kind": "cryostat", + }, + }, + Ingress: []netv1.NetworkPolicyIngressRule{ + netv1.NetworkPolicyIngressRule{ + From: []netv1.NetworkPolicyPeer{ + netv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": r.Namespace, + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": r.Name, + "component": "cryostat", + "kind": "cryostat", + }, + }, + }, + }, + Ports: []netv1.NetworkPolicyPort{ + netv1.NetworkPolicyPort{ + Port: &intstr.IntOrString{IntVal: 10000}, + }, + }, + }, + }, + }, + } +} + func (r *TestResources) NewGrafanaService() *corev1.Service { c := true return &corev1.Service{