From ea6e92429fd4d5de4b110fdd1a1d4e081a49ab55 Mon Sep 17 00:00:00 2001 From: Penkey Suresh Date: Mon, 6 Jan 2025 08:57:36 -0800 Subject: [PATCH] Adding controller for installing CCS components --- api/v1/ccs_api_types.go | 65 +++ api/v1/ccs_controller_types.go | 65 +++ ...compliance_configuration_security_types.go | 68 +++ api/v1/zz_generated.deepcopy.go | 310 +++++++++++ cmd/main.go | 2 +- go.mod | 13 +- go.sum | 2 - internal/controller/ccs_controller.go | 39 ++ internal/controller/controllers.go | 9 +- pkg/components/enterprise.go | 16 +- pkg/controller/ccs/ccs_controller.go | 486 ++++++++++++++++++ pkg/controller/ccs/ccs_controller_test.go | 151 ++++++ pkg/controller/ccs/ccs_suit_test.go | 29 ++ ....io_complianceconfigurationsecurities.yaml | 311 +++++++++++ pkg/render/ccs/api.go | 262 ++++++++++ pkg/render/ccs/ccs.go | 141 +++++ pkg/render/ccs/ccs_suit_test.go | 29 ++ pkg/render/ccs/ccs_test.go | 198 +++++++ pkg/render/ccs/controller.go | 412 +++++++++++++++ pkg/render/ccs/default-config-inputs.json | 142 +++++ pkg/render/ccs/host-scanner.yaml.template | 77 +++ .../common/networkpolicy/networkpolicy.go | 6 +- pkg/render/logstorage/linseed/linseed.go | 8 +- 23 files changed, 2827 insertions(+), 14 deletions(-) create mode 100644 api/v1/ccs_api_types.go create mode 100644 api/v1/ccs_controller_types.go create mode 100644 api/v1/compliance_configuration_security_types.go create mode 100644 internal/controller/ccs_controller.go create mode 100644 pkg/controller/ccs/ccs_controller.go create mode 100644 pkg/controller/ccs/ccs_controller_test.go create mode 100644 pkg/controller/ccs/ccs_suit_test.go create mode 100644 pkg/crds/operator/operator.tigera.io_complianceconfigurationsecurities.yaml create mode 100644 pkg/render/ccs/api.go create mode 100644 pkg/render/ccs/ccs.go create mode 100644 pkg/render/ccs/ccs_suit_test.go create mode 100644 pkg/render/ccs/ccs_test.go create mode 100644 pkg/render/ccs/controller.go create mode 100644 pkg/render/ccs/default-config-inputs.json create mode 100644 pkg/render/ccs/host-scanner.yaml.template diff --git a/api/v1/ccs_api_types.go b/api/v1/ccs_api_types.go new file mode 100644 index 0000000000..6a5fd87b0f --- /dev/null +++ b/api/v1/ccs_api_types.go @@ -0,0 +1,65 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in CCS 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 v1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// CCSAPIDeployment is the configuration for the CCS API Deployment. +type CCSAPIDeployment struct { + // Spec is the specification of the CCS API Deployment. + // +optional + Spec *CCSAPIDeploymentSpec `json:"spec,omitempty"` +} + +// CCSAPIDeploymentSpec defines configuration for the CCS API Deployment. +type CCSAPIDeploymentSpec struct { + // Template describes the CCS API Deployment pod that will be created. + // +optional + Template *CCSAPIDeploymentPodTemplateSpec `json:"template,omitempty"` +} + +// CCSAPIDeploymentPodTemplateSpec is the CCS API Deployment's PodTemplateSpec +type CCSAPIDeploymentPodTemplateSpec struct { + // Spec is the CCS API Deployment's PodSpec. + // +optional + Spec *CCSAPIDeploymentPodSpec `json:"spec,omitempty"` +} + +// CCSAPIDeploymentPodSpec is the CCS API Deployment's PodSpec. +type CCSAPIDeploymentPodSpec struct { + // Containers is a list of CCS API containers. + // If specified, this overrides the specified CCS API Deployment containers. + // If omitted, the CCS API Deployment will use its default values for its containers. + // +optional + Containers []CCSAPIDeploymentContainer `json:"containers,omitempty"` +} + +// CCSAPIDeploymentContainer is a CCS API Deployment container. +type CCSAPIDeploymentContainer struct { + // Name is an enum which identifies the CCS API Deployment container by name. + // Supported values are: tigera-ccs-api + // +kubebuilder:validation:Enum=tigera-ccs-api + Name string `json:"name"` + + // Resources allows customization of limits and requests for compute resources such as cpu and memory. + // If specified, this overrides the named CCS API Deployment container's resources. + // If omitted, the CCS API Deployment will use its default value for this container's resources. + // +optional + Resources *v1.ResourceRequirements `json:"resources,omitempty"` +} diff --git a/api/v1/ccs_controller_types.go b/api/v1/ccs_controller_types.go new file mode 100644 index 0000000000..034ff50486 --- /dev/null +++ b/api/v1/ccs_controller_types.go @@ -0,0 +1,65 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in CCS 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 v1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// CCSControllerDeployment is the configuration for the CCS controller Deployment. +type CCSControllerDeployment struct { + // Spec is the specification of the CCS controller Deployment. + // +optional + Spec *CCSControllerDeploymentSpec `json:"spec,omitempty"` +} + +// CCSControllerDeploymentSpec defines configuration for the CCS controller Deployment. +type CCSControllerDeploymentSpec struct { + // Template describes the CCS controller Deployment pod that will be created. + // +optional + Template *CCSControllerDeploymentPodTemplateSpec `json:"template,omitempty"` +} + +// CCSControllerDeploymentPodTemplateSpec is the CCS controller Deployment's PodTemplateSpec +type CCSControllerDeploymentPodTemplateSpec struct { + // Spec is the CCS controller Deployment's PodSpec. + // +optional + Spec *CCSControllerDeploymentPodSpec `json:"spec,omitempty"` +} + +// CCSControllerDeploymentPodSpec is the CCS controller Deployment's PodSpec. +type CCSControllerDeploymentPodSpec struct { + // Containers is a list of CCS controller containers. + // If specified, this overrides the specified CCS controller Deployment containers. + // If omitted, the CCS controller Deployment will use its default values for its containers. + // +optional + Containers []CCSControllerDeploymentContainer `json:"containers,omitempty"` +} + +// CCSControllerDeploymentContainer is a CCS controller Deployment container. +type CCSControllerDeploymentContainer struct { + // Name is an enum which identifies the CCS controller Deployment container by name. + // Supported values are: tigera-ccs-controller + // +kubebuilder:validation:Enum=tigera-ccs-controller + Name string `json:"name"` + + // Resources allows customization of limits and requests for compute resources such as cpu and memory. + // If specified, this overrides the named CCS controller Deployment container's resources. + // If omitted, the CCS controller Deployment will use its default value for this container's resources. + // +optional + Resources *v1.ResourceRequirements `json:"resources,omitempty"` +} diff --git a/api/v1/compliance_configuration_security_types.go b/api/v1/compliance_configuration_security_types.go new file mode 100644 index 0000000000..b85e629359 --- /dev/null +++ b/api/v1/compliance_configuration_security_types.go @@ -0,0 +1,68 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. +/* + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ComplianceConfigurationSecuritySpec defines the desired state of CCS. +type ComplianceConfigurationSecuritySpec struct { + // This controls the deployment of the CCS controller. + CCSControllerDeployment *CCSControllerDeployment `json:"ccsControllerDeployment,omitempty"` + + // This controls the deployment of the CCS API. + CCSAPIDeployment *CCSAPIDeployment `json:"ccsAPIDeployment,omitempty"` +} + +// ComplianceConfigurationSecurityStatus defines the observed state of CCS. +type ComplianceConfigurationSecurityStatus struct { + // State provides user-readable status. + State string `json:"state,omitempty"` + + // Conditions represents the latest observed set of conditions for the component. A component may be one or more of + // Ready, Progressing, Degraded or other customer types. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +// ComplianceConfigurationSecurity installs the components required for CCS reports. +type ComplianceConfigurationSecurity struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the desired state for CCS. + Spec ComplianceConfigurationSecuritySpec `json:"spec,omitempty"` + // Most recently observed state for CCS. + Status ComplianceConfigurationSecurityStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ComplianceConfigurationSecurityList contains a list of ComplianceConfigurationSecurity +type ComplianceConfigurationSecurityList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ComplianceConfigurationSecurity `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ComplianceConfigurationSecurity{}, &ComplianceConfigurationSecurityList{}) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 69307ba009..9407fdd4e2 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -772,6 +772,210 @@ func (in *Azure) DeepCopy() *Azure { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSAPIDeployment) DeepCopyInto(out *CCSAPIDeployment) { + *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(CCSAPIDeploymentSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSAPIDeployment. +func (in *CCSAPIDeployment) DeepCopy() *CCSAPIDeployment { + if in == nil { + return nil + } + out := new(CCSAPIDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSAPIDeploymentContainer) DeepCopyInto(out *CCSAPIDeploymentContainer) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSAPIDeploymentContainer. +func (in *CCSAPIDeploymentContainer) DeepCopy() *CCSAPIDeploymentContainer { + if in == nil { + return nil + } + out := new(CCSAPIDeploymentContainer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSAPIDeploymentPodSpec) DeepCopyInto(out *CCSAPIDeploymentPodSpec) { + *out = *in + if in.Containers != nil { + in, out := &in.Containers, &out.Containers + *out = make([]CCSAPIDeploymentContainer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSAPIDeploymentPodSpec. +func (in *CCSAPIDeploymentPodSpec) DeepCopy() *CCSAPIDeploymentPodSpec { + if in == nil { + return nil + } + out := new(CCSAPIDeploymentPodSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSAPIDeploymentPodTemplateSpec) DeepCopyInto(out *CCSAPIDeploymentPodTemplateSpec) { + *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(CCSAPIDeploymentPodSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSAPIDeploymentPodTemplateSpec. +func (in *CCSAPIDeploymentPodTemplateSpec) DeepCopy() *CCSAPIDeploymentPodTemplateSpec { + if in == nil { + return nil + } + out := new(CCSAPIDeploymentPodTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSAPIDeploymentSpec) DeepCopyInto(out *CCSAPIDeploymentSpec) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(CCSAPIDeploymentPodTemplateSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSAPIDeploymentSpec. +func (in *CCSAPIDeploymentSpec) DeepCopy() *CCSAPIDeploymentSpec { + if in == nil { + return nil + } + out := new(CCSAPIDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSControllerDeployment) DeepCopyInto(out *CCSControllerDeployment) { + *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(CCSControllerDeploymentSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSControllerDeployment. +func (in *CCSControllerDeployment) DeepCopy() *CCSControllerDeployment { + if in == nil { + return nil + } + out := new(CCSControllerDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSControllerDeploymentContainer) DeepCopyInto(out *CCSControllerDeploymentContainer) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSControllerDeploymentContainer. +func (in *CCSControllerDeploymentContainer) DeepCopy() *CCSControllerDeploymentContainer { + if in == nil { + return nil + } + out := new(CCSControllerDeploymentContainer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSControllerDeploymentPodSpec) DeepCopyInto(out *CCSControllerDeploymentPodSpec) { + *out = *in + if in.Containers != nil { + in, out := &in.Containers, &out.Containers + *out = make([]CCSControllerDeploymentContainer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSControllerDeploymentPodSpec. +func (in *CCSControllerDeploymentPodSpec) DeepCopy() *CCSControllerDeploymentPodSpec { + if in == nil { + return nil + } + out := new(CCSControllerDeploymentPodSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSControllerDeploymentPodTemplateSpec) DeepCopyInto(out *CCSControllerDeploymentPodTemplateSpec) { + *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(CCSControllerDeploymentPodSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSControllerDeploymentPodTemplateSpec. +func (in *CCSControllerDeploymentPodTemplateSpec) DeepCopy() *CCSControllerDeploymentPodTemplateSpec { + if in == nil { + return nil + } + out := new(CCSControllerDeploymentPodTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CCSControllerDeploymentSpec) DeepCopyInto(out *CCSControllerDeploymentSpec) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(CCSControllerDeploymentPodTemplateSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CCSControllerDeploymentSpec. +func (in *CCSControllerDeploymentSpec) DeepCopy() *CCSControllerDeploymentSpec { + if in == nil { + return nil + } + out := new(CCSControllerDeploymentSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CNILogging) DeepCopyInto(out *CNILogging) { *out = *in @@ -1837,6 +2041,112 @@ func (in *ComplianceBenchmarkerDaemonSetSpec) DeepCopy() *ComplianceBenchmarkerD return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComplianceConfigurationSecurity) DeepCopyInto(out *ComplianceConfigurationSecurity) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComplianceConfigurationSecurity. +func (in *ComplianceConfigurationSecurity) DeepCopy() *ComplianceConfigurationSecurity { + if in == nil { + return nil + } + out := new(ComplianceConfigurationSecurity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ComplianceConfigurationSecurity) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComplianceConfigurationSecurityList) DeepCopyInto(out *ComplianceConfigurationSecurityList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ComplianceConfigurationSecurity, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComplianceConfigurationSecurityList. +func (in *ComplianceConfigurationSecurityList) DeepCopy() *ComplianceConfigurationSecurityList { + if in == nil { + return nil + } + out := new(ComplianceConfigurationSecurityList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ComplianceConfigurationSecurityList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComplianceConfigurationSecuritySpec) DeepCopyInto(out *ComplianceConfigurationSecuritySpec) { + *out = *in + if in.CCSControllerDeployment != nil { + in, out := &in.CCSControllerDeployment, &out.CCSControllerDeployment + *out = new(CCSControllerDeployment) + (*in).DeepCopyInto(*out) + } + if in.CCSAPIDeployment != nil { + in, out := &in.CCSAPIDeployment, &out.CCSAPIDeployment + *out = new(CCSAPIDeployment) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComplianceConfigurationSecuritySpec. +func (in *ComplianceConfigurationSecuritySpec) DeepCopy() *ComplianceConfigurationSecuritySpec { + if in == nil { + return nil + } + out := new(ComplianceConfigurationSecuritySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComplianceConfigurationSecurityStatus) DeepCopyInto(out *ComplianceConfigurationSecurityStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComplianceConfigurationSecurityStatus. +func (in *ComplianceConfigurationSecurityStatus) DeepCopy() *ComplianceConfigurationSecurityStatus { + if in == nil { + return nil + } + out := new(ComplianceConfigurationSecurityStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ComplianceControllerDeployment) DeepCopyInto(out *ComplianceControllerDeployment) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 27829ff115..7afcdd90bc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2020-2025 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/go.mod b/go.mod index 00945833b1..a5802dfffa 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,8 @@ require ( sigs.k8s.io/yaml v1.4.0 ) +require k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect + require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/alessio/shellescape v1.4.2 // indirect @@ -49,7 +51,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/corazawaf/coraza-coreruleset/v4 v4.7.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elastic/go-sysinfo v1.13.1 // indirect github.com/elastic/go-ucfg v0.8.8 // indirect @@ -116,16 +117,14 @@ require ( howett.net/plist v1.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect - sigs.k8s.io/gateway-api v1.1.0 // indirect + sigs.k8s.io/gateway-api v1.1.0 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) -require ( - github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc // indirect - github.com/magefile/mage v1.14.0 // indirect -) +require github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc + +require github.com/magefile/mage v1.14.0 // indirect replace ( github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM diff --git a/go.sum b/go.sum index 2bcf796112..947247ef96 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8F github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M= github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc= github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU= -github.com/corazawaf/coraza-coreruleset/v4 v4.7.0 h1:j02CDxQYHVFZfBxbKLWYg66jSLbPmZp1GebyMwzN9Z0= -github.com/corazawaf/coraza-coreruleset/v4 v4.7.0/go.mod h1:1FQt1p+JSQ6tYrafMqZrEEdDmhq6aVuIJdnk+bM9hMY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/controller/ccs_controller.go b/internal/controller/ccs_controller.go new file mode 100644 index 0000000000..149db3fa13 --- /dev/null +++ b/internal/controller/ccs_controller.go @@ -0,0 +1,39 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 controller + +import ( + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/tigera/operator/pkg/controller/ccs" + "github.com/tigera/operator/pkg/controller/options" +) + +// CCSReconciler reconciles a CCS object +type CCSReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=operator.tigera.io,resources=complianceconfigurationsecurities,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=operator.tigera.io,resources=complianceconfigurationsecurities/status,verbs=get;update;patch + +func (r *CCSReconciler) SetupWithManager(mgr ctrl.Manager, opts options.AddOptions) error { + return ccs.Add(mgr, opts) +} diff --git a/internal/controller/controllers.go b/internal/controller/controllers.go index a01b9018f6..24e1908ac7 100644 --- a/internal/controller/controllers.go +++ b/internal/controller/controllers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2020-2025 Tigera, Inc. All rights reserved. /* Licensed under the Apache License, Version 2.0 (the "License"); @@ -170,6 +170,13 @@ func AddToManager(mgr ctrl.Manager, options options.AddOptions) error { }).SetupWithManager(mgr, options); err != nil { return fmt.Errorf("failed to create controller %s: %v", "GatewayAPI", err) } + if err := (&CCSReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("CCS"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, options); err != nil { + return fmt.Errorf("failed to create controller %s: %v", "CCS", err) + } // +kubebuilder:scaffold:builder return nil } diff --git a/pkg/components/enterprise.go b/pkg/components/enterprise.go index c121de1649..62bbd4940e 100644 --- a/pkg/components/enterprise.go +++ b/pkg/components/enterprise.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2020-2025 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -197,6 +197,18 @@ var ( Registry: "", } + ComponentCCSController = Component{ + Version: "master", + Image: "tigera/ccs-controller", + Registry: "", + } + + ComponentCCSAPI = Component{ + Version: "master", + Image: "tigera/ccs-api", + Registry: "", + } + ComponentEnvoyProxy = Component{ Version: "master", Image: "tigera/envoy", @@ -376,5 +388,7 @@ var ( ComponentGatewayAPIEnvoyGateway, ComponentGatewayAPIEnvoyProxy, ComponentGatewayAPIEnvoyRatelimit, + ComponentCCSAPI, + ComponentCCSController, } ) diff --git a/pkg/controller/ccs/ccs_controller.go b/pkg/controller/ccs/ccs_controller.go new file mode 100644 index 0000000000..dd1719ad4f --- /dev/null +++ b/pkg/controller/ccs/ccs_controller.go @@ -0,0 +1,486 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/controller/certificatemanager" + "github.com/tigera/operator/pkg/controller/options" + "github.com/tigera/operator/pkg/controller/status" + "github.com/tigera/operator/pkg/controller/tenancy" + "github.com/tigera/operator/pkg/controller/utils" + "github.com/tigera/operator/pkg/controller/utils/imageset" + "github.com/tigera/operator/pkg/ctrlruntime" + "github.com/tigera/operator/pkg/dns" + commonrender "github.com/tigera/operator/pkg/render" + ccsrender "github.com/tigera/operator/pkg/render/ccs" + rcertificatemanagement "github.com/tigera/operator/pkg/render/certificatemanagement" + "github.com/tigera/operator/pkg/render/common/networkpolicy" + "github.com/tigera/operator/pkg/tls/certificatemanagement" +) + +const ResourceName = "complianceconfigurationsecurity" + +var log = logf.Log.WithName("controller_ccs") + +// Add creates a new CCSController and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager, opts options.AddOptions) error { + if !opts.EnterpriseCRDExists { + // No need to start this controller. + return nil + } + licenseAPIReady := &utils.ReadyFlag{} + tierWatchReady := &utils.ReadyFlag{} + + // create the reconciler + reconciler := newReconciler(mgr, opts, licenseAPIReady, tierWatchReady) + + // Create a new controller + ccsController, err := ctrlruntime.NewController("ccs-controller", mgr, controller.Options{Reconciler: reconciler}) + if err != nil { + return err + } + + // Determine how to handle watch events for cluster-scoped resources. For multi-tenant clusters, + // we should update all tenants whenever one changes. For single-tenant clusters, we can just queue the object. + var eventHandler handler.EventHandler = &handler.EnqueueRequestForObject{} + if opts.MultiTenant { + eventHandler = utils.EnqueueAllTenants(mgr.GetClient()) + if err = ccsController.WatchObject(&operatorv1.Tenant{}, &handler.EnqueueRequestForObject{}); err != nil { + return fmt.Errorf("ccs-controller failed to watch Tenant resource: %w", err) + } + } + + k8sClient, err := kubernetes.NewForConfig(mgr.GetConfig()) + if err != nil { + log.Error(err, "Failed to establish a connection to k8s") + return err + } + + installNS, _, watchNamespaces := tenancy.GetWatchNamespaces(opts.MultiTenant, commonrender.PolicyRecommendationNamespace) + + go utils.WaitToAddLicenseKeyWatch(ccsController, k8sClient, log, licenseAPIReady) + + go utils.WaitToAddTierWatch(networkpolicy.TigeraComponentTierName, ccsController, k8sClient, log, tierWatchReady) + go utils.WaitToAddNetworkPolicyWatches(ccsController, k8sClient, log, []types.NamespacedName{ + //{Name: ccsrender.CCSAPIPolicyName, Namespace: installNS}, + //{Name: ccsrender.CCSControllerPolicyName, Namespace: installNS}, + {Name: networkpolicy.TigeraComponentDefaultDenyPolicyName, Namespace: installNS}, + }) + + // Watch for changes to primary resource CCS + err = ccsController.WatchObject(&operatorv1.ComplianceConfigurationSecurity{}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + if err = ccsController.WatchObject(&operatorv1.Installation{}, eventHandler); err != nil { + return fmt.Errorf("ccs-controller failed to watch Installation resource: %w", err) + } + + if err = ccsController.WatchObject(&operatorv1.ImageSet{}, eventHandler); err != nil { + return fmt.Errorf("ccs-controller failed to watch ImageSet: %w", err) + } + + if err = ccsController.WatchObject(&operatorv1.APIServer{}, eventHandler); err != nil { + return fmt.Errorf("ccs-controller failed to watch APIServer resource: %w", err) + } + + // Watch the given secrets in each both the CCSand operator namespaces + for _, namespace := range watchNamespaces { + for _, secretName := range []string{ + ccsrender.APICertSecretName, certificatemanagement.CASecretName, commonrender.ManagerInternalTLSSecretName, + commonrender.VoltronLinseedTLS, commonrender.VoltronLinseedPublicCert, + } { + if err = utils.AddSecretsWatch(ccsController, secretName, namespace); err != nil { + return fmt.Errorf("ccs-controller failed to watch the secret '%s' in '%s' namespace: %w", secretName, namespace, err) + } + } + } + + // Watch for changes to primary resource ManagementCluster + if err = ccsController.WatchObject(&operatorv1.ManagementCluster{}, eventHandler); err != nil { + return fmt.Errorf("ccs-controller failed to watch primary resource: %w", err) + } + + // Watch for changes to primary resource ManagementClusterConnection + if err = ccsController.WatchObject(&operatorv1.ManagementClusterConnection{}, eventHandler); err != nil { + return fmt.Errorf("ccs-controller failed to watch primary resource: %w", err) + } + + if err = ccsController.WatchObject(&operatorv1.Authentication{}, eventHandler); err != nil { + return fmt.Errorf("ccs-controller failed to watch resource: %w", err) + } + + // Watch for changes to TigeraStatus. + if err = utils.AddTigeraStatusWatch(ccsController, ResourceName); err != nil { + return fmt.Errorf("ccs-controller failed to watch CCS Tigerastatus: %w", err) + } + + return nil +} + +// newReconciler returns a new *reconcile.Reconciler +func newReconciler(mgr manager.Manager, opts options.AddOptions, licenseAPIReady *utils.ReadyFlag, tierWatchReady *utils.ReadyFlag) reconcile.Reconciler { + r := &ReconcileCCS{ + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + provider: opts.DetectedProvider, + status: status.New(mgr.GetClient(), "ccs", opts.KubernetesVersion), + clusterDomain: opts.ClusterDomain, + licenseAPIReady: licenseAPIReady, + tierWatchReady: tierWatchReady, + multiTenant: opts.MultiTenant, + externalElastic: opts.ElasticExternal, + } + r.status.Run(opts.ShutdownContext) + return r +} + +// blank assignment to verify that ReconcileCCS reconcile.Reconciler +var _ reconcile.Reconciler = &ReconcileCCS{} + +type ReconcileCCS struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme + provider operatorv1.Provider + status status.StatusManager + clusterDomain string + licenseAPIReady *utils.ReadyFlag + tierWatchReady *utils.ReadyFlag + multiTenant bool + externalElastic bool +} + +func GetCCS(ctx context.Context, cli client.Client, mt bool, ns string) (*operatorv1.ComplianceConfigurationSecurity, error) { + key := client.ObjectKey{Name: "tigera-secure"} + if mt { + key.Namespace = ns + } + instance := &operatorv1.ComplianceConfigurationSecurity{} + err := cli.Get(ctx, key, instance) + if err != nil { + return nil, err + } + return instance, nil +} + +// Reconcile reads that state of the cluster for a CCS object and makes changes based on the state read +// and what is in the CCS.Spec +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *ReconcileCCS) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + helper := utils.NewNamespaceHelper(r.multiTenant, ccsrender.Namespace, request.Namespace) + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name, "installNS", helper.InstallNamespace(), "truthNS", helper.TruthNamespace()) + reqLogger.Info("Reconciling CCS") + + // We skip requests without a namespace specified in multi-tenant setups. + if r.multiTenant && request.Namespace == "" { + return reconcile.Result{}, nil + } + + // Check if this is a tenant-scoped request. + tenant, _, err := utils.GetTenant(ctx, r.multiTenant, r.client, request.Namespace) + if errors.IsNotFound(err) { + reqLogger.Info("No Tenant in this Namespace, skip") + return reconcile.Result{}, nil + } else if err != nil { + r.status.SetDegraded(operatorv1.ResourceReadError, "An error occurred while querying Tenant", err, reqLogger) + return reconcile.Result{}, err + } + + // Fetch the CCS instance + instance, err := GetCCS(ctx, r.client, r.multiTenant, request.Namespace) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + reqLogger.Info("ComplianceConfigurationSecurity not found") + r.status.OnCRNotFound() + return reconcile.Result{}, nil + } + r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying ccs", err, reqLogger) + return reconcile.Result{}, err + } + r.status.OnCRFound() + reqLogger.V(2).Info("Loaded config", "config", instance) + + // SetMetaData in the TigeraStatus such as observedGenerations. + defer r.status.SetMetaData(&instance.ObjectMeta) + + // Changes for updating CCSstatus conditions. + if request.Name == ResourceName && request.Namespace == "" { + ts := &operatorv1.TigeraStatus{} + err := r.client.Get(ctx, types.NamespacedName{Name: ResourceName}, ts) + if err != nil { + return reconcile.Result{}, err + } + //instance.Status.Conditions = status.UpdateStatusCondition(instance.Status.Conditions, ts.Status.Conditions) + if err := r.client.Status().Update(ctx, instance); err != nil { + log.WithValues("reason", err).Info("Failed to create CCSstatus conditions.") + return reconcile.Result{}, err + } + } + + if !utils.IsAPIServerReady(r.client, reqLogger) { + r.status.SetDegraded(operatorv1.ResourceNotReady, "Waiting for Tigera API server to be ready", nil, reqLogger) + return reconcile.Result{}, err + } + + // Validate that the tier watch is ready before querying the tier to ensure we utilize the cache. + if !r.tierWatchReady.IsReady() { + r.status.SetDegraded(operatorv1.ResourceNotReady, "Waiting for Tier watch to be established", err, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + + // Ensure the allow-tigera tier exists, before rendering any network policies within it. + if err := r.client.Get(ctx, client.ObjectKey{Name: networkpolicy.TigeraComponentTierName}, &v3.Tier{}); err != nil { + if errors.IsNotFound(err) { + r.status.SetDegraded(operatorv1.ResourceNotReady, "Waiting for allow-tigera tier to be created, see the 'tiers' TigeraStatus for more information", err, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } else { + log.Error(err, "Error querying allow-tigera tier") + r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying allow-tigera tier", err, reqLogger) + return reconcile.Result{}, err + } + } + + if !r.licenseAPIReady.IsReady() { + r.status.SetDegraded(operatorv1.ResourceNotReady, "Waiting for LicenseKeyAPI to be ready", nil, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + + license, err := utils.FetchLicenseKey(ctx, r.client) + if err != nil { + if errors.IsNotFound(err) { + r.status.SetDegraded(operatorv1.ResourceNotFound, "License not found", err, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying license", err, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + + // Query for the installation object. + variant, network, err := utils.GetInstallation(ctx, r.client) + if err != nil { + if errors.IsNotFound(err) { + r.status.SetDegraded(operatorv1.ResourceNotFound, "Installation not found", err, reqLogger) + return reconcile.Result{}, err + } + r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying installation", err, reqLogger) + return reconcile.Result{}, err + } + + pullSecrets, err := utils.GetNetworkingPullSecrets(network, r.client) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceReadError, "Failed to retrieve pull secrets", err, reqLogger) + return reconcile.Result{}, err + } + + managementCluster, err := utils.GetManagementCluster(ctx, r.client) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading ManagementCluster", err, reqLogger) + return reconcile.Result{}, err + } + + managementClusterConnection, err := utils.GetManagementClusterConnection(ctx, r.client) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading ManagementClusterConnection", err, reqLogger) + return reconcile.Result{}, err + } + + if managementClusterConnection != nil && managementCluster != nil { + err = fmt.Errorf("having both a ManagementCluster and a ManagementClusterConnection is not supported") + r.status.SetDegraded(operatorv1.ResourceValidationError, "", err, reqLogger) + return reconcile.Result{}, err + } + + var opts []certificatemanager.Option + opts = append(opts, certificatemanager.WithTenant(tenant), certificatemanager.WithLogger(reqLogger)) + certificateManager, err := certificatemanager.Create(r.client, network, r.clusterDomain, helper.TruthNamespace(), opts...) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceCreateError, "Unable to create the Tigera CA", err, reqLogger) + return reconcile.Result{}, err + } + var managerInternalTLSSecret certificatemanagement.CertificateInterface + if managementCluster != nil { + managerInternalTLSSecret, err = certificateManager.GetCertificate(r.client, commonrender.ManagerInternalTLSSecretName, helper.TruthNamespace()) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceValidationError, fmt.Sprintf("failed to retrieve / validate %s", commonrender.ManagerInternalTLSSecretName), err, reqLogger) + return reconcile.Result{}, err + } + } + + // The location of the Linseed certificate varies based on if this is a managed cluster or not. + // For standalone and management clusters, we just use Linseed's actual certificate. + linseedCertLocation := commonrender.TigeraLinseedSecret + if managementClusterConnection != nil { + // For managed clusters, we need to add the certificate of the Voltron endpoint. This certificate is copied from the + // management cluster into the managed cluster by kube-controllers. + linseedCertLocation = commonrender.VoltronLinseedPublicCert + } + linseedCertificate, err := certificateManager.GetCertificate(r.client, linseedCertLocation, helper.TruthNamespace()) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceValidationError, fmt.Sprintf("Failed to retrieve / validate %s", commonrender.TigeraLinseedSecret), err, reqLogger) + return reconcile.Result{}, err + } else if linseedCertificate == nil { + log.Info("Linseed certificate is not available yet, waiting until it becomes available") + r.status.SetDegraded(operatorv1.ResourceNotReady, "Linseed certificate is not available yet, waiting until it becomes available", nil, reqLogger) + return reconcile.Result{}, nil + } + bundleMaker := certificateManager.CreateTrustedBundle(managerInternalTLSSecret, linseedCertificate) + trustedBundle := bundleMaker.(certificatemanagement.TrustedBundleRO) + // TODO - Copied from compliance, verify if this is the case for CCS as well. + if r.multiTenant { + // For multi-tenant systems, we load the pre-created bundle for this tenant instead of using the one we built here. + trustedBundle, err = certificateManager.LoadMultiTenantTrustedBundleWithRootCertificates(ctx, r.client, helper.InstallNamespace()) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceReadError, "Error getting trusted bundle", err, reqLogger) + return reconcile.Result{}, err + } + bundleMaker = nil + } + + var apiKeyPair certificatemanagement.KeyPairInterface + // TODO: To support MCM cluster we need MCC components creating this separately into operator namespace. + // TODO: After creating MCM component we wait on these secrets. + // create tls key pair for ccs api. + apiKeyPair, err = certificateManager.GetOrCreateKeyPair( + r.client, + ccsrender.APICertSecretName, + helper.TruthNamespace(), + dns.GetServiceDNSNames(ccsrender.APIResourceName, helper.InstallNamespace(), r.clusterDomain)) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceValidationError, fmt.Sprintf("failed to retrieve / validate %s", ccsrender.APICertSecretName), err, reqLogger) + return reconcile.Result{}, err + } + certificateManager.AddToStatusManager(r.status, helper.InstallNamespace()) + + // Fetch the Authentication spec. If present, we use to configure user authentication. + authenticationCR, err := utils.GetAuthentication(ctx, r.client) + if err != nil && !errors.IsNotFound(err) { + r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying Authentication", err, reqLogger) + return reconcile.Result{}, err + } + if authenticationCR != nil && authenticationCR.Status.State != operatorv1.TigeraStatusReady { + r.status.SetDegraded(operatorv1.ResourceNotReady, fmt.Sprintf("Authentication is not ready - authenticationCR status: %s", authenticationCR.Status.State), nil, reqLogger) + return reconcile.Result{}, nil + } + + // Determine the namespaces to which we must bind the cluster role. + // For multi-tenant, the cluster role will be bind to the service account in the tenant namespace + bindNamespaces, err := helper.TenantNamespaces(r.client) + if err != nil { + return reconcile.Result{}, err + } + + keyValidatorConfig, err := utils.GetKeyValidatorConfig(ctx, r.client, authenticationCR, r.clusterDomain) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceValidationError, "Failed to process the authentication CR.", err, reqLogger) + return reconcile.Result{}, err + } + + reqLogger.V(3).Info("rendering components") + + namespaceComp := commonrender.NewPassthrough(commonrender.CreateNamespace(helper.InstallNamespace(), network.KubernetesProvider, commonrender.PSSBaseline, network.Azure)) + + hasNoLicense := !utils.IsFeatureActive(license, common.ComplianceFeature) + openshift := r.provider.IsOpenShift() + cfg := &ccsrender.Config{ + TrustedBundle: trustedBundle, + Installation: network, + APIKeyPair: apiKeyPair, + PullSecrets: pullSecrets, + OpenShift: openshift, + ManagementCluster: managementCluster, + ManagementClusterConnection: managementClusterConnection, + KeyValidatorConfig: keyValidatorConfig, + ClusterDomain: r.clusterDomain, + HasNoLicense: hasNoLicense, + Namespace: helper.InstallNamespace(), + BindingNamespaces: bindNamespaces, + Tenant: tenant, + ComplianceConfigurationSecurity: instance, + ExternalElastic: r.externalElastic, + } + + // Render the desired objects from the CRD and create or update them. + ccsComponent := ccsrender.CCS(cfg) + if err = imageset.ApplyImageSet(ctx, r.client, variant, ccsComponent); err != nil { + r.status.SetDegraded(operatorv1.ResourceUpdateError, "Error with images from ImageSet", err, reqLogger) + return reconcile.Result{}, err + } + certificateComponent := rcertificatemanagement.CertificateManagement(&rcertificatemanagement.Config{ + Namespace: helper.InstallNamespace(), + TruthNamespace: helper.TruthNamespace(), + ServiceAccounts: []string{ccsrender.APIResourceName}, + KeyPairOptions: []rcertificatemanagement.KeyPairOption{ + rcertificatemanagement.NewKeyPairOption(apiKeyPair, true, true), + }, + TrustedBundle: bundleMaker, + }) + + hdlr := utils.NewComponentHandler(log, r.client, r.scheme, instance) + for _, c := range []commonrender.Component{namespaceComp, certificateComponent, ccsComponent} { + if err := hdlr.CreateOrUpdateOrDelete(ctx, c, r.status); err != nil { + r.status.SetDegraded(operatorv1.ResourceUpdateError, "Error creating / updating / deleting resource", err, reqLogger) + return reconcile.Result{}, err + } + } + + if hasNoLicense { + log.V(4).Info("CCS is not activated as part of this license") + r.status.SetDegraded(operatorv1.ResourceValidationError, "Feature is not active - License does not support this feature", nil, reqLogger) + return reconcile.Result{}, nil + } + + // Clear the degraded bit if we've reached this far. + r.status.ClearDegraded() + + if !r.status.IsAvailable() { + // Schedule a kick to check again in the near future. Hopefully by then + // things will be available. + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + + // Everything is available - update the CRD status. + instance.Status.State = operatorv1.TigeraStatusReady + if err = r.client.Status().Update(ctx, instance); err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{}, nil +} diff --git a/pkg/controller/ccs/ccs_controller_test.go b/pkg/controller/ccs/ccs_controller_test.go new file mode 100644 index 0000000000..f6744addd3 --- /dev/null +++ b/pkg/controller/ccs/ccs_controller_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/stretchr/testify/mock" + v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/apis" + "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/controller/certificatemanager" + "github.com/tigera/operator/pkg/controller/status" + "github.com/tigera/operator/pkg/controller/utils" + ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake" + "github.com/tigera/operator/pkg/dns" + "github.com/tigera/operator/pkg/render" + ccsrender "github.com/tigera/operator/pkg/render/ccs" + + appsv1 "k8s.io/api/apps/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("CCS controller tests", func() { + var c client.Client + var ctx context.Context + var cr *operatorv1.ComplianceConfigurationSecurity + var r ReconcileCCS + var mockStatus *status.MockStatus + var scheme *runtime.Scheme + var installation *operatorv1.Installation + + BeforeEach(func() { + // The schema contains all objects that should be known to the fake client when the test runs. + scheme = runtime.NewScheme() + Expect(apis.AddToScheme(scheme)).NotTo(HaveOccurred()) + Expect(appsv1.SchemeBuilder.AddToScheme(scheme)).ShouldNot(HaveOccurred()) + Expect(rbacv1.SchemeBuilder.AddToScheme(scheme)).ShouldNot(HaveOccurred()) + Expect(operatorv1.SchemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) + + // Create a client that will have a crud interface of k8s objects. + c = ctrlrfake.DefaultFakeClientBuilder(scheme).Build() + ctx = context.Background() + + mockStatus = &status.MockStatus{} + mockStatus.On("AddDeployments", mock.Anything).Return() + mockStatus.On("RemoveDeployments", mock.Anything).Return() + mockStatus.On("RemoveCertificateSigningRequests", mock.Anything).Return() + mockStatus.On("IsAvailable").Return(true) + mockStatus.On("OnCRFound").Return() + mockStatus.On("OnCRNotFound").Return() + mockStatus.On("AddCertificateSigningRequests", mock.Anything).Return() + mockStatus.On("ClearDegraded") + mockStatus.On("SetDegraded", "Waiting for LicenseKeyAPI to be ready", "").Return().Maybe() + mockStatus.On("ReadyToMonitor") + mockStatus.On("SetMetaData", mock.Anything).Return() + + // Create an object we can use throughout the test to do the CCS reconcile loops. + // As the parameters in the client changes, we expect the outcomes of the reconcile loops to change. + r = ReconcileCCS{ + client: c, + scheme: scheme, + provider: operatorv1.ProviderNone, + status: mockStatus, + clusterDomain: dns.DefaultClusterDomain, + licenseAPIReady: &utils.ReadyFlag{}, + tierWatchReady: &utils.ReadyFlag{}, + } + // We start off with a 'standard' installation, with nothing special + installation = &operatorv1.Installation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: operatorv1.InstallationSpec{ + Variant: operatorv1.TigeraSecureEnterprise, + Registry: "some.registry.org/", + }, + Status: operatorv1.InstallationStatus{ + Variant: operatorv1.TigeraSecureEnterprise, + Computed: &operatorv1.InstallationSpec{ + Registry: "my-reg", + // The test is provider agnostic. + KubernetesProvider: operatorv1.ProviderNone, + }, + }, + } + Expect(c.Create(ctx, installation)).NotTo(HaveOccurred()) + + Expect(c.Create(ctx, &operatorv1.APIServer{ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"}, Status: operatorv1.APIServerStatus{State: operatorv1.TigeraStatusReady}})).NotTo(HaveOccurred()) + Expect(c.Create(ctx, &v3.Tier{ObjectMeta: metav1.ObjectMeta{Name: "allow-tigera"}})).NotTo(HaveOccurred()) + Expect(c.Create(ctx, &v3.LicenseKey{ObjectMeta: metav1.ObjectMeta{Name: "default"}, Status: v3.LicenseKeyStatus{Features: []string{common.ComplianceFeature}}})).NotTo(HaveOccurred()) + + certificateManager, err := certificatemanager.Create(c, nil, dns.DefaultClusterDomain, common.OperatorNamespace(), certificatemanager.AllowCACreation()) + Expect(err).NotTo(HaveOccurred()) + Expect(c.Create(context.Background(), certificateManager.KeyPair().Secret(common.OperatorNamespace()))).NotTo(HaveOccurred()) + + esDNSNames := dns.GetServiceDNSNames(render.TigeraElasticsearchGatewaySecret, render.ElasticsearchNamespace, dns.DefaultClusterDomain) + linseedKeyPair, err := certificateManager.GetOrCreateKeyPair(c, render.TigeraLinseedSecret, render.ElasticsearchNamespace, esDNSNames) + Expect(err).NotTo(HaveOccurred()) + + //For managed clusters, we also need the public cert for Linseed. + linseedPublicCert, err := certificateManager.GetOrCreateKeyPair(c, render.VoltronLinseedPublicCert, common.OperatorNamespace(), esDNSNames) + Expect(err).NotTo(HaveOccurred()) + + Expect(c.Create(ctx, linseedKeyPair.Secret(common.OperatorNamespace()))).NotTo(HaveOccurred()) + Expect(c.Create(ctx, linseedPublicCert.Secret(common.OperatorNamespace()))).NotTo(HaveOccurred()) + + // Apply the CCS CR to the fake cluster. + cr = &operatorv1.ComplianceConfigurationSecurity{ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"}} + Expect(c.Create(ctx, cr)).NotTo(HaveOccurred()) + + // Mark that watches were successful. + r.licenseAPIReady.MarkAsReady() + r.tierWatchReady.MarkAsReady() + }) + + It("should create resources for standalone clusters", func() { + By("reconciling when clustertype is Standalone") + result, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).NotTo(HaveOccurred()) + Expect(result.Requeue).NotTo(BeTrue()) + + dpl := appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{}} + + Expect(c.Get(ctx, client.ObjectKey{Name: ccsrender.APIResourceName, Namespace: ccsrender.Namespace}, &dpl)).Error().NotTo(HaveOccurred()) + Expect(dpl.Spec.Template.ObjectMeta.Name).To(Equal(ccsrender.APIResourceName)) + Expect(c.Get(ctx, client.ObjectKey{Name: ccsrender.ControllerResourceName, Namespace: ccsrender.Namespace}, &dpl)).Error().NotTo(HaveOccurred()) + Expect(dpl.Spec.Template.ObjectMeta.Name).To(Equal(ccsrender.ControllerResourceName)) + + }) + +}) diff --git a/pkg/controller/ccs/ccs_suit_test.go b/pkg/controller/ccs/ccs_suit_test.go new file mode 100644 index 0000000000..13e7d7d374 --- /dev/null +++ b/pkg/controller/ccs/ccs_suit_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/reporters" + . "github.com/onsi/gomega" +) + +func TestStatus(t *testing.T) { + RegisterFailHandler(Fail) + junitReporter := reporters.NewJUnitReporter("../../../report/ut/applicationlayer_controller_suite.xml") + RunSpecsWithDefaultAndCustomReporters(t, "pkg/controller/applicationlayer Controller Suite", []Reporter{junitReporter}) +} diff --git a/pkg/crds/operator/operator.tigera.io_complianceconfigurationsecurities.yaml b/pkg/crds/operator/operator.tigera.io_complianceconfigurationsecurities.yaml new file mode 100644 index 0000000000..cdf9d8e760 --- /dev/null +++ b/pkg/crds/operator/operator.tigera.io_complianceconfigurationsecurities.yaml @@ -0,0 +1,311 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: complianceconfigurationsecurities.operator.tigera.io +spec: + group: operator.tigera.io + names: + kind: ComplianceConfigurationSecurity + listKind: ComplianceConfigurationSecurityList + plural: complianceconfigurationsecurities + singular: complianceconfigurationsecurity + scope: Namespace + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ComplianceConfigurationSecurity installs the components required + for CCS reports. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Specification of the desired state for CCS. + properties: + ccsAPIDeployment: + description: This controls the deployment of the CCS API. + properties: + spec: + description: Spec is the specification of the CCS controller Deployment. + properties: + template: + description: Template describes the CCS controller Deployment + pod that will be created. + properties: + spec: + description: Spec is the CCS controller Deployment's PodSpec. + properties: + containers: + description: |- + Containers is a list of CCS controller containers. + If specified, this overrides the specified CCS controller Deployment containers. + If omitted, the CCS controller Deployment will use its default values for its containers. + items: + description: CCSAPIDeploymentContainer is a CCS + controller Deployment container. + properties: + name: + description: |- + Name is an enum which identifies the CCS controller Deployment container by name. + Supported values are: ccs-api + enum: + - ccs-api + type: string + resources: + description: |- + Resources allows customization of limits and requests for compute resources such as cpu and memory. + If specified, this overrides the named CCS controller Deployment container's resources. + If omitted, the CCS controller Deployment will use its default value for this container's resources. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references + one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + required: + - name + type: object + type: array + type: object + type: object + type: object + type: object + ccsControllerDeployment: + description: CCSControllerDeployment is the configuration for the + CCS controller Deployment. + properties: + spec: + description: Spec is the specification of the CCS controller Deployment. + properties: + template: + description: Template describes the CCS controller Deployment + pod that will be created. + properties: + spec: + description: Spec is the CCS controller Deployment's PodSpec. + properties: + containers: + description: |- + Containers is a list of CCS controller containers. + If specified, this overrides the specified CCS controller Deployment containers. + If omitted, the CCS controller Deployment will use its default values for its containers. + items: + description: CCSControllerDeploymentContainer is + a CCS controller Deployment container. + properties: + name: + description: |- + Name is an enum which identifies the CCS controller Deployment container by name. + Supported values are: ccs-controller + enum: + - ccs-controller + type: string + resources: + description: |- + Resources allows customization of limits and requests for compute resources such as cpu and memory. + If specified, this overrides the named CCS controller Deployment container's resources. + If omitted, the CCS controller Deployment will use its default value for this container's resources. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references + one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + required: + - name + type: object + type: array + type: object + type: object + type: object + type: object + type: object + status: + description: Most recently observed state for CCS. + properties: + conditions: + description: |- + Conditions represents the latest observed set of conditions for the component. A component may be one or more of + Ready, Progressing, Degraded or other customer types. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + state: + description: State provides user-readable status. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/render/ccs/api.go b/pkg/render/ccs/api.go new file mode 100644 index 0000000000..c446b7d0bd --- /dev/null +++ b/pkg/render/ccs/api.go @@ -0,0 +1,262 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + calicov3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + "github.com/tigera/operator/pkg/render" + rcomponents "github.com/tigera/operator/pkg/render/common/components" + "github.com/tigera/operator/pkg/render/common/networkpolicy" + "github.com/tigera/operator/pkg/render/common/secret" + "github.com/tigera/operator/pkg/render/common/securitycontext" + "github.com/tigera/operator/pkg/tls/certificatemanagement" +) + +const ( + APIResourceName = "tigera-ccs-api" + APICertSecretName = "tigera-ccs-api-tls" + + APIAccessPolicyName = networkpolicy.TigeraComponentPolicyPrefix + "ccs-api-access" + ControllerAccessPolicyName = networkpolicy.TigeraComponentPolicyPrefix + "ccs-controller-access" +) + +func (c *component) apiServiceAccount() *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{Kind: "ServiceAccount", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: APIResourceName, Namespace: c.cfg.Namespace}, + } +} + +func (c *component) apiRole() *rbacv1.Role { + return &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{Kind: "Role", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: APIResourceName, Namespace: c.cfg.Namespace}, + Rules: []rbacv1.PolicyRule{}, + } +} + +func (c *component) apiRoleBinding() *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{Kind: "RoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: APIResourceName, Namespace: c.cfg.Namespace}, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: APIResourceName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: APIResourceName, + Namespace: c.cfg.Namespace, + }, + }, + } +} + +func (c *component) apiClusterRole() *rbacv1.ClusterRole { + return &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: APIResourceName}, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"linseed.tigera.io"}, + Resources: []string{"ccsresults", "ccsruns"}, + Verbs: []string{"get", "create"}, + }, + { + APIGroups: []string{"authorization.k8s.io"}, + Resources: []string{"subjectaccessreviews"}, + Verbs: []string{"create"}, + }, + { + APIGroups: []string{"authentication.k8s.io"}, + Resources: []string{"tokenreviews"}, + Verbs: []string{"create"}, + }, + }, + } +} + +func (c *component) apiClusterRoleBinding() *rbacv1.ClusterRoleBinding { + return &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: APIResourceName}, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: APIResourceName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: APIResourceName, + Namespace: c.cfg.Namespace, + }, + }, + } +} + +func (c *component) apiDeployment() *appsv1.Deployment { + var keyPath, certPath string + if c.cfg.APIKeyPair != nil { + keyPath, certPath = c.cfg.APIKeyPair.VolumeMountKeyFilePath(), c.cfg.APIKeyPair.VolumeMountCertificateFilePath() + } + + envVars := []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "trace"}, + {Name: "HTTPS_ENABLED", Value: "true"}, + {Name: "HTTPS_CERT", Value: certPath}, + {Name: "HTTPS_KEY", Value: keyPath}, + {Name: "LINSEED_CLIENT_CERT", Value: certPath}, + {Name: "LINSEED_CLIENT_KEY", Value: keyPath}, + {Name: "LINSEED_URL", Value: "https://tigera-linseed.tigera-elasticsearch.svc"}, + {Name: "LINSEED_CA", Value: "/etc/pki/tls/certs/tigera-ca-bundle.crt"}, + {Name: "LINSEED_TOKEN", Value: render.GetLinseedTokenPath(c.cfg.ManagementClusterConnection != nil)}, + {Name: "RESOURCE_AUTHORIZATION_MODE", Value: "k8s_rbac"}, + {Name: "MULTI_CLUSTER_FORWARDING_CA", Value: certificatemanagement.TrustedCertBundleMountPath}, + } + + if c.cfg.Tenant != nil { + // Configure the tenant id in order to read /write linseed data using the correct tenant ID + // Multi-tenant and single tenant with external elastic needs this variable set + if c.cfg.ExternalElastic { + envVars = append(envVars, corev1.EnvVar{Name: "TENANT_ID", Value: c.cfg.Tenant.Spec.ID}) + } + if c.cfg.Tenant.MultiTenant() { + envVars = append(envVars, corev1.EnvVar{Name: "TENANT_NAMESPACE", Value: c.cfg.Tenant.Namespace}) + envVars = append(envVars, corev1.EnvVar{Name: "LINSEED_URL", Value: fmt.Sprintf("https://tigera-linseed.%s.svc", c.cfg.Tenant.Namespace)}) + envVars = append(envVars, corev1.EnvVar{Name: "MULTI_CLUSTER_FORWARDING_ENDPOINT", Value: render.ManagerService(c.cfg.Tenant)}) + } + } + + annots := c.cfg.TrustedBundle.HashAnnotations() + if c.cfg.APIKeyPair != nil { + annots[c.cfg.APIKeyPair.HashAnnotationKey()] = c.cfg.APIKeyPair.HashAnnotationValue() + } + + podTemplate := &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: APIResourceName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{"k8s-app": APIResourceName}, + Annotations: annots, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: APIResourceName, + NodeSelector: c.cfg.Installation.ControlPlaneNodeSelector, + ImagePullSecrets: secret.GetReferenceList(c.cfg.PullSecrets), + Containers: []corev1.Container{ + { + Name: APIResourceName, + Image: c.apiImage, + ImagePullPolicy: render.ImagePullPolicy(), + Env: envVars, + Ports: []corev1.ContainerPort{{ContainerPort: 5557}}, + SecurityContext: securitycontext.NewNonRootContext(), + VolumeMounts: append( + c.cfg.TrustedBundle.VolumeMounts(c.SupportedOSType()), + c.cfg.APIKeyPair.VolumeMount(c.SupportedOSType()), + ), + }, + }, + RestartPolicy: corev1.RestartPolicyAlways, + Volumes: []corev1.Volume{ + c.cfg.APIKeyPair.Volume(), + c.cfg.TrustedBundle.Volume(), + }, + }, + } + + d := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: APIResourceName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{"k8s-app": APIResourceName}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"k8s-app": APIResourceName}, + }, + Template: *podTemplate, + }, + } + + if c.cfg.ComplianceConfigurationSecurity != nil { + if overrides := c.cfg.ComplianceConfigurationSecurity.Spec.CCSAPIDeployment; overrides != nil { + rcomponents.ApplyDeploymentOverrides(d, overrides) + } + } + + return d +} + +func (c *component) apiService() *corev1.Service { + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: APIResourceName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{"k8s-app": APIResourceName}, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{"k8s-app": APIResourceName}, + Ports: []corev1.ServicePort{ + { + Protocol: corev1.ProtocolTCP, + Port: 443, + TargetPort: intstr.FromInt32(5557), + }, + }, + Type: corev1.ServiceTypeClusterIP, + }, + } +} + +func (c *component) apiAllowTigeraNetworkPolicy() *calicov3.NetworkPolicy { + _ = networkpolicy.Helper(c.cfg.Tenant.MultiTenant(), c.cfg.Namespace) + return &calicov3.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{Kind: "NetworkPolicy", APIVersion: "projectcalico.org/v3"}, + ObjectMeta: metav1.ObjectMeta{ + Name: APIAccessPolicyName, + Namespace: c.cfg.Namespace, + }, + Spec: calicov3.NetworkPolicySpec{ + Order: &networkpolicy.HighPrecedenceOrder, + Tier: networkpolicy.TigeraComponentTierName, + Selector: networkpolicy.KubernetesAppSelector(APIResourceName), + Types: []calicov3.PolicyType{calicov3.PolicyTypeIngress, calicov3.PolicyTypeEgress}, + Ingress: []calicov3.Rule{ + { + Action: calicov3.Allow, + }, + }, + Egress: []calicov3.Rule{ + { + Action: calicov3.Allow, + }, + }, + }, + } +} diff --git a/pkg/render/ccs/ccs.go b/pkg/render/ccs/ccs.go new file mode 100644 index 0000000000..9689a68b9d --- /dev/null +++ b/pkg/render/ccs/ccs.go @@ -0,0 +1,141 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/components" + "github.com/tigera/operator/pkg/render" + "github.com/tigera/operator/pkg/render/common/authentication" + rmeta "github.com/tigera/operator/pkg/render/common/meta" + "github.com/tigera/operator/pkg/render/common/secret" + "github.com/tigera/operator/pkg/tls/certificatemanagement" +) + +const ( + Namespace = "tigera-compliance" +) + +func CCS(cfg *Config) render.Component { + return &component{ + cfg: cfg, + } +} + +type component struct { + cfg *Config + apiImage string + controllerImage string + + hostScannerConfigMap *corev1.ConfigMap + hostScannerInputsConfigMap *corev1.ConfigMap +} + +// Config contains all the config information needed to render the component. +type Config struct { + Installation *operatorv1.InstallationSpec + PullSecrets []*corev1.Secret + OpenShift bool + ManagementCluster *operatorv1.ManagementCluster + ManagementClusterConnection *operatorv1.ManagementClusterConnection + KeyValidatorConfig authentication.KeyValidatorConfig + ClusterDomain string + HasNoLicense bool + + // Trusted certificate bundle for all ccs pods. + TrustedBundle certificatemanagement.TrustedBundleRO + APIKeyPair certificatemanagement.KeyPairInterface + + Namespace string + BindingNamespaces []string + + // Whether to run the rendered components in multi-tenant, single-tenant, or zero-tenant mode + Tenant *operatorv1.Tenant + ExternalElastic bool + ComplianceConfigurationSecurity *operatorv1.ComplianceConfigurationSecurity +} + +func (c *component) ResolveImages(is *operatorv1.ImageSet) error { + reg := c.cfg.Installation.Registry + path := c.cfg.Installation.ImagePath + prefix := c.cfg.Installation.ImagePrefix + + var err error + errMsgs := []string{} + c.apiImage, err = components.GetReference(components.ComponentCCSAPI, reg, path, prefix, is) + if err != nil { + errMsgs = append(errMsgs, err.Error()) + } + + c.controllerImage, err = components.GetReference(components.ComponentCCSController, reg, path, prefix, is) + if err != nil { + errMsgs = append(errMsgs, err.Error()) + } + + if len(errMsgs) != 0 { + return fmt.Errorf("%s", strings.Join(errMsgs, ",")) + } + return nil +} + +func (c *component) SupportedOSType() rmeta.OSType { + return rmeta.OSTypeLinux +} + +func (c *component) Objects() ([]client.Object, []client.Object) { + var objs []client.Object + + objs = append(objs, secret.ToRuntimeObjects(secret.CopyToNamespace(c.cfg.Namespace, c.cfg.PullSecrets...)...)...) + + objs = append(objs, + c.apiServiceAccount(), + c.apiRole(), + c.apiRoleBinding(), + c.apiClusterRole(), + c.apiClusterRoleBinding(), + c.apiDeployment(), + c.apiService(), + // TODO: the policy is broad but works. + c.apiAllowTigeraNetworkPolicy(), + ) + + c.hostScannerConfigMap = c.hostScannerYamlConfigMap() + c.hostScannerInputsConfigMap = c.hostScannerDefaultConfigInputsConfigMap() + + objs = append(objs, + c.controllerServiceAccount(), + c.controllerRole(), + c.controllerRoleBinding(), + c.controllerClusterRole(), + c.controllerClusterRoleBinding(), + c.hostScannerYamlConfigMap(), + c.hostScannerDefaultConfigInputsConfigMap(), + c.controllerDeployment(), + // TODO: the policy is broad but works. + c.controllerAllowTigeraNetworkPolicy(), + ) + + return objs, nil +} + +func (c *component) Ready() bool { + return true +} diff --git a/pkg/render/ccs/ccs_suit_test.go b/pkg/render/ccs/ccs_suit_test.go new file mode 100644 index 0000000000..39f36e0b94 --- /dev/null +++ b/pkg/render/ccs/ccs_suit_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs + +import ( + "testing" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/reporters" + . "github.com/onsi/gomega" +) + +func TestRender(t *testing.T) { + RegisterFailHandler(Fail) + junitReporter := reporters.NewJUnitReporter("../../../../report/ccs_suite.xml") + RunSpecsWithDefaultAndCustomReporters(t, "pkg/ccs/CCS Suite", []Reporter{junitReporter}) +} diff --git a/pkg/render/ccs/ccs_test.go b/pkg/render/ccs/ccs_test.go new file mode 100644 index 0000000000..4d07d154af --- /dev/null +++ b/pkg/render/ccs/ccs_test.go @@ -0,0 +1,198 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/tigera/operator/test" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/apis" + "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/controller/certificatemanager" + ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake" + "github.com/tigera/operator/pkg/render/ccs" + rtest "github.com/tigera/operator/pkg/render/common/test" +) + +var _ = Describe("Tigera Secure CCS rendering tests", func() { + var ( + cfg *ccs.Config + cli client.Client + ) + + ccsResources := corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "memory": resource.MustParse("300Mi"), + "storage": resource.MustParse("20Gi"), + }, + Requests: corev1.ResourceList{ + "cpu": resource.MustParse("1"), + "memory": resource.MustParse("150Mi"), + "storage": resource.MustParse("10Gi"), + }, + } + + BeforeEach(func() { + scheme := runtime.NewScheme() + Expect(apis.AddToScheme(scheme)).NotTo(HaveOccurred()) + cli = ctrlrfake.DefaultFakeClientBuilder(scheme).Build() + + certificateManager, err := certificatemanager.Create(cli, nil, "cluster.local", common.OperatorNamespace(), certificatemanager.AllowCACreation()) + Expect(err).NotTo(HaveOccurred()) + + bundle := certificateManager.CreateTrustedBundle() + apiKP, err := certificateManager.GetOrCreateKeyPair(cli, ccs.APICertSecretName, common.OperatorNamespace(), []string{""}) + Expect(err).NotTo(HaveOccurred()) + + cfg = &ccs.Config{ + Installation: &operatorv1.InstallationSpec{ + KubernetesProvider: operatorv1.ProviderNone, + Registry: "testregistry.com/", + }, + OpenShift: false, + ClusterDomain: "cluster.local", + TrustedBundle: bundle, + Namespace: ccs.Namespace, + APIKeyPair: apiKP, + } + }) + + It("should render with default (standalone) ccs configuration", func() { + expectedResources := []struct { + name string + ns string + group string + version string + kind string + }{ + // api resources + {name: ccs.APIResourceName, ns: ccs.Namespace, group: "", version: "v1", kind: "ServiceAccount"}, + {name: ccs.APIResourceName, ns: ccs.Namespace, group: "rbac.authorization.k8s.io", version: "v1", kind: "Role"}, + {name: ccs.APIResourceName, ns: ccs.Namespace, group: "rbac.authorization.k8s.io", version: "v1", kind: "RoleBinding"}, + {name: ccs.APIResourceName, group: "rbac.authorization.k8s.io", version: "v1", kind: "ClusterRole"}, + {name: ccs.APIResourceName, group: "rbac.authorization.k8s.io", version: "v1", kind: "ClusterRoleBinding"}, + {name: ccs.APIResourceName, ns: ccs.Namespace, group: "apps", version: "v1", kind: "Deployment"}, + {name: ccs.APIResourceName, ns: ccs.Namespace, group: "", version: "v1", kind: "Service"}, + {name: ccs.APIAccessPolicyName, ns: ccs.Namespace, group: "projectcalico.org", version: "v3", kind: "NetworkPolicy"}, + + // controller resources + {name: ccs.ControllerResourceName, ns: ccs.Namespace, group: "", version: "v1", kind: "ServiceAccount"}, + {name: ccs.ControllerResourceName, ns: ccs.Namespace, group: "rbac.authorization.k8s.io", version: "v1", kind: "Role"}, + {name: ccs.ControllerResourceName, ns: ccs.Namespace, group: "rbac.authorization.k8s.io", version: "v1", kind: "RoleBinding"}, + {name: ccs.ControllerResourceName, group: "rbac.authorization.k8s.io", version: "v1", kind: "ClusterRole"}, + {name: ccs.ControllerResourceName, group: "rbac.authorization.k8s.io", version: "v1", kind: "ClusterRoleBinding"}, + {name: ccs.HostScannerConfigName, ns: ccs.Namespace, group: "", version: "v1", kind: "ConfigMap"}, + {name: ccs.ControllerResourceName, ns: ccs.Namespace, group: "apps", version: "v1", kind: "Deployment"}, + {name: ccs.ControllerAccessPolicyName, ns: ccs.Namespace, group: "projectcalico.org", version: "v3", kind: "NetworkPolicy"}, + } + // Should render the correct resources. + component := ccs.CCS(cfg) + resources, _ := component.Objects() + Expect(resources).To(HaveLen(len(expectedResources))) + + for i, expectedRes := range expectedResources { + rtest.ExpectResourceTypeAndObjectMetadata(resources[i], expectedRes.name, expectedRes.ns, expectedRes.group, expectedRes.version, expectedRes.kind) + } + + // Check rendering of api deployment. + d := rtest.GetResource(resources, ccs.APIResourceName, ccs.Namespace, "apps", "v1", "Deployment").(*appsv1.Deployment) + Expect(d.Spec.Template.Spec.Containers).To(HaveLen(1)) + api := d.Spec.Template.Spec.Containers[0] + + apienvs := api.Env + expectedEnvs := []corev1.EnvVar{ + {Name: "LINSEED_CLIENT_KEY", Value: "/tigera-ccs-api-tls/tls.key"}, + {Name: "LINSEED_CLIENT_CERT", Value: "/tigera-ccs-api-tls/tls.crt"}, + {Name: "HTTPS_CERT", Value: "/tigera-ccs-api-tls/tls.crt"}, + {Name: "HTTPS_KEY", Value: "/tigera-ccs-api-tls/tls.key"}, + {Name: "RESOURCE_AUTHORIZATION_MODE", Value: "k8s_rbac"}, + {Name: "MULTI_CLUSTER_FORWARDING_CA", Value: "/etc/pki/tls/certs/tigera-ca-bundle.crt"}, + {Name: "LINSEED_CA", Value: "/etc/pki/tls/certs/tigera-ca-bundle.crt"}, + } + for _, expected := range expectedEnvs { + Expect(apienvs).To(ContainElement(expected)) + } + + c := rtest.GetResource(resources, ccs.ControllerResourceName, ccs.Namespace, "apps", "v1", "Deployment").(*appsv1.Deployment) + Expect(c.Spec.Template.Spec.Containers).To(HaveLen(1)) + controller := c.Spec.Template.Spec.Containers[0] + + controllerenvs := controller.Env + controllerExpectedEnvs := []corev1.EnvVar{ + {Name: "CCS_API_CA", Value: "/tigera-ccs-api-tls/tls.crt"}, + {Name: "CCS_API_TOKEN", Value: "/var/run/secrets/kubernetes.io/serviceaccount/token"}, + {Name: "CCS_HOST_SCANNER_YAML_PATH", Value: "/etc/ccs/host-scanner.yaml"}, + } + for _, expected := range controllerExpectedEnvs { + Expect(controllerenvs).To(ContainElement(expected)) + } + }) + + It("should render resource requests and limits for ccs components when set", func() { + cfg.ComplianceConfigurationSecurity = &operatorv1.ComplianceConfigurationSecurity{ + Spec: operatorv1.ComplianceConfigurationSecuritySpec{ + CCSAPIDeployment: &operatorv1.CCSAPIDeployment{ + Spec: &operatorv1.CCSAPIDeploymentSpec{ + Template: &operatorv1.CCSAPIDeploymentPodTemplateSpec{ + Spec: &operatorv1.CCSAPIDeploymentPodSpec{ + Containers: []operatorv1.CCSAPIDeploymentContainer{{ + Name: "tigera-ccs-api", + Resources: &ccsResources, + }}, + }, + }, + }, + }, + CCSControllerDeployment: &operatorv1.CCSControllerDeployment{ + Spec: &operatorv1.CCSControllerDeploymentSpec{ + Template: &operatorv1.CCSControllerDeploymentPodTemplateSpec{ + Spec: &operatorv1.CCSControllerDeploymentPodSpec{ + Containers: []operatorv1.CCSControllerDeploymentContainer{{ + Name: "tigera-ccs-controller", + Resources: &ccsResources, + }}, + }, + }, + }, + }, + }, + } + + component := ccs.CCS(cfg) + resources, _ := component.Objects() + d, ok := rtest.GetResource(resources, "tigera-ccs-api", "tigera-compliance", "apps", "v1", "Deployment").(*appsv1.Deployment) + Expect(ok).To(BeTrue()) + Expect(d.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := test.GetContainer(d.Spec.Template.Spec.Containers, "tigera-ccs-api") + Expect(container).NotTo(BeNil()) + Expect(container.Resources).To(Equal(ccsResources)) + + d, ok = rtest.GetResource(resources, "tigera-ccs-controller", "tigera-compliance", "apps", "v1", "Deployment").(*appsv1.Deployment) + Expect(ok).To(BeTrue()) + Expect(d.Spec.Template.Spec.Containers).To(HaveLen(1)) + container = test.GetContainer(d.Spec.Template.Spec.Containers, "tigera-ccs-controller") + Expect(container).NotTo(BeNil()) + Expect(container.Resources).To(Equal(ccsResources)) + }) + +}) diff --git a/pkg/render/ccs/controller.go b/pkg/render/ccs/controller.go new file mode 100644 index 0000000000..0aa9d4804b --- /dev/null +++ b/pkg/render/ccs/controller.go @@ -0,0 +1,412 @@ +// Copyright (c) 2025 Tigera, Inc. All rights reserved. + +// 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 ccs + +import ( + "bytes" + _ "embed" + "fmt" + "text/template" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + calicov3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + + "github.com/tigera/operator/pkg/render" + rcomponents "github.com/tigera/operator/pkg/render/common/components" + rmeta "github.com/tigera/operator/pkg/render/common/meta" + "github.com/tigera/operator/pkg/render/common/networkpolicy" + "github.com/tigera/operator/pkg/render/common/secret" + "github.com/tigera/operator/pkg/render/common/securitycontext" +) + +const ( + ControllerResourceName = "tigera-ccs-controller" + HostScannerConfigName = "tigera-ccs-host-scanner-config" + ScannerControlsConfigConfigMapName = "tigera-ccs-default-config-inputs" + ScannerControlsConfigConfigMapKey = "default-config-inputs.json" + HostScannerConfigKey = "host-scanner.yaml" + HostScannerConfigMountPath = "/etc/ccs" + ScannerControlsConfigMountPath = "/etc/ccs" + HostScannerYamlPath = HostScannerConfigMountPath + "/" + HostScannerConfigKey +) + +func (c *component) controllerServiceAccount() *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{Kind: "ServiceAccount", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: ControllerResourceName, Namespace: c.cfg.Namespace}, + } +} + +func (c *component) controllerRole() *rbacv1.Role { + return &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{Kind: "Role", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: ControllerResourceName, Namespace: c.cfg.Namespace}, + Rules: []rbacv1.PolicyRule{}, + } +} + +func (c *component) controllerRoleBinding() *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{Kind: "RoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: ControllerResourceName, Namespace: c.cfg.Namespace}, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: ControllerResourceName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ControllerResourceName, + Namespace: c.cfg.Namespace, + }, + }, + } +} + +func (c *component) controllerClusterRole() *rbacv1.ClusterRole { + // Set of permissions for kubescape host sensor. + rules := []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{ + "pods", "pods/proxy", "namespaces", "secrets", "nodes", "configmaps", + "services", "serviceaccounts", "endpoints", "persistentvolumes", + "persistentvolumeclaims", "limitranges", "replicationcontrollers", + "podtemplates", "resourcequotas", "events", + }, + Verbs: []string{"get", "watch", "list"}, + }, + // TODO : namespace can be removed once we update the yaml + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"update", "create", "delete"}, + }, + { + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"mutatingwebhookconfigurations", "validatingwebhookconfigurations"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"apiregistration.k8s.io"}, + Resources: []string{"apiservices"}, + Verbs: []string{"get", "watch", "list"}, + }, + // TODO : create may be removed have to check + { + APIGroups: []string{"apps"}, + Resources: []string{"deployments", "statefulsets", "daemonsets", "replicasets", "controllerrevisions"}, + Verbs: []string{"get", "watch", "list", "create", "update", "delete"}, + }, + { + APIGroups: []string{"autoscaling"}, + Resources: []string{"horizontalpodautoscalers"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"batch"}, + Resources: []string{"jobs", "cronjobs"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"coordination.k8s.io"}, + Resources: []string{"leases"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"discovery.k8s.io"}, + Resources: []string{"endpointslices"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"events.k8s.io"}, + Resources: []string{"events"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"hostdata.kubescape.cloud"}, + Resources: []string{"APIServerInfo", "ControlPlaneInfo"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"networking.k8s.io"}, + Resources: []string{"networkpolicies", "ingresses"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"policy"}, + Resources: []string{"poddisruptionbudgets", "podsecuritypolicies", "PodSecurityPolicy"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"clusterroles", "clusterrolebindings", "roles", "rolebindings"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"storage.k8s.io"}, + Resources: []string{"csistoragecapacities", "storageclasses"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"extensions"}, + Resources: []string{"ingresses"}, + Verbs: []string{"get", "watch", "list"}, + }, + { + APIGroups: []string{"spdx.softwarecomposition.kubescape.io"}, + Resources: []string{"workloadconfigurationscans", "workloadconfigurationscansummaries"}, + Verbs: []string{"create", "update", "patch"}, + }, + } + + // Add the rules for the CCS controller. + rules = append(rules, []rbacv1.PolicyRule{ + { + APIGroups: []string{"alphaccs.projectcalico.org"}, + Resources: []string{"frameworks"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{"ccs.tigera.io"}, + Resources: []string{"runs"}, + Verbs: []string{"get", "create", "update"}, + }, + { + APIGroups: []string{"ccs.tigera.io"}, + Resources: []string{"results"}, + Verbs: []string{"create"}, + }, + }...) + + return &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: ControllerResourceName}, + Rules: rules, + } + +} + +func (c *component) controllerClusterRoleBinding() *rbacv1.ClusterRoleBinding { + return &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: ControllerResourceName}, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: ControllerResourceName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ControllerResourceName, + Namespace: c.cfg.Namespace, + }, + }, + } +} + +func (c *component) controllerDeployment() *appsv1.Deployment { + var certPath string + if c.cfg.APIKeyPair != nil { + certPath = c.cfg.APIKeyPair.VolumeMountCertificateFilePath() + } + + envVars := []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "CCS_API_CA", Value: certPath}, + {Name: "CCS_API_TOKEN", Value: "/var/run/secrets/kubernetes.io/serviceaccount/token"}, + {Name: "CCS_HOST_SCANNER_YAML_PATH", Value: HostScannerYamlPath}, + } + + if c.cfg.Tenant != nil && c.cfg.Tenant.MultiTenant() { + envVars = append(envVars, corev1.EnvVar{Name: "CCS_API_URL", Value: fmt.Sprintf("https://tigera-ccs-api.%s.svc", c.cfg.Tenant.Namespace)}) + } else { + envVars = append(envVars, corev1.EnvVar{Name: "CCS_API_URL", Value: "https://tigera-ccs-api.tigera-compliance.svc"}) + } + + annots := c.cfg.TrustedBundle.HashAnnotations() + // Add the hash of the host scanner controls config map to the annotations so that the controller will be restarted if the config changes. + annots[ScannerControlsConfigConfigMapKey] = rmeta.AnnotationHash(c.hostScannerInputsConfigMap) + if c.cfg.APIKeyPair != nil { + annots[c.cfg.APIKeyPair.HashAnnotationKey()] = c.cfg.APIKeyPair.HashAnnotationValue() + } + + podTemplate := &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: ControllerResourceName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{"k8s-app": APIResourceName}, + Annotations: annots, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: ControllerResourceName, + NodeSelector: c.cfg.Installation.ControlPlaneNodeSelector, + ImagePullSecrets: secret.GetReferenceList(c.cfg.PullSecrets), + Containers: []corev1.Container{ + { + Name: ControllerResourceName, + Image: "gcr.io/unique-caldron-775/suresh/ccs-controller:operator-v9", // TODO c.controllerImage, + ImagePullPolicy: render.ImagePullPolicy(), + Env: envVars, + SecurityContext: securitycontext.NewNonRootContext(), + VolumeMounts: c.controllerVolumeMounts(), + }, + }, + Volumes: c.controllerVolumes(), + }, + } + + d := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: ControllerResourceName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{"k8s-app": ControllerResourceName}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"k8s-app": ControllerResourceName}, + }, + Template: *podTemplate, + }, + } + + if c.cfg.ComplianceConfigurationSecurity != nil { + if overrides := c.cfg.ComplianceConfigurationSecurity.Spec.CCSControllerDeployment; overrides != nil { + rcomponents.ApplyDeploymentOverrides(d, overrides) + } + } + return d +} + +func (c *component) controllerVolumeMounts() []corev1.VolumeMount { + vms := []corev1.VolumeMount{ + c.cfg.APIKeyPair.VolumeMount(c.SupportedOSType()), + } + vms = append(vms, c.cfg.TrustedBundle.VolumeMounts(c.SupportedOSType())...) + + vms = append(vms, corev1.VolumeMount{ + Name: HostScannerConfigName, + ReadOnly: true, + MountPath: HostScannerConfigMountPath, + }) + vms = append(vms, corev1.VolumeMount{ + Name: ScannerControlsConfigConfigMapName, + ReadOnly: true, + MountPath: ScannerControlsConfigMountPath, + }) + + return vms +} + +func (c *component) controllerVolumes() []corev1.Volume { + volumes := []corev1.Volume{c.cfg.APIKeyPair.Volume(), c.cfg.TrustedBundle.Volume()} + volumes = append(volumes, corev1.Volume{ + Name: HostScannerConfigName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: HostScannerConfigName}, + }, + }, + }) + volumes = append(volumes, corev1.Volume{ + Name: ScannerControlsConfigConfigMapName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: ScannerControlsConfigConfigMapName}, + }, + }, + }) + return volumes +} + +func (c *component) controllerAllowTigeraNetworkPolicy() *calicov3.NetworkPolicy { + _ = networkpolicy.Helper(c.cfg.Tenant.MultiTenant(), c.cfg.Namespace) + return &calicov3.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{Kind: "NetworkPolicy", APIVersion: "projectcalico.org/v3"}, + ObjectMeta: metav1.ObjectMeta{ + Name: ControllerAccessPolicyName, + Namespace: c.cfg.Namespace, + }, + Spec: calicov3.NetworkPolicySpec{ + Order: &networkpolicy.HighPrecedenceOrder, + Tier: networkpolicy.TigeraComponentTierName, + Selector: networkpolicy.KubernetesAppSelector(ControllerResourceName), + Types: []calicov3.PolicyType{calicov3.PolicyTypeIngress, calicov3.PolicyTypeEgress}, + Ingress: []calicov3.Rule{ + { + Action: calicov3.Allow, + }, + }, + Egress: []calicov3.Rule{ + { + Action: calicov3.Allow, + }, + }, + }, + } +} + +//go:embed host-scanner.yaml.template +var hostScannerConfigTemplate string + +func (c *component) hostScannerYamlConfigMap() *corev1.ConfigMap { + var config bytes.Buffer + + tpl, err := template.New("hostScannerConfigTemplate").Parse(hostScannerConfigTemplate) + if err != nil { + return nil + } + + err = tpl.Execute(&config, c.cfg) + if err != nil { + return nil + } + + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: HostScannerConfigName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{}, + }, + Data: map[string]string{ + HostScannerConfigKey: config.String(), + }, + } +} + +//go:embed default-config-inputs.json +var defaultConfigInputs string + +func (c *component) hostScannerDefaultConfigInputsConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: ScannerControlsConfigConfigMapName, + Namespace: c.cfg.Namespace, + Labels: map[string]string{}, + }, + Data: map[string]string{ + ScannerControlsConfigConfigMapKey: defaultConfigInputs, + }, + } +} diff --git a/pkg/render/ccs/default-config-inputs.json b/pkg/render/ccs/default-config-inputs.json new file mode 100644 index 0000000000..a51cac2013 --- /dev/null +++ b/pkg/render/ccs/default-config-inputs.json @@ -0,0 +1,142 @@ +{ + "name": "default", + "attributes": {}, + "scope": { + "designatorType": "attributes", + "attributes": {} + }, + "settings": { + "postureControlInputs": { + "imageRepositoryAllowList": [], + "trustedCosignPublicKeys": [], + "insecureCapabilities": [ + "SETPCAP", + "NET_ADMIN", + "NET_RAW", + "SYS_MODULE", + "SYS_RAWIO", + "SYS_PTRACE", + "SYS_ADMIN", + "SYS_BOOT", + "MAC_OVERRIDE", + "MAC_ADMIN", + "PERFMON", + "ALL", + "BPF" + ], + "listOfDangerousArtifacts": [ + "bin/bash", + "sbin/sh", + "bin/ksh", + "bin/tcsh", + "bin/zsh", + "usr/bin/scsh", + "bin/csh", + "bin/busybox", + "usr/bin/busybox" + ], + "publicRegistries": [], + "sensitiveInterfaces": [ + "nifi", + "argo-server", + "weave-scope-app", + "kubeflow", + "kubernetes-dashboard", + "jenkins", + "prometheus-deployment" + ], + "max_critical_vulnerabilities": [ + "5" + ], + "max_high_vulnerabilities": [ + "10" + ], + "sensitiveKeyNames": [ + "aws_secret_access_key", + "azure_batchai_storage_key", + "azure_batch_key", + "secret", + "key", + "password", + "pwd", + "token", + "jwt", + "bearer", + "credential" + ], + "sensitiveValues": [ + "BEGIN \\w+ PRIVATE KEY", + "PRIVATE KEY", + "eyJhbGciO", + "JWT", + "Bearer", + "_key_", + "_secret_" + ], + "sensitiveKeyNamesAllowed": [], + "sensitiveValuesAllowed": [], + "servicesNames": [ + "nifi-service", + "argo-server", + "minio", + "postgres", + "workflow-controller-metrics", + "weave-scope-app", + "kubernetes-dashboard" + ], + "untrustedRegistries": [], + "memory_request_max": [], + "memory_request_min": [ + "0" + ], + "memory_limit_max": [], + "memory_limit_min": [ + "0" + ], + "cpu_request_max": [], + "cpu_request_min": [ + "0" + ], + "cpu_limit_max": [], + "cpu_limit_min": [ + "0" + ], + "wlKnownNames": [ + "coredns", + "kube-proxy", + "event-exporter-gke", + "kube-dns", + "17-default-backend", + "metrics-server", + "ca-audit", + "ca-dashboard-aggregator", + "ca-notification-server", + "ca-ocimage", + "ca-oracle", + "ca-posture", + "ca-rbac", + "ca-vuln-scan", + "ca-webhook", + "ca-websocket", + "clair-clair" + ], + "recommendedLabels": [ + "app", + "tier", + "phase", + "version", + "owner", + "env" + ], + "k8sRecommendedLabels": [ + "app.kubernetes.io/name", + "app.kubernetes.io/instance", + "app.kubernetes.io/version", + "app.kubernetes.io/component", + "app.kubernetes.io/part-of", + "app.kubernetes.io/managed-by", + "app.kubernetes.io/created-by" + ] + } + } +} diff --git a/pkg/render/ccs/host-scanner.yaml.template b/pkg/render/ccs/host-scanner.yaml.template new file mode 100644 index 0000000000..a8cfbe1f8b --- /dev/null +++ b/pkg/render/ccs/host-scanner.yaml.template @@ -0,0 +1,77 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app: kubescape-host-scanner + k8s-app: kubescape-host-scanner + kubernetes.io/metadata.name: kubescape-host-scanner + tier: kubescape-host-scanner-control-plane + name: {{ .Namespace }} + +--- + +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: host-scanner + namespace: {{ .Namespace }} + labels: + app: host-scanner + k8s-app: kubescape-host-scanner + otel: enabled +spec: + selector: + matchLabels: + name: host-scanner + template: + metadata: + labels: + name: host-scanner + spec: + tolerations: + # this toleration is to have the DaemonDet runnable on all nodes (including masters) + # remove it if your masters can't run pods + - operator: Exists + containers: + - name: host-sensor + image: quay.io/kubescape/host-scanner:v1.0.61 + securityContext: + allowPrivilegeEscalation: true + privileged: true + readOnlyRootFilesystem: true + procMount: Unmasked + ports: + - name: scanner # Do not change port name + containerPort: 7888 + protocol: TCP + resources: + limits: + cpu: 0.1m + memory: 200Mi + requests: + cpu: 1m + memory: 200Mi + volumeMounts: + - mountPath: /host_fs + name: host-filesystem + startupProbe: + httpGet: + path: /readyz + port: 7888 + failureThreshold: 30 + periodSeconds: 1 + livenessProbe: + httpGet: + path: /healthz + port: 7888 + periodSeconds: 10 + terminationGracePeriodSeconds: 120 + dnsPolicy: ClusterFirstWithHostNet + automountServiceAccountToken: false + volumes: + - hostPath: + path: / + type: Directory + name: host-filesystem + hostPID: true + hostIPC: true diff --git a/pkg/render/common/networkpolicy/networkpolicy.go b/pkg/render/common/networkpolicy/networkpolicy.go index 7adf66ca1c..02a5467aca 100644 --- a/pkg/render/common/networkpolicy/networkpolicy.go +++ b/pkg/render/common/networkpolicy/networkpolicy.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2022-2025 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -305,6 +305,10 @@ func (h *NetworkPolicyHelper) ComplianceControllerSourceEntityRule() v3.EntityRu return CreateSourceEntityRule(h.namespace("tigera-compliance"), "compliance-controller") } +func (h *NetworkPolicyHelper) CCSAPISourceEntityRule() v3.EntityRule { + return CreateSourceEntityRule(h.namespace("tigera-compliance"), "tigera-ccs-api") +} + func (h *NetworkPolicyHelper) ComplianceSnapshotterSourceEntityRule() v3.EntityRule { return CreateSourceEntityRule(h.namespace("tigera-compliance"), "compliance-snapshotter") } diff --git a/pkg/render/logstorage/linseed/linseed.go b/pkg/render/logstorage/linseed/linseed.go index b7da874997..93cd063e1b 100644 --- a/pkg/render/logstorage/linseed/linseed.go +++ b/pkg/render/logstorage/linseed/linseed.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2022-2025 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -620,6 +620,12 @@ func (l *linseed) linseedAllowTigeraPolicy() *v3.NetworkPolicy { Source: networkpolicyHelper.ComplianceReporterSourceEntityRule(), Destination: linseedIngressDestinationEntityRule, }, + { + Action: v3.Allow, + Protocol: &networkpolicy.TCPProtocol, + Source: networkpolicyHelper.CCSAPISourceEntityRule(), + Destination: linseedIngressDestinationEntityRule, + }, { Action: v3.Allow, Protocol: &networkpolicy.TCPProtocol,