diff --git a/README.md b/README.md index a3b59fe9d..7a3932e25 100644 --- a/README.md +++ b/README.md @@ -63,19 +63,6 @@ make run ``` NOTE: You can also run this in one step by running: make install run -#### Port-forward service(s) -After installation of your resource(s), you will need to allow the locally running operator to the internal service(s). -This workaround is needed because the trillian server use insecure RPC protocol for communication with others. -Currently, it is not possible to route insecure GRPC outside the cluster so the local deployment rely on port-forward. - -##### Procedure -Install your CR and wait until the operator log prints -``` -Operator is running on localhost. You need to port-forward services. -Execute `oc port-forward service/trillian-logserver 8091 8091` in your namespace to continue. -``` -Then execute the command as is written `oc port-forward service/trillian-logserver 8091 8091` - ## EKS deployment It is possible to run RHTAS on EKS. If image building and signing all occurs within the cluster Ingress and Certifcates are not required. However, this will make it difficult to verify the image signatures from outside the cluster. It is highly suggested to deploy with Ingress and Certificates in place. diff --git a/api/v1alpha1/ctlog_types.go b/api/v1alpha1/ctlog_types.go index 5c644bcd7..619a02641 100644 --- a/api/v1alpha1/ctlog_types.go +++ b/api/v1alpha1/ctlog_types.go @@ -101,3 +101,13 @@ func (i *CTlog) GetConditions() []metav1.Condition { func (i *CTlog) SetCondition(newCondition metav1.Condition) { meta.SetStatusCondition(&i.Status.Conditions, newCondition) } + +func (i *CTlog) GetTrustedCA() *LocalObjectReference { + if v, ok := i.GetAnnotations()["rhtas.redhat.com/trusted-ca"]; ok { + return &LocalObjectReference{ + Name: v, + } + } + + return nil +} diff --git a/api/v1alpha1/fulcio_types.go b/api/v1alpha1/fulcio_types.go index daf3d3add..d020e5af4 100644 --- a/api/v1alpha1/fulcio_types.go +++ b/api/v1alpha1/fulcio_types.go @@ -144,3 +144,17 @@ func (i *Fulcio) GetConditions() []metav1.Condition { func (i *Fulcio) SetCondition(newCondition metav1.Condition) { meta.SetStatusCondition(&i.Status.Conditions, newCondition) } + +func (i *Fulcio) GetTrustedCA() *LocalObjectReference { + if i.Spec.TrustedCA != nil { + return i.Spec.TrustedCA + } + + if v, ok := i.GetAnnotations()["rhtas.redhat.com/trusted-ca"]; ok { + return &LocalObjectReference{ + Name: v, + } + } + + return nil +} diff --git a/api/v1alpha1/rekor_types.go b/api/v1alpha1/rekor_types.go index cd1c5daa9..c54d6e6f4 100644 --- a/api/v1alpha1/rekor_types.go +++ b/api/v1alpha1/rekor_types.go @@ -39,6 +39,9 @@ type RekorSpec struct { // +patchMergeKey=treeID // +kubebuilder:default:={} Sharding []RekorLogRange `json:"sharding,omitempty"` + // ConfigMap with additional bundle of trusted CA + //+optional + TrustedCA *LocalObjectReference `json:"trustedCA,omitempty"` } type RekorSigner struct { @@ -164,3 +167,17 @@ func (i *Rekor) GetConditions() []metav1.Condition { func (i *Rekor) SetCondition(newCondition metav1.Condition) { meta.SetStatusCondition(&i.Status.Conditions, newCondition) } + +func (i *Rekor) GetTrustedCA() *LocalObjectReference { + if i.Spec.TrustedCA != nil { + return i.Spec.TrustedCA + } + + if v, ok := i.GetAnnotations()["rhtas.redhat.com/trusted-ca"]; ok { + return &LocalObjectReference{ + Name: v, + } + } + + return nil +} diff --git a/api/v1alpha1/timestampauthority_types.go b/api/v1alpha1/timestampauthority_types.go index a8540ead6..0f7f2bb67 100644 --- a/api/v1alpha1/timestampauthority_types.go +++ b/api/v1alpha1/timestampauthority_types.go @@ -208,3 +208,17 @@ type TimestampAuthorityList struct { func init() { SchemeBuilder.Register(&TimestampAuthority{}, &TimestampAuthorityList{}) } + +func (i *TimestampAuthority) GetTrustedCA() *LocalObjectReference { + if i.Spec.TrustedCA != nil { + return i.Spec.TrustedCA + } + + if v, ok := i.GetAnnotations()["rhtas.redhat.com/trusted-ca"]; ok { + return &LocalObjectReference{ + Name: v, + } + } + + return nil +} diff --git a/api/v1alpha1/trillian_types.go b/api/v1alpha1/trillian_types.go index 838504433..95112b8c5 100644 --- a/api/v1alpha1/trillian_types.go +++ b/api/v1alpha1/trillian_types.go @@ -29,6 +29,9 @@ type TrillianSpec struct { Db TrillianDB `json:"database,omitempty"` // Enable Monitoring for Logsigner and Logserver Monitoring MonitoringConfig `json:"monitoring,omitempty"` + // Configuration for enabling TLS (Transport Layer Security) encryption for manged log-server and log-signer services. + //+optional + TLS TLS `json:"tls,omitempty"` // ConfigMap with additional bundle of trusted CA //+optional TrustedCA *LocalObjectReference `json:"trustedCA,omitempty"` @@ -58,6 +61,9 @@ type TrillianDB struct { // TrillianStatus defines the observed state of Trillian type TrillianStatus struct { Db TrillianDB `json:"database,omitempty"` + // Configuration for enabling TLS (Transport Layer Security) encryption for manged log-server and log-signer services. + //+optional + TLS TLS `json:"tls,omitempty"` // +listType=map // +listMapKey=type // +patchStrategy=merge @@ -99,3 +105,17 @@ func (i *Trillian) GetConditions() []metav1.Condition { func (i *Trillian) SetCondition(newCondition metav1.Condition) { meta.SetStatusCondition(&i.Status.Conditions, newCondition) } + +func (i *Trillian) GetTrustedCA() *LocalObjectReference { + if i.Spec.TrustedCA != nil { + return i.Spec.TrustedCA + } + + if v, ok := i.GetAnnotations()["rhtas.redhat.com/trusted-ca"]; ok { + return &LocalObjectReference{ + Name: v, + } + } + + return nil +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7d7f7754d..db4c5f25c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -796,6 +796,11 @@ func (in *RekorSpec) DeepCopyInto(out *RekorSpec) { *out = make([]RekorLogRange, len(*in)) copy(*out, *in) } + if in.TrustedCA != nil { + in, out := &in.TrustedCA, &out.TrustedCA + *out = new(LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RekorSpec. @@ -1339,6 +1344,7 @@ func (in *TrillianSpec) DeepCopyInto(out *TrillianSpec) { *out = *in in.Db.DeepCopyInto(&out.Db) out.Monitoring = in.Monitoring + in.TLS.DeepCopyInto(&out.TLS) if in.TrustedCA != nil { in, out := &in.TrustedCA, &out.TrustedCA *out = new(LocalObjectReference) @@ -1360,6 +1366,7 @@ func (in *TrillianSpec) DeepCopy() *TrillianSpec { func (in *TrillianStatus) DeepCopyInto(out *TrillianStatus) { *out = *in in.Db.DeepCopyInto(&out.Db) + in.TLS.DeepCopyInto(&out.TLS) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) diff --git a/bundle/manifests/rhtas-operator.clusterserviceversion.yaml b/bundle/manifests/rhtas-operator.clusterserviceversion.yaml index c31b366bc..95ee9717b 100644 --- a/bundle/manifests/rhtas-operator.clusterserviceversion.yaml +++ b/bundle/manifests/rhtas-operator.clusterserviceversion.yaml @@ -297,7 +297,7 @@ metadata: ] capabilities: Seamless Upgrades containerImage: registry.redhat.io/rhtas/rhtas-rhel9-operator@sha256:52ba6cd82bc400a08c6f89811e8086126596a873b9b12619de8c5064a2d4faf7 - createdAt: "2025-01-16T14:07:22Z" + createdAt: "2025-02-20T12:46:27Z" features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" features.operators.openshift.io/csi: "false" @@ -309,7 +309,7 @@ metadata: features.operators.openshift.io/token-auth-azure: "false" features.operators.openshift.io/token-auth-gcp: "false" operators.openshift.io/valid-subscription: '["Red Hat Trusted Artifact Signer"]' - operators.operatorframework.io/builder: operator-sdk-v1.37.0 + operators.operatorframework.io/builder: operator-sdk-v1.39.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 repository: https://github.com/securesign/secure-sign-operator support: Red Hat @@ -900,6 +900,8 @@ spec: value: registry.redhat.io/rhtas/trillian-database-rhel9@sha256:501612745e63e5504017079388bec191ffacf00ffdebde7be6ca5b8e4fd9d323 - name: RELATED_IMAGE_TRILLIAN_NETCAT value: registry.redhat.io/openshift4/ose-tools-rhel8@sha256:486b4d2dd0d10c5ef0212714c94334e04fe8a3d36cf619881986201a50f123c7 + - name: RELATED_IMAGE_TRILLIAN_CREATE_TREE + value: registry.redhat.io/rhtas/createtree-rhel9@sha256:f66a707e68fb0cdcfcddc318407fe60d72f50a7b605b5db55743eccc14a422ba - name: RELATED_IMAGE_FULCIO_SERVER value: registry.redhat.io/rhtas/fulcio-rhel9@sha256:4b5765bbfd3dac5fa027d2fb3d672b6ebffbc573b9413ab4cb189c50fa6f9a09 - name: RELATED_IMAGE_REKOR_REDIS @@ -1037,6 +1039,8 @@ spec: name: trillian-db - image: registry.redhat.io/openshift4/ose-tools-rhel8@sha256:486b4d2dd0d10c5ef0212714c94334e04fe8a3d36cf619881986201a50f123c7 name: trillian-netcat + - image: registry.redhat.io/rhtas/createtree-rhel9@sha256:f66a707e68fb0cdcfcddc318407fe60d72f50a7b605b5db55743eccc14a422ba + name: trillian-create-tree - image: registry.redhat.io/rhtas/fulcio-rhel9@sha256:4b5765bbfd3dac5fa027d2fb3d672b6ebffbc573b9413ab4cb189c50fa6f9a09 name: fulcio-server - image: registry.redhat.io/rhtas/trillian-redis-rhel9@sha256:18820b1fbdbc2cc3e917822974910332d937b03cfe781628bd986fd6a5ee318e diff --git a/bundle/manifests/rhtas-related-images_v1_configmap.yaml b/bundle/manifests/rhtas-related-images_v1_configmap.yaml index 415b1d257..54a8b9826 100644 --- a/bundle/manifests/rhtas-related-images_v1_configmap.yaml +++ b/bundle/manifests/rhtas-related-images_v1_configmap.yaml @@ -10,6 +10,7 @@ data: RELATED_IMAGE_REKOR_SERVER: registry.redhat.io/rhtas/rekor-server-rhel9@sha256:81e10e34f02b21bb8295e7b5c93797fc8c0e43a1a0d8304cca1b07415a3ed6f5 RELATED_IMAGE_SEGMENT_REPORTING: registry.redhat.io/rhtas/segment-reporting-rhel9@sha256:1b87ff1ad02c476c08e06038a26af7abe61f177e491a9ff42d507550a8587ac8 RELATED_IMAGE_TIMESTAMP_AUTHORITY: registry.redhat.io/rhtas/timestamp-authority-rhel9@sha256:fce0a22c8872309554236bab3457715dda0a83eb40dc6a9ecd3477b8023369d0 + RELATED_IMAGE_TRILLIAN_CREATE_TREE: registry.redhat.io/rhtas/createtree-rhel9@sha256:f66a707e68fb0cdcfcddc318407fe60d72f50a7b605b5db55743eccc14a422ba RELATED_IMAGE_TRILLIAN_DB: registry.redhat.io/rhtas/trillian-database-rhel9@sha256:501612745e63e5504017079388bec191ffacf00ffdebde7be6ca5b8e4fd9d323 RELATED_IMAGE_TRILLIAN_LOG_SERVER: registry.redhat.io/rhtas/trillian-logserver-rhel9@sha256:7af78c7bc4df097ffeeef345f1d13289695f715221957579ee65daeef2fa3f5b RELATED_IMAGE_TRILLIAN_LOG_SIGNER: registry.redhat.io/rhtas/trillian-logsigner-rhel9@sha256:2d707d12e4f65e1a92b4de11465a5976d55e15ad6c9fefe994646ccd44c83840 diff --git a/bundle/manifests/rhtas.redhat.com_rekors.yaml b/bundle/manifests/rhtas.redhat.com_rekors.yaml index ee0fc83a5..b2d64dae5 100644 --- a/bundle/manifests/rhtas.redhat.com_rekors.yaml +++ b/bundle/manifests/rhtas.redhat.com_rekors.yaml @@ -300,6 +300,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object status: description: RekorStatus defines the observed state of Rekor diff --git a/bundle/manifests/rhtas.redhat.com_securesigns.yaml b/bundle/manifests/rhtas.redhat.com_securesigns.yaml index 3c3c4c87b..2e19b20dc 100644 --- a/bundle/manifests/rhtas.redhat.com_securesigns.yaml +++ b/bundle/manifests/rhtas.redhat.com_securesigns.yaml @@ -710,6 +710,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object trillian: description: TrillianSpec defines the desired state of Trillian @@ -866,6 +878,52 @@ spec: required: - enabled type: object + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for manged log-server and log-signer services. + properties: + certificateRef: + description: Reference to the certificate secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) trustedCA: description: ConfigMap with additional bundle of trusted CA properties: diff --git a/bundle/manifests/rhtas.redhat.com_trillians.yaml b/bundle/manifests/rhtas.redhat.com_trillians.yaml index 98ac8c36c..6477bd673 100644 --- a/bundle/manifests/rhtas.redhat.com_trillians.yaml +++ b/bundle/manifests/rhtas.redhat.com_trillians.yaml @@ -195,6 +195,52 @@ spec: required: - enabled type: object + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for manged log-server and log-signer services. + properties: + certificateRef: + description: Reference to the certificate secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) trustedCA: description: ConfigMap with additional bundle of trusted CA properties: @@ -410,6 +456,52 @@ spec: required: - create type: object + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for manged log-server and log-signer services. + properties: + certificateRef: + description: Reference to the certificate secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) type: object type: object served: true diff --git a/ci/dev-images.sed b/ci/dev-images.sed index 25225c281..fc929da18 100644 --- a/ci/dev-images.sed +++ b/ci/dev-images.sed @@ -5,3 +5,4 @@ s#certificate-transparency#certificate-transparency-go# s#tuf-server#scaffold-tuf-server# s#client-server#client-server# s#segment-reporting#segment-backup-job# +s#createtree#trillian-createtree# diff --git a/config/crd/bases/rhtas.redhat.com_rekors.yaml b/config/crd/bases/rhtas.redhat.com_rekors.yaml index 26896c18b..940b81734 100644 --- a/config/crd/bases/rhtas.redhat.com_rekors.yaml +++ b/config/crd/bases/rhtas.redhat.com_rekors.yaml @@ -300,6 +300,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object status: description: RekorStatus defines the observed state of Rekor diff --git a/config/crd/bases/rhtas.redhat.com_securesigns.yaml b/config/crd/bases/rhtas.redhat.com_securesigns.yaml index 0429d397f..7f1b76a18 100644 --- a/config/crd/bases/rhtas.redhat.com_securesigns.yaml +++ b/config/crd/bases/rhtas.redhat.com_securesigns.yaml @@ -710,6 +710,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object trillian: description: TrillianSpec defines the desired state of Trillian @@ -866,6 +878,52 @@ spec: required: - enabled type: object + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for manged log-server and log-signer services. + properties: + certificateRef: + description: Reference to the certificate secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) trustedCA: description: ConfigMap with additional bundle of trusted CA properties: diff --git a/config/crd/bases/rhtas.redhat.com_trillians.yaml b/config/crd/bases/rhtas.redhat.com_trillians.yaml index cb1c67d28..1621574ee 100644 --- a/config/crd/bases/rhtas.redhat.com_trillians.yaml +++ b/config/crd/bases/rhtas.redhat.com_trillians.yaml @@ -195,6 +195,52 @@ spec: required: - enabled type: object + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for manged log-server and log-signer services. + properties: + certificateRef: + description: Reference to the certificate secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) trustedCA: description: ConfigMap with additional bundle of trusted CA properties: @@ -410,6 +456,52 @@ spec: required: - create type: object + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for manged log-server and log-signer services. + properties: + certificateRef: + description: Reference to the certificate secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for TLS + encryption. + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) type: object type: object served: true diff --git a/config/default/images.env b/config/default/images.env index 6a8624502..ae42cfc02 100644 --- a/config/default/images.env +++ b/config/default/images.env @@ -2,6 +2,7 @@ RELATED_IMAGE_TRILLIAN_LOG_SIGNER=registry.redhat.io/rhtas/trillian-logsigner-rh RELATED_IMAGE_TRILLIAN_LOG_SERVER=registry.redhat.io/rhtas/trillian-logserver-rhel9@sha256:7af78c7bc4df097ffeeef345f1d13289695f715221957579ee65daeef2fa3f5b RELATED_IMAGE_TRILLIAN_DB=registry.redhat.io/rhtas/trillian-database-rhel9@sha256:501612745e63e5504017079388bec191ffacf00ffdebde7be6ca5b8e4fd9d323 RELATED_IMAGE_TRILLIAN_NETCAT=registry.redhat.io/openshift4/ose-tools-rhel8@sha256:486b4d2dd0d10c5ef0212714c94334e04fe8a3d36cf619881986201a50f123c7 +RELATED_IMAGE_TRILLIAN_CREATE_TREE=registry.redhat.io/rhtas/createtree-rhel9@sha256:f66a707e68fb0cdcfcddc318407fe60d72f50a7b605b5db55743eccc14a422ba RELATED_IMAGE_FULCIO_SERVER=registry.redhat.io/rhtas/fulcio-rhel9@sha256:4b5765bbfd3dac5fa027d2fb3d672b6ebffbc573b9413ab4cb189c50fa6f9a09 RELATED_IMAGE_REKOR_REDIS=registry.redhat.io/rhtas/trillian-redis-rhel9@sha256:18820b1fbdbc2cc3e917822974910332d937b03cfe781628bd986fd6a5ee318e RELATED_IMAGE_REKOR_SERVER=registry.redhat.io/rhtas/rekor-server-rhel9@sha256:81e10e34f02b21bb8295e7b5c93797fc8c0e43a1a0d8304cca1b07415a3ed6f5 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index a7470039b..f04c2e238 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -90,6 +90,17 @@ replacements: select: kind: Deployment name: operator-controller-manager +- source: + fieldPath: data.RELATED_IMAGE_TRILLIAN_CREATE_TREE + kind: ConfigMap + name: related-images + version: v1 + targets: + - fieldPaths: + - spec.template.spec.containers.[name=^manager$].env.[name=^RELATED_IMAGE_TRILLIAN_CREATE_TREE$].value + select: + kind: Deployment + name: operator-controller-manager - source: fieldPath: data.RELATED_IMAGE_FULCIO_SERVER kind: ConfigMap diff --git a/config/default/manager_images_patch.yaml b/config/default/manager_images_patch.yaml index 9704bbc32..f79f363c3 100644 --- a/config/default/manager_images_patch.yaml +++ b/config/default/manager_images_patch.yaml @@ -16,6 +16,8 @@ spec: value: PLACEHOLDER - name: RELATED_IMAGE_TRILLIAN_NETCAT value: PLACEHOLDER + - name: RELATED_IMAGE_TRILLIAN_CREATE_TREE + value: PLACEHOLDER - name: RELATED_IMAGE_FULCIO_SERVER value: PLACEHOLDER - name: RELATED_IMAGE_REKOR_REDIS diff --git a/internal/apis/tls.go b/internal/apis/tls.go new file mode 100644 index 000000000..1a9a4ccf2 --- /dev/null +++ b/internal/apis/tls.go @@ -0,0 +1,7 @@ +package apis + +import "github.com/securesign/operator/api/v1alpha1" + +type TlsClient interface { + GetTrustedCA() *v1alpha1.LocalObjectReference +} diff --git a/internal/controller/common/action/tree/action.go b/internal/controller/common/action/tree/action.go new file mode 100644 index 000000000..ce21bc5d5 --- /dev/null +++ b/internal/controller/common/action/tree/action.go @@ -0,0 +1,421 @@ +package tree + +import ( + "context" + _ "embed" + "fmt" + "strconv" + "strings" + + "github.com/securesign/operator/internal/controller/common/action" + "github.com/securesign/operator/internal/controller/common/utils" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/job" + "github.com/securesign/operator/internal/controller/common/utils/tls" + ensureTls "github.com/securesign/operator/internal/controller/common/utils/tls/ensure" + "github.com/securesign/operator/internal/controller/constants" + "github.com/securesign/operator/internal/controller/labels" + actions2 "github.com/securesign/operator/internal/controller/trillian/actions" + "github.com/securesign/operator/internal/images" + "golang.org/x/exp/maps" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func NewResolveTreeAction[T tlsAwareObject](component string, wrapper func(T) *wrapper[T]) action.Action[T] { + return &resolveTree[T]{ + component: component, + treeDisplayName: fmt.Sprintf("%s-tree", component), + wrapper: wrapper, + } +} + +type resolveTree[T tlsAwareObject] struct { + action.BaseAction + component string + treeDisplayName string + wrapper func(T) *wrapper[T] +} + +func (i resolveTree[T]) Name() string { + return "resolve tree" +} + +func (i resolveTree[T]) CanHandle(ctx context.Context, instance T) bool { + wrapped := i.wrapper(instance) + + switch { + case wrapped.GetStatusTreeID() == nil: + return true + case wrapped.GetTreeID() != nil: + return !equality.Semantic.DeepEqual(wrapped.GetTreeID(), wrapped.GetStatusTreeID()) + default: + return !meta.IsStatusConditionTrue(instance.GetConditions(), JobCondition) + } +} + +func (i resolveTree[T]) handleManual(ctx context.Context, instance T) *action.Result { + wrapped := i.wrapper(instance) + + if wrapped.GetTreeID() != nil && *wrapped.GetTreeID() != int64(0) { + wrapped.SetStatusTreeID(wrapped.GetTreeID()) + return i.StatusUpdate(ctx, instance) + } + + return i.Continue() +} + +func (i resolveTree[T]) handleRbac(ctx context.Context, instance T) *action.Result { + var err error + rbacName := fmt.Sprintf(RBACNameMask, i.component) + + labels := labels.For("createtree", i.component, instance.GetName()) + + // ServiceAccount + if _, err = kubernetes.CreateOrUpdate(ctx, i.Client, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: rbacName, + Namespace: instance.GetNamespace(), + }, + }, + ensure.ControllerReference[*corev1.ServiceAccount](instance, i.Client), + ensure.Labels[*corev1.ServiceAccount](maps.Keys(labels), labels), + ); err != nil { + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not create SA: %w", err)), instance) + } + + // Role + if _, err = kubernetes.CreateOrUpdate(ctx, i.Client, &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: rbacName, + Namespace: instance.GetNamespace(), + }, + }, + ensure.ControllerReference[*rbacv1.Role](instance, i.Client), + ensure.Labels[*rbacv1.Role](maps.Keys(labels), labels), + kubernetes.EnsureRoleRules( + rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"patch"}, + }), + ); err != nil { + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not create Role: %w", err)), instance) + } + + // RoleBinding + if _, err = kubernetes.CreateOrUpdate(ctx, i.Client, &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: rbacName, + Namespace: instance.GetNamespace(), + }, + }, + ensure.ControllerReference[*rbacv1.RoleBinding](instance, i.Client), + ensure.Labels[*rbacv1.RoleBinding](maps.Keys(labels), labels), + kubernetes.EnsureRoleBinding( + rbacv1.RoleRef{ + APIGroup: corev1.SchemeGroupVersion.Group, + Kind: "Role", + Name: rbacName, + }, + rbacv1.Subject{Kind: "ServiceAccount", Name: rbacName, Namespace: instance.GetNamespace()}, + ), + ); err != nil { + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not create RoleBinding: %w", err)), instance) + } + + return i.Continue() +} + +func (i resolveTree[T]) handleConfigMap(ctx context.Context, instance T) *action.Result { + var ( + result controllerutil.OperationResult + err error + ) + + labels := labels.For("createtree", i.component, instance.GetName()) + + // Needed for configMap clean-up + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf(configMapResultMask, i.component, instance.GetName()), + Namespace: instance.GetNamespace(), + }, + } + + if result, err = kubernetes.CreateOrUpdate(ctx, i.Client, + configMap, + ensure.ControllerReference[*corev1.ConfigMap](instance, i.Client), + ensure.Labels[*corev1.ConfigMap](maps.Keys(labels), labels), + ); err != nil { + return i.Error(ctx, fmt.Errorf("could not create %s ConfigMap: %w", configMap.GetName(), err), instance) + } + + if result != controllerutil.OperationResultNone { + instance.SetCondition(metav1.Condition{ + Type: JobCondition, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: fmt.Sprintf("ConfigMap `%s` %s", configMap.GetName(), result)}, + ) + return i.StatusUpdate(ctx, instance) + } else { + return i.Continue() + } +} + +func (i resolveTree[T]) handleJob(ctx context.Context, instance T) *action.Result { + var err error + var trillUrl string + wrapped := i.wrapper(instance) + + labels := labels.For("createtree", i.component, instance.GetName()) + + configMapName := fmt.Sprintf(configMapResultMask, i.component, instance.GetName()) + configMap, err := kubernetes.GetConfigMap(ctx, i.Client, instance.GetNamespace(), configMapName) + if err != nil { + if apierrors.IsNotFound(err) { + return i.Requeue() + } + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not get configmap: %w", err)), instance) + } + + for _, ref := range configMap.GetOwnerReferences() { + if ref.Kind == "Job" { + return i.Continue() + } + } + + trillianService := wrapped.GetTrillianService() + + switch { + case trillianService.Port == nil: + err = fmt.Errorf("%s: %v", i.Name(), TrillianPortNotSpecified) + case trillianService.Address == "": + trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.GetNamespace(), *trillianService.Port) + default: + trillUrl = fmt.Sprintf("%s:%d", trillianService.Address, *trillianService.Port) + } + if err != nil { + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not resolve trillian service: %w", err)), instance) + } + i.Logger.V(1).Info("trillian logserver", "address", trillUrl) + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf(JobNameMask, i.component), + Namespace: instance.GetNamespace(), + }, + } + + extraArgs := []string{} + if instance.GetTrustedCA() != nil || kubernetes.IsOpenShift() { + caPath, err := tls.CAPath(ctx, i.Client, instance) + if err != nil { + return i.Error(ctx, fmt.Errorf("could not get CA path: %w", err), instance) + } + extraArgs = append(extraArgs, "--tls_cert_file", caPath) + } + + if _, err = kubernetes.CreateOrUpdate(ctx, i.Client, + job, + i.ensureJob(fmt.Sprintf(configMapResultMask, i.component, instance.GetName()), trillUrl, i.treeDisplayName, extraArgs...), + ensure.ControllerReference[*batchv1.Job](instance, i.Client), + ensure.Labels[*batchv1.Job](maps.Keys(labels), labels), + func(object *batchv1.Job) error { + return ensureTls.TrustedCA(instance.GetTrustedCA())(&object.Spec.Template) + }, + ); err != nil { + return i.Error(ctx, fmt.Errorf("could not create segment backup job: %w", err), instance, + metav1.Condition{ + Type: JobCondition, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: err.Error(), + }) + } + + if _, err = kubernetes.CreateOrUpdate(ctx, i.Client, configMap, + func(object *corev1.ConfigMap) error { + return controllerutil.SetOwnerReference(job, object, i.Client.Scheme()) + }, + ); err != nil { + return i.Error(ctx, fmt.Errorf("could not update annotations on %s ConfigMap: %w", configMap.GetName(), err), instance, + metav1.Condition{ + Type: JobCondition, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: err.Error(), + }) + } + + instance.SetCondition(metav1.Condition{ + Type: JobCondition, + Status: metav1.ConditionFalse, + Reason: constants.Initialize, + Message: "createtree job created", + }) + + return i.StatusUpdate(ctx, instance) +} + +func (i resolveTree[T]) handleJobFinished(ctx context.Context, instance T) *action.Result { + var ( + jobName string + err error + ) + + configMapName := fmt.Sprintf(configMapResultMask, i.component, instance.GetName()) + configMap, err := kubernetes.GetConfigMap(ctx, i.Client, instance.GetNamespace(), configMapName) + if err != nil { + if apierrors.IsNotFound(err) { + return i.Requeue() + } + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not get configmap: %w", err)), instance) + } + + for _, ref := range configMap.GetOwnerReferences() { + if ref.Kind == "Job" { + jobName = ref.Name + break + } + } + if jobName == "" { + return i.Requeue() + } + + j, err := job.GetJob(ctx, i.Client, instance.GetNamespace(), jobName) + if client.IgnoreNotFound(err) != nil { + return i.Error(ctx, err, instance) + } + + if j == nil { + return i.Requeue() + } + i.Logger.V(1).Info("createtree job is already present.", "Succeeded", j.Status.Succeeded, "Failures", j.Status.Failed) + + if !job.IsCompleted(*j) { + return i.Requeue() + } + + if job.IsFailed(*j) { + instance.SetCondition(metav1.Condition{ + Type: JobCondition, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: JobFailed.Error(), + }) + return i.Error(ctx, reconcile.TerminalError(JobFailed), instance) + } + + return i.Continue() +} + +func (i resolveTree[T]) handleExtractJobResult(ctx context.Context, instance T) *action.Result { + wrapped := i.wrapper(instance) + + configMapName := fmt.Sprintf(configMapResultMask, i.component, instance.GetName()) + configMap, err := kubernetes.GetConfigMap(ctx, i.Client, instance.GetNamespace(), configMapName) + if err != nil { + if apierrors.IsNotFound(err) { + return i.Requeue() + } + return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("could not get configmap: %w", err)), instance) + } + + if result, ok := configMap.Data[configMapResultField]; ok && result != "" { + treeID, err := strconv.ParseInt(result, 10, 64) + if err != nil { + return i.Error(ctx, reconcile.TerminalError(err), instance) + } + + wrapped.SetStatusTreeID(&treeID) + instance.SetCondition(metav1.Condition{ + Type: JobCondition, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + }) + i.Recorder.Eventf(instance, corev1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", treeID) + return i.StatusUpdate(ctx, instance) + } else { + i.Logger.V(1).Info("ConfigMap not ready or data is empty, requeuing reconciliation") + return i.Requeue() + } +} + +func (i resolveTree[T]) ensureJob(cfgName, adminServer, displayName string, extraArgs ...string) func(*batchv1.Job) error { + return func(job *batchv1.Job) error { + + spec := &job.Spec + spec.Parallelism = utils.Pointer[int32](1) + spec.Completions = utils.Pointer[int32](1) + spec.ActiveDeadlineSeconds = utils.Pointer[int64](600) + spec.BackoffLimit = utils.Pointer[int32](5) + + templateSpec := &spec.Template.Spec + templateSpec.ServiceAccountName = fmt.Sprintf(RBACNameMask, i.component) + templateSpec.RestartPolicy = "OnFailure" + + container := kubernetes.FindContainerByNameOrCreate(templateSpec, "createtree") + container.Image = images.Registry.Get(images.TrillianCreateTree) + container.Command = []string{"/bin/sh", "-c"} + container.Args = []string{string(jobScript)} + + cfgNameEnv := kubernetes.FindEnvByNameOrCreate(container, "CONFIGMAP_NAME") + cfgNameEnv.Value = cfgName + + adminServerEnv := kubernetes.FindEnvByNameOrCreate(container, "ADMIN_SERVER") + adminServerEnv.Value = adminServer + + displayNameEnv := kubernetes.FindEnvByNameOrCreate(container, "DISPLAY_NAME") + displayNameEnv.Value = displayName + + extraArgsEnv := kubernetes.FindEnvByNameOrCreate(container, "EXTRA_ARGS") + extraArgsEnv.Value = strings.Join(extraArgs, " ") + + return nil + } +} + +func (i resolveTree[T]) Handle(ctx context.Context, instance T) *action.Result { + result := i.handleManual(ctx, instance) + if result != nil { + return result + } + + result = i.handleRbac(ctx, instance) + if result != nil { + return result + } + + result = i.handleConfigMap(ctx, instance) + if result != nil { + return result + } + + result = i.handleJob(ctx, instance) + if result != nil { + return result + } + + result = i.handleJobFinished(ctx, instance) + if result != nil { + return result + } + + result = i.handleExtractJobResult(ctx, instance) + if result != nil { + return result + } + + return i.Continue() +} diff --git a/internal/controller/common/action/tree/action_test.go b/internal/controller/common/action/tree/action_test.go new file mode 100644 index 000000000..54c2bc5e6 --- /dev/null +++ b/internal/controller/common/action/tree/action_test.go @@ -0,0 +1,647 @@ +package tree + +import ( + "context" + "fmt" + "reflect" + "strconv" + "testing" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gstruct" + testAction "github.com/securesign/operator/internal/testing/action" + v1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + "k8s.io/utils/ptr" +) + +type namedTest struct { + name string + run func(t *testing.T) +} + +var tests []namedTest + +var defaultWrapper = Wrapper[*v1alpha1.Rekor]( + func(rekor *v1alpha1.Rekor) *int64 { + return rekor.Spec.TreeID + }, + func(rekor *v1alpha1.Rekor) *int64 { + return rekor.Status.TreeID + }, + func(rekor *v1alpha1.Rekor, i *int64) { + rekor.Status.TreeID = i + }, + func(rekor *v1alpha1.Rekor) *v1alpha1.TrillianService { + return &rekor.Spec.Trillian + }, +) + +var ( + nnObject = types.NamespacedName{Name: "test", Namespace: "default"} + nnResult = types.NamespacedName{Name: fmt.Sprintf(configMapResultMask, "test", "test"), Namespace: "default"} +) + +func init() { + tests = []namedTest{ + {name: "manual", run: testManual}, + {name: "rbac", run: testRbac}, + {name: "configmap", run: testConfigMap}, + {name: "create-job", run: testCreateJob}, + {name: "monitor-job", run: testMonitorJob}, + {name: "extract-result", run: testExtractResult}, + } +} + +type pre struct { + warmUp bool + before func(context.Context, Gomega, client.WithWatch) +} +type want struct { + result *action.Result + verify func(context.Context, Gomega, client.WithWatch) +} + +func testManual(t *testing.T) { + for _, tc := range []struct { + desc string + pre pre + want want + }{ + { + desc: "not-set", + want: want{ + result: testAction.Continue(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + r := v1alpha1.Rekor{} + g.Expect(c.Get(ctx, nnObject, &r)).To(Succeed()) + g.Expect(r.Spec.TreeID).Should(BeNil()) + g.Expect(r.Status.TreeID).Should(BeNil()) + }, + }, + }, + { + desc: "set", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + r := v1alpha1.Rekor{} + g.Expect(c.Get(ctx, nnObject, &r)).To(Succeed()) + + r.Spec.TreeID = ptr.To(int64(123456789)) + g.Expect(c.Update(ctx, &r)).To(Succeed()) + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + r := v1alpha1.Rekor{} + g.Expect(c.Get(ctx, nnObject, &r)).To(Succeed()) + g.Expect(r.Spec.TreeID).ShouldNot(BeNil()) + g.Expect(r.Status.TreeID).ShouldNot(BeNil()) + g.Expect(*r.Spec.TreeID).Should(Equal(int64(123456789))) + g.Expect(*r.Status.TreeID).Should(Equal(int64(123456789))) + + cond := meta.FindStatusCondition(r.GetConditions(), JobCondition) + g.Expect(cond).Should(BeNil()) + }, + }, + }, + } { + t.Run(tc.desc, testRunner(tc.pre, tc.want, func(r *resolveTree[*v1alpha1.Rekor], ctx context.Context, rekor *v1alpha1.Rekor) *action.Result { + return r.handleManual(ctx, rekor) + })) + } +} + +func testRbac(t *testing.T) { + for _, tc := range []struct { + desc string + want want + }{ + {desc: "ensure", want: want{ + result: testAction.Continue(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + nn := types.NamespacedName{Name: fmt.Sprintf(RBACNameMask, "test"), Namespace: "default"} + g.Expect(c.Get(ctx, nn, &corev1.ServiceAccount{})).To(Succeed()) + g.Expect(c.Get(ctx, nn, &rbacv1.Role{})).To(Succeed()) + g.Expect(c.Get(ctx, nn, &rbacv1.RoleBinding{})).To(Succeed()) + }, + }}, + } { + t.Run(tc.desc, testRunner(pre{}, tc.want, func(r *resolveTree[*v1alpha1.Rekor], ctx context.Context, rekor *v1alpha1.Rekor) *action.Result { + return r.handleRbac(ctx, rekor) + })) + } +} + +func testConfigMap(t *testing.T) { + for _, tc := range []struct { + desc string + pre pre + want want + }{ + { + desc: "not-exists", + pre: pre{ + warmUp: false, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + g.Expect(c.Get(ctx, nnResult, &corev1.ConfigMap{})).To(Succeed()) + }, + }}, + { + desc: "exists", + pre: pre{ + warmUp: true, + }, + want: want{ + result: testAction.Continue(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + g.Expect(c.Get(ctx, nnResult, &corev1.ConfigMap{})).To(Succeed()) + }, + }}, + { + desc: "ignore-changes-data", + pre: pre{ + warmUp: true, + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{} + g.Expect(c.Get(ctx, nnResult, cm)).To(Succeed()) + + cm.Data = map[string]string{ + "foo": "bar", + } + g.Expect(c.Update(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Continue(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{} + g.Expect(c.Get(ctx, nnResult, cm)).To(Succeed()) + + g.Expect(cm.Data["foo"]).To(Equal("bar")) + }, + }}, + } { + t.Run(tc.desc, testRunner(tc.pre, tc.want, func(r *resolveTree[*v1alpha1.Rekor], ctx context.Context, rekor *v1alpha1.Rekor) *action.Result { + return r.handleConfigMap(ctx, rekor) + })) + } +} + +func testCreateJob(t *testing.T) { + for _, tc := range []struct { + desc string + pre pre + want want + }{ + { + desc: "requeue", + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "continue", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job-name"}, + }, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Continue(), + }, + }, + { + desc: "ensure", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + jobs := &v1.JobList{} + g.Expect(c.List(ctx, jobs, client.InNamespace("default"))).To(Succeed()) + g.Expect(jobs.Items).To(HaveLen(1)) + jobName := jobs.Items[0].Name + + cm := &corev1.ConfigMap{} + g.Expect(c.Get(ctx, nnResult, cm)).To(Succeed()) + g.Expect(cm.GetOwnerReferences()).To(ContainElements(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Kind": Equal("Job"), + "Name": Equal(jobName), + }))) + }, + }, + }, + } { + t.Run(tc.desc, testRunner(tc.pre, tc.want, func(r *resolveTree[*v1alpha1.Rekor], ctx context.Context, rekor *v1alpha1.Rekor) *action.Result { + return r.handleJob(ctx, rekor) + })) + } +} + +func testMonitorJob(t *testing.T) { + for _, tc := range []struct { + desc string + pre pre + want want + }{ + { + desc: "requeue: missing configmap", + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "requeue: missing reference", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "requeue: missing job", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "requeue: job not completed", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + + job := v1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job", + Namespace: nnResult.Namespace, + }, + } + g.Expect(c.Create(ctx, &job)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "job failed", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + + job := v1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job", + Namespace: nnResult.Namespace, + }, + Status: v1.JobStatus{ + Conditions: []v1.JobCondition{ + { + Status: corev1.ConditionTrue, Type: v1.JobComplete, + }, + { + Status: corev1.ConditionTrue, Type: v1.JobFailed, + }, + }, + }, + } + g.Expect(c.Create(ctx, &job)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Error(reconcile.TerminalError(JobFailed)), + }, + }, + { + desc: "continue", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + job := v1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job", + Namespace: nnResult.Namespace, + }, + Status: v1.JobStatus{ + Conditions: []v1.JobCondition{ + { + Status: corev1.ConditionTrue, Type: v1.JobComplete, + }, + }, + }, + } + g.Expect(c.Create(ctx, &job)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Continue(), + }, + }, + } { + t.Run(tc.desc, testRunner(tc.pre, tc.want, func(r *resolveTree[*v1alpha1.Rekor], ctx context.Context, rekor *v1alpha1.Rekor) *action.Result { + return r.handleJobFinished(ctx, rekor) + })) + } +} + +func testExtractResult(t *testing.T) { + for _, tc := range []struct { + desc string + pre pre + want want + }{ + { + desc: "requeue: missing configmap", + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "requeue: missing data", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Requeue(), + }, + }, + { + desc: "error: corrupted data", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + Data: map[string]string{ + "tree_id": "not-a-number", + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.Error(reconcile.TerminalError( + &strconv.NumError{Func: "ParseInt", Num: "not-a-number", Err: strconv.ErrSyntax}, + )), + }, + }, + { + desc: "success: update TreeID status", + pre: pre{ + before: func(ctx context.Context, g Gomega, c client.WithWatch) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnResult.Name, + Namespace: nnResult.Namespace, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "Job", Name: "job"}, + }, + }, + Data: map[string]string{ + "tree_id": "123456789", + }, + } + g.Expect(c.Create(ctx, cm)).To(Succeed()) + }, + }, + want: want{ + result: testAction.StatusUpdate(), + verify: func(ctx context.Context, g Gomega, c client.WithWatch) { + r := v1alpha1.Rekor{} + g.Expect(c.Get(ctx, nnObject, &r)).To(Succeed()) + + g.Expect(r.Status.TreeID).ToNot(BeNil()) + g.Expect(*r.Status.TreeID).To(Equal(int64(123456789))) + }, + }, + }, + } { + t.Run(tc.desc, testRunner(tc.pre, tc.want, func(r *resolveTree[*v1alpha1.Rekor], ctx context.Context, rekor *v1alpha1.Rekor) *action.Result { + return r.handleExtractJobResult(ctx, rekor) + })) + } +} + +type handleFn func(*resolveTree[*v1alpha1.Rekor], context.Context, *v1alpha1.Rekor) *action.Result + +func testRunner(pre pre, want want, handleFn handleFn) func(t *testing.T) { + return func(t *testing.T) { + g := NewWithT(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + instance := &v1alpha1.Rekor{ + ObjectMeta: metav1.ObjectMeta{ + Name: nnObject.Name, + Namespace: nnObject.Namespace, + }, + Spec: v1alpha1.RekorSpec{ + Trillian: v1alpha1.TrillianService{ + Address: "trillian-logserver", + Port: ptr.To(int32(8091)), + }, + }, + } + + c := testAction.FakeClientBuilder(). + WithObjects(instance). + WithStatusSubresource(instance). + Build() + + a := testAction.PrepareAction(c, NewResolveTreeAction("test", defaultWrapper)) + ra := a.(*resolveTree[*v1alpha1.Rekor]) + + if pre.warmUp { + handleFn(ra, ctx, instance) + } + + if pre.before != nil { + pre.before(ctx, g, c) + } + + g.Expect(c.Get(ctx, nnObject, instance)).To(Succeed()) + + if got := handleFn(ra, ctx, instance); !reflect.DeepEqual(got, want.result) { + t.Errorf("CanHandle() = %v, want %v", got, want.result) + } + if want.verify != nil { + want.verify(ctx, g, c) + } + } +} + +func TestResolveTree_CanHandle(t *testing.T) { + tests := []struct { + name string + condition metav1.ConditionStatus + canHandle bool + treeID *int64 + statusTreeID *int64 + }{ + { + name: "spec.treeID is not nil and status.treeID is nil", + condition: metav1.ConditionTrue, + canHandle: true, + treeID: ptr.To(int64(123456)), + }, { + name: "spec.treeID != status.treeID", + condition: metav1.ConditionTrue, + canHandle: true, + treeID: ptr.To(int64(123456)), + statusTreeID: ptr.To(int64(654321)), + }, { + name: "spec.treeID is nil and status.treeID is not nil", + condition: metav1.ConditionTrue, + canHandle: false, + statusTreeID: ptr.To(int64(654321)), + }, { + name: "spec.treeID is nil and status.treeID is nil", + condition: metav1.ConditionTrue, + canHandle: true, + }, { + name: "status condition is false", + condition: metav1.ConditionFalse, + canHandle: true, + statusTreeID: ptr.To(int64(654321)), + }, { + name: "status condition is Unknown", + condition: metav1.ConditionUnknown, + canHandle: true, + statusTreeID: ptr.To(int64(654321)), + }, { + name: "empty status", + condition: "", + canHandle: true, + statusTreeID: ptr.To(int64(654321)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := testAction.FakeClientBuilder().Build() + a := testAction.PrepareAction(c, NewResolveTreeAction("test", defaultWrapper)) + instance := v1alpha1.Rekor{ + Spec: v1alpha1.RekorSpec{ + TreeID: tt.treeID, + }, + Status: v1alpha1.RekorStatus{ + TreeID: tt.statusTreeID, + }, + } + if tt.condition != "" { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: JobCondition, + Status: tt.condition, + }) + } + + if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { + t.Errorf("CanHandle() = %v, want %v", got, tt.canHandle) + } + }) + } +} + +func TestResolveTree_Handle(t *testing.T) { + for _, nt := range tests { + t.Run(nt.name, func(t *testing.T) { + nt.run(t) + }) + } +} diff --git a/internal/controller/common/action/tree/data/script.sh b/internal/controller/common/action/tree/data/script.sh new file mode 100644 index 000000000..059ab3228 --- /dev/null +++ b/internal/controller/common/action/tree/data/script.sh @@ -0,0 +1,32 @@ +set -e + +# Define API server variables +APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" +TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) +CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt +NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + +# Use the environment variable CONFIGMAP_NAME or default to "tree-config" +CONFIGMAP_NAME=${CONFIGMAP_NAME:-tree-config} + +TREE_ID=$(/createtree --rpc_deadline=240s --admin_server="${ADMIN_SERVER}" --display_name="${DISPLAY_NAME}" ${EXTRA_ARGS}) +if [ $? -ne 0 ]; then + echo "Failed to create tree" >&2 + exit 1 +fi +echo "Created tree with id: $TREE_ID" + +echo "Updating ${CONFIGMAP_NAME} ConfigMap..." +# Update the ConfigMap with a strategic merge patch +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --cacert ${CACERT} \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/strategic-merge-patch+json" \ + -X PATCH ${APISERVER}/api/v1/namespaces/${NAMESPACE}/configmaps/${CONFIGMAP_NAME} \ + -d "{\"data\": {\"tree_id\": \"${TREE_ID}\"}}") + +if [ "$HTTP_CODE" -ne "200" ]; then + echo "Failed to PATCH ${CONFIGMAP_NAME} ConfigMap, HTTP_CODE: ${HTTP_CODE}" >&2 + exit 1 +fi + +echo "Success" diff --git a/internal/controller/common/action/tree/doc.go b/internal/controller/common/action/tree/doc.go new file mode 100644 index 000000000..56ef99306 --- /dev/null +++ b/internal/controller/common/action/tree/doc.go @@ -0,0 +1,40 @@ +/* +Package tree implements the "resolve tree" action. + +This action manages the creation of Merkle trees using the Trillian service. It encapsulates +all the necessary steps from setting up required resources to launching and monitoring the +tree creation process and updates the custom resource with the new tree information. The main +purpose of this action is to ensure that Merkle trees are reliably created and tracked as part +of the operator's reconciliation process. + +The action is implemented as a generic type to work with any custom resource that satisfies +the [apis.ConditionsAwareObject] and [apis.TlsClient] interfaces. + +Workflow: + 1. Check if the tree is already resolved. If a tree ID exists in the resource, update the status and exit. + 2. Prepare the environment by creating or updating the necessary RBAC resources (ServiceAccount, Role, and RoleBinding) + and setting up a ConfigMap used to store the result of the tree creation job. + 3. Launch the tree creation job by submitting a Kubernetes Job that executes the Trillian tree creation script. Configuration, + such as the ConfigMap name, admin server address, and TLS settings, is passed to the job via environment variables. + 4. Monitor and handle the job by waiting for its completion. If the job is still running or fails, requeue the reconciliation. + 5. Process the job results by extracting the tree ID from the ConfigMap, updating the custom resource status with the new tree ID, + and recording a success event. + +Usage: + + wrapper := tree.Wrapper[*v1alpha1.Rekor]( + func(obj *v1alpha1.Rekor) *int64 { + return obj.Spec.TreeID + }, + func(obj *v1alpha1.Rekor) *int64 { + return obj.Status.TreeID + }, + func(rekor *v1alpha1.Rekor, i *int64) { + obj.Status.TreeID = i + }, + func(obj *v1alpha1.Rekor) *v1alpha1.TrillianService { + return &obj.Spec.Trillian + }) + tree.NewResolveTreeAction[*v1alpha1.Rekor]("rekor", wrapper) +*/ +package tree diff --git a/internal/controller/common/action/tree/errors.go b/internal/controller/common/action/tree/errors.go new file mode 100644 index 000000000..d0ff7b8ac --- /dev/null +++ b/internal/controller/common/action/tree/errors.go @@ -0,0 +1,10 @@ +package tree + +import ( + "errors" +) + +var ( + TrillianPortNotSpecified = errors.New("trillian port not specified") + JobFailed = errors.New("createtree job failed") +) diff --git a/internal/controller/common/action/tree/types.go b/internal/controller/common/action/tree/types.go new file mode 100644 index 000000000..4650ced71 --- /dev/null +++ b/internal/controller/common/action/tree/types.go @@ -0,0 +1,61 @@ +package tree + +import ( + _ "embed" + + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/apis" +) + +//go:embed data/script.sh +var jobScript []byte + +const ( + RBACNameMask = "%s-createtree-job" + JobNameMask = "%s-createtree-job-" + JobCondition = "Tree" + configMapResultMask = "%s-%s-createtree-result" + configMapResultField = "tree_id" +) + +func Wrapper[T tlsAwareObject](getTree, getStatusTree func(T) *int64, setStatusTree func(T, *int64), getTrillianService func(T) *v1alpha1.TrillianService) func(T) *wrapper[T] { + return func(obj T) *wrapper[T] { + return &wrapper[T]{ + object: obj, + callTree: getTree, + callStatusTree: getStatusTree, + callSetStatusTree: setStatusTree, + callTrillianService: getTrillianService, + } + } +} + +type wrapper[T tlsAwareObject] struct { + object T + + callTree func(T) *int64 + callStatusTree func(T) *int64 + callSetStatusTree func(T, *int64) + callTrillianService func(T) *v1alpha1.TrillianService +} + +func (c *wrapper[T]) GetTreeID() *int64 { + return c.callTree(c.object) +} + +func (c *wrapper[T]) GetStatusTreeID() *int64 { + return c.callStatusTree(c.object) +} + +func (c *wrapper[T]) SetStatusTreeID(treeID *int64) { + c.callSetStatusTree(c.object, treeID) +} + +func (c *wrapper[T]) GetTrillianService() *v1alpha1.TrillianService { + return c.callTrillianService(c.object) +} + +type tlsAwareObject interface { + apis.ConditionsAwareObject + apis.TlsClient +} diff --git a/internal/controller/common/create_tree.go b/internal/controller/common/create_tree.go deleted file mode 100644 index 0a60e0f50..000000000 --- a/internal/controller/common/create_tree.go +++ /dev/null @@ -1,98 +0,0 @@ -package common - -import ( - "context" - "fmt" - "net" - "time" - - "github.com/google/trillian" - "github.com/google/trillian/client" - "github.com/securesign/operator/internal/controller/common/utils/kubernetes" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/protobuf/types/known/durationpb" - "k8s.io/klog/v2" -) - -// reference code https://github.com/sigstore/scaffolding/blob/main/cmd/trillian/createtree/main.go -func CreateTrillianTree(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) { - var err error - inContainer, err := kubernetes.ContainerMode() - if err == nil { - if !inContainer { - fmt.Println("Operator is running on localhost. You need to port-forward services.") - for it := 0; it < 60; it++ { - if rawConnect("localhost", "8091") { - fmt.Println("Connection is open.") - trillianURL = "localhost:8091" - break - } else { - fmt.Println("Execute `oc port-forward service/trillian-logserver 8091 8091` in your namespace to continue.") - time.Sleep(time.Duration(5) * time.Second) - } - } - - } - } else { - klog.Info("Can't recognise operator mode - expecting in-container run") - } - req, err := newRequest(displayName) - if err != nil { - return nil, err - } - var opts grpc.DialOption - klog.Warning("Using an insecure gRPC connection to Trillian") - opts = grpc.WithTransportCredentials(insecure.NewCredentials()) - conn, err := grpc.Dial(trillianURL, opts) - if err != nil { - return nil, fmt.Errorf("failed to dial: %w", err) - } - defer func() { _ = conn.Close() }() - - adminClient := trillian.NewTrillianAdminClient(conn) - logClient := trillian.NewTrillianLogClient(conn) - - timeout := time.Duration(deadline) * time.Second - ctx2, cancel := context.WithTimeout(ctx, timeout) - tree, err := client.CreateAndInitTree(ctx2, req, adminClient, logClient) - defer cancel() - if err != nil { - return nil, fmt.Errorf("could not create Trillian tree: %w", err) - } - return tree, err -} - -func rawConnect(host string, port string) bool { - timeout := time.Second - conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) - if err != nil { - return false - } - if conn != nil { - defer func() { _ = conn.Close() }() - return true - } - return false -} - -func newRequest(displayName string) (*trillian.CreateTreeRequest, error) { - ts, ok := trillian.TreeState_value[trillian.TreeState_ACTIVE.String()] - if !ok { - return nil, fmt.Errorf("unknown TreeState: %v", trillian.TreeState_ACTIVE) - } - - tt, ok := trillian.TreeType_value[trillian.TreeType_LOG.String()] - if !ok { - return nil, fmt.Errorf("unknown TreeType: %v", trillian.TreeType_LOG) - } - - ctr := &trillian.CreateTreeRequest{Tree: &trillian.Tree{ - TreeState: trillian.TreeState(ts), - TreeType: trillian.TreeType(tt), - DisplayName: displayName, - MaxRootDuration: durationpb.New(time.Hour), - }} - - return ctr, nil -} diff --git a/internal/controller/common/utils/kubernetes/ensure/deployment.go b/internal/controller/common/utils/kubernetes/ensure/deployment.go deleted file mode 100644 index b0379d97a..000000000 --- a/internal/controller/common/utils/kubernetes/ensure/deployment.go +++ /dev/null @@ -1,112 +0,0 @@ -package ensure - -import ( - "slices" - - "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common/utils/kubernetes" - v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" -) - -const ( - CaTrustVolumeName = "ca-trust" - TLSVolumeName = "tls-cert" - CATRustMountPath = "/var/run/configs/tas/ca-trust" - - TLSVolumeMount = "/var/run/secrets/tas" - - TLSKeyPath = TLSVolumeMount + "/tls.key" - TLSCertPath = TLSVolumeMount + "/tls.crt" -) - -func Proxy() func(*v1.Deployment) error { - return func(dp *v1.Deployment) error { - SetProxyEnvs(dp.Spec.Template.Spec.Containers) - return nil - } -} - -// TrustedCA mount config map with trusted CA bundle to all deployment's containers. -func TrustedCA(lor *v1alpha1.LocalObjectReference) func(dp *v1.Deployment) error { - return func(dp *v1.Deployment) error { - template := &dp.Spec.Template - for i := range template.Spec.Containers { - env := kubernetes.FindEnvByNameOrCreate(&template.Spec.Containers[i], "SSL_CERT_DIR") - env.Value = CATRustMountPath + ":/var/run/secrets/kubernetes.io/serviceaccount" - - volumeMount := kubernetes.FindVolumeMountByNameOrCreate(&template.Spec.Containers[i], CaTrustVolumeName) - volumeMount.MountPath = CATRustMountPath - volumeMount.ReadOnly = true - - } - - projections := make([]corev1.VolumeProjection, 0) - if lor != nil { - projections = append(projections, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: lor.Name, - }, - }, - }) - } - - volume := kubernetes.FindVolumeByNameOrCreate(&template.Spec, CaTrustVolumeName) - if volume.Projected == nil { - volume.Projected = &corev1.ProjectedVolumeSource{} - } - volume.Projected.Sources = projections - - return nil - } -} - -// TLS mount secret with tls cert to all deployment's containers. -func TLS(tls v1alpha1.TLS, containerNames ...string) func(dp *v1.Deployment) error { - return func(dp *v1.Deployment) error { - template := &dp.Spec.Template - - for i, c := range template.Spec.Containers { - if slices.Contains(containerNames, c.Name) { - volumeMount := kubernetes.FindVolumeMountByNameOrCreate(&template.Spec.Containers[i], TLSVolumeName) - volumeMount.MountPath = TLSVolumeMount - volumeMount.ReadOnly = true - } - } - - volume := kubernetes.FindVolumeByNameOrCreate(&template.Spec, TLSVolumeName) - if volume.Projected == nil { - volume.Projected = &corev1.ProjectedVolumeSource{} - } - volume.Projected.Sources = []corev1.VolumeProjection{ - { - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: tls.CertRef.Name, - }, - Items: []corev1.KeyToPath{ - { - Key: tls.CertRef.Key, - Path: "tls.crt", - }, - }, - }, - }, - { - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: tls.PrivateKeyRef.Name, - }, - Items: []corev1.KeyToPath{ - { - Key: tls.PrivateKeyRef.Key, - Path: "tls.key", - }, - }, - }, - }, - } - return nil - } -} diff --git a/internal/controller/common/utils/kubernetes/ensure/deployment/deployment.go b/internal/controller/common/utils/kubernetes/ensure/deployment/deployment.go new file mode 100644 index 000000000..14a04c407 --- /dev/null +++ b/internal/controller/common/utils/kubernetes/ensure/deployment/deployment.go @@ -0,0 +1,29 @@ +package deployment + +import ( + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure" + ensureTls "github.com/securesign/operator/internal/controller/common/utils/tls/ensure" + v1 "k8s.io/api/apps/v1" +) + +func Proxy() func(*v1.Deployment) error { + return func(dp *v1.Deployment) error { + ensure.SetProxyEnvs(dp.Spec.Template.Spec.Containers) + return nil + } +} + +// TrustedCA mount config map with trusted CA bundle to all deployment's containers. +func TrustedCA(lor *v1alpha1.LocalObjectReference, containerNames ...string) func(dp *v1.Deployment) error { + return func(dp *v1.Deployment) error { + return ensureTls.TrustedCA(lor, containerNames...)(&dp.Spec.Template) + } +} + +// TLS mount secret with tls cert to all deployment's containers. +func TLS(tls v1alpha1.TLS, containerNames ...string) func(dp *v1.Deployment) error { + return func(dp *v1.Deployment) error { + return ensureTls.TLS(tls, containerNames...)(&dp.Spec.Template) + } +} diff --git a/internal/controller/common/utils/kubernetes/ensure/deployment_test.go b/internal/controller/common/utils/kubernetes/ensure/deployment/deployment_test.go similarity index 94% rename from internal/controller/common/utils/kubernetes/ensure/deployment_test.go rename to internal/controller/common/utils/kubernetes/ensure/deployment/deployment_test.go index 3d3f731bd..0bed9bc65 100644 --- a/internal/controller/common/utils/kubernetes/ensure/deployment_test.go +++ b/internal/controller/common/utils/kubernetes/ensure/deployment/deployment_test.go @@ -1,4 +1,4 @@ -package ensure +package deployment import ( "context" @@ -6,8 +6,8 @@ import ( "github.com/onsi/gomega" "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/common/utils/tls" testAction "github.com/securesign/operator/internal/testing/action" v1 "k8s.io/api/apps/v1" v3 "k8s.io/api/core/v1" @@ -18,7 +18,7 @@ import ( const name = "dp" -func TestEnsureTrustedCAFromAnnotations(t *testing.T) { +func TestEnsureTrustedCA(t *testing.T) { gomega.RegisterTestingT(t) t.Run("update existing object", func(t *testing.T) { @@ -56,7 +56,7 @@ func TestEnsureTrustedCAFromAnnotations(t *testing.T) { result, err := kubernetes.CreateOrUpdate(ctx, c, &v1.Deployment{ObjectMeta: v2.ObjectMeta{Name: name, Namespace: "default"}}, - TrustedCA(TrustedCAAnnotationToReference(map[string]string{annotations.TrustedCA: "test"})), + TrustedCA(&v1alpha1.LocalObjectReference{Name: "test"}, name), ) gomega.Expect(err).ToNot(gomega.HaveOccurred()) @@ -133,13 +133,13 @@ func TestEnsureTLS(t *testing.T) { gomega.Expect(c.Get(ctx, client.ObjectKey{Namespace: "default", Name: name}, existing)).To(gomega.Succeed()) gomega.Expect(existing.Spec.Template.Spec.Containers[0].VolumeMounts).To(gomega.HaveLen(1)) - gomega.Expect(existing.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(gomega.Equal(TLSVolumeName)) + gomega.Expect(existing.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(gomega.Equal(tls.TLSVolumeName)) gomega.Expect(existing.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath).To(gomega.Equal("/var/run/secrets/tas")) gomega.Expect(existing.Spec.Template.Spec.Containers[1].VolumeMounts).To(gomega.BeEmpty()) gomega.Expect(existing.Spec.Template.Spec.Volumes).To(gomega.HaveLen(1)) - gomega.Expect(existing.Spec.Template.Spec.Volumes[0].Name).To(gomega.Equal(TLSVolumeName)) + gomega.Expect(existing.Spec.Template.Spec.Volumes[0].Name).To(gomega.Equal(tls.TLSVolumeName)) gomega.Expect(existing.Spec.Template.Spec.Volumes[0].Projected.Sources).To(gomega.HaveLen(2)) gomega.Expect(existing.Spec.Template.Spec.Volumes[0].Projected.Sources).To(gomega.ContainElements( gomega.And( diff --git a/internal/controller/common/utils/kubernetes/ensure/utils.go b/internal/controller/common/utils/kubernetes/ensure/utils.go index 8eb1595dc..3dd43bc7b 100644 --- a/internal/controller/common/utils/kubernetes/ensure/utils.go +++ b/internal/controller/common/utils/kubernetes/ensure/utils.go @@ -2,8 +2,6 @@ package ensure import ( "github.com/operator-framework/operator-lib/proxy" - "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" v1 "k8s.io/api/core/v1" ) @@ -19,12 +17,3 @@ func SetProxyEnvs(containers []v1.Container) { } } } - -func TrustedCAAnnotationToReference(anns map[string]string) *v1alpha1.LocalObjectReference { - if v, ok := anns[annotations.TrustedCA]; ok { - return &v1alpha1.LocalObjectReference{ - Name: v, - } - } - return nil -} diff --git a/internal/controller/common/utils/tls/ensure/client.go b/internal/controller/common/utils/tls/ensure/client.go new file mode 100644 index 000000000..cb2675b43 --- /dev/null +++ b/internal/controller/common/utils/tls/ensure/client.go @@ -0,0 +1,45 @@ +package ensure + +import ( + "slices" + + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/common/utils/tls" + corev1 "k8s.io/api/core/v1" +) + +// TrustedCA mount config map with trusted CA bundle to all pod template's containers. +func TrustedCA(lor *v1alpha1.LocalObjectReference, containerNames ...string) func(template *corev1.PodTemplateSpec) error { + return func(template *corev1.PodTemplateSpec) error { + for i, c := range template.Spec.Containers { + if slices.Contains(containerNames, c.Name) { + env := kubernetes.FindEnvByNameOrCreate(&template.Spec.Containers[i], "SSL_CERT_DIR") + env.Value = tls.CATrustMountPath + ":/var/run/secrets/kubernetes.io/serviceaccount" + + volumeMount := kubernetes.FindVolumeMountByNameOrCreate(&template.Spec.Containers[i], tls.CaTrustVolumeName) + volumeMount.MountPath = tls.CATrustMountPath + volumeMount.ReadOnly = true + } + } + + projections := make([]corev1.VolumeProjection, 0) + if lor != nil { + projections = append(projections, corev1.VolumeProjection{ + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: lor.Name, + }, + }, + }) + } + + volume := kubernetes.FindVolumeByNameOrCreate(&template.Spec, tls.CaTrustVolumeName) + if volume.Projected == nil { + volume.Projected = &corev1.ProjectedVolumeSource{} + } + volume.Projected.Sources = projections + + return nil + } +} diff --git a/internal/controller/common/utils/tls/ensure/server.go b/internal/controller/common/utils/tls/ensure/server.go new file mode 100644 index 000000000..f9d0143d2 --- /dev/null +++ b/internal/controller/common/utils/tls/ensure/server.go @@ -0,0 +1,57 @@ +package ensure + +import ( + "slices" + + "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/common/utils/tls" + corev1 "k8s.io/api/core/v1" +) + +// TLS mount secret with tls cert to all deployment's containers. +func TLS(tlsCfg v1alpha1.TLS, containerNames ...string) func(*corev1.PodTemplateSpec) error { + return func(template *corev1.PodTemplateSpec) error { + for i, c := range template.Spec.Containers { + if slices.Contains(containerNames, c.Name) { + volumeMount := kubernetes.FindVolumeMountByNameOrCreate(&template.Spec.Containers[i], tls.TLSVolumeName) + volumeMount.MountPath = tls.TLSVolumeMount + volumeMount.ReadOnly = true + } + } + + volume := kubernetes.FindVolumeByNameOrCreate(&template.Spec, tls.TLSVolumeName) + if volume.Projected == nil { + volume.Projected = &corev1.ProjectedVolumeSource{} + } + volume.Projected.Sources = []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: tlsCfg.CertRef.Name, + }, + Items: []corev1.KeyToPath{ + { + Key: tlsCfg.CertRef.Key, + Path: "tls.crt", + }, + }, + }, + }, + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: tlsCfg.PrivateKeyRef.Name, + }, + Items: []corev1.KeyToPath{ + { + Key: tlsCfg.PrivateKeyRef.Key, + Path: "tls.key", + }, + }, + }, + }, + } + return nil + } +} diff --git a/internal/controller/common/utils/tls/tls.go b/internal/controller/common/utils/tls/tls.go new file mode 100644 index 000000000..fb58137ac --- /dev/null +++ b/internal/controller/common/utils/tls/tls.go @@ -0,0 +1,40 @@ +package tls + +import ( + "context" + "fmt" + + "github.com/securesign/operator/internal/apis" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "golang.org/x/exp/maps" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type objectWithTlsClient interface { + client.Object + apis.TlsClient +} + +func CAPath(ctx context.Context, cli client.Client, instance objectWithTlsClient) (string, error) { + lor := instance.GetTrustedCA() + switch { + case lor != nil: + cfgTrust, err := kubernetes.GetConfigMap(ctx, cli, instance.GetNamespace(), lor.Name) + if err != nil { + return "", err + } + if len(cfgTrust.Data) != 1 { + err = fmt.Errorf("%s ConfigMap can contain only 1 record", lor.Name) + return "", err + } + return CATrustMountPath + maps.Keys(cfgTrust.Data)[0], nil + case kubernetes.IsOpenShift(): + return "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt", nil + default: + return "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", nil + } +} + +func UseTlsClient(instance objectWithTlsClient) bool { + return kubernetes.IsOpenShift() || instance.GetTrustedCA() != nil +} diff --git a/internal/controller/common/utils/tls/types.go b/internal/controller/common/utils/tls/types.go new file mode 100644 index 000000000..f8e6a1cac --- /dev/null +++ b/internal/controller/common/utils/tls/types.go @@ -0,0 +1,11 @@ +package tls + +const ( + CaTrustVolumeName = "ca-trust" + CATrustMountPath = "/var/run/configs/tas/ca-trust" + + TLSVolumeName = "tls-cert" + TLSVolumeMount = "/var/run/secrets/tas" + TLSKeyPath = TLSVolumeMount + "/tls.key" + TLSCertPath = TLSVolumeMount + "/tls.crt" +) diff --git a/internal/controller/ctlog/actions/deployment.go b/internal/controller/ctlog/actions/deployment.go index 296e54b24..66423435d 100644 --- a/internal/controller/ctlog/actions/deployment.go +++ b/internal/controller/ctlog/actions/deployment.go @@ -5,6 +5,8 @@ import ( "fmt" "strconv" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/common/utils/tls" "github.com/securesign/operator/internal/images" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" @@ -70,8 +72,9 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) i.ensureDeployment(instance, RBACName, labels), ensure.ControllerReference[*v1.Deployment](instance, i.Client), ensure.Labels[*v1.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(ensure.TrustedCAAnnotationToReference(instance.Annotations)), + deployment.Proxy(), + deployment.TrustedCA(instance.GetTrustedCA(), "server"), + ensure.Optional(tls.UseTlsClient(instance), i.ensureTlsTrillian(ctx, instance)), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create ctlog server deployment: %w", err), instance) } @@ -172,3 +175,17 @@ func (i deployAction) ensureDeployment(instance *rhtasv1alpha1.CTlog, sa string, return nil } } + +func (i deployAction) ensureTlsTrillian(ctx context.Context, instance *rhtasv1alpha1.CTlog) func(*v1.Deployment) error { + return func(dp *v1.Deployment) error { + caPath, err := tls.CAPath(ctx, i.Client, instance) + if err != nil { + return fmt.Errorf("failed to get CA path: %w", err) + } + + container := kubernetes.FindContainerByNameOrCreate(&dp.Spec.Template.Spec, containerName) + + container.Args = append(container.Args, "--trillian_tls_ca_cert_file", caPath) + return nil + } +} diff --git a/internal/controller/ctlog/actions/resolve_tree.go b/internal/controller/ctlog/actions/resolve_tree.go index 004d4a226..966923b20 100644 --- a/internal/controller/ctlog/actions/resolve_tree.go +++ b/internal/controller/ctlog/actions/resolve_tree.go @@ -1,116 +1,24 @@ package actions import ( - "context" - "fmt" - - "github.com/google/trillian" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common" "github.com/securesign/operator/internal/controller/common/action" - "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/ctlog/utils" - actions2 "github.com/securesign/operator/internal/controller/trillian/actions" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/securesign/operator/internal/controller/common/action/tree" ) -type createTree func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) - -func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtasv1alpha1.CTlog] { - a := &resolveTreeAction{ - createTree: common.CreateTrillianTree, - } - - for _, opt := range opts { - opt(a) - } - return a -} - -type resolveTreeAction struct { - action.BaseAction - createTree createTree -} - -func (i resolveTreeAction) Name() string { - return "resolve treeID" -} - -func (i resolveTreeAction) CanHandle(_ context.Context, instance *rhtasv1alpha1.CTlog) bool { - c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) - switch { - case c == nil: - return false - case c.Reason != constants.Creating && c.Reason != constants.Ready: - return false - case instance.Status.TreeID == nil: - return true - case instance.Spec.TreeID != nil: - return !equality.Semantic.DeepEqual(instance.Spec.TreeID, instance.Status.TreeID) - default: - return false - } -} - -func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) *action.Result { - if instance.Spec.TreeID != nil && *instance.Spec.TreeID != int64(0) { - instance.Status.TreeID = instance.Spec.TreeID - // invalidate server config - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: ConfigCondition, - Status: metav1.ConditionFalse, - Reason: TrillianTreeReason, - Message: "Trillian tree changed", - }) - return i.StatusUpdate(ctx, instance) - } - var err error - var tree *trillian.Tree - var trillUrl string - - switch { - case instance.Spec.Trillian.Port == nil: - err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) - case instance.Spec.Trillian.Address == "": - trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.Namespace, *instance.Spec.Trillian.Port) - default: - trillUrl = fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port) - } - if err != nil { - return i.Failed(err) - } - i.Logger.V(1).Info("trillian logserver", "address", trillUrl) - - tree, err = i.createTree(ctx, "ctlog-tree", trillUrl, constants.CreateTreeDeadline) - if err != nil { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: ServerCondition, - Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), - }) - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), - ObservedGeneration: instance.Generation, +func NewResolveTreeAction() action.Action[*rhtasv1alpha1.CTlog] { + wrapper := tree.Wrapper[*rhtasv1alpha1.CTlog]( + func(rekor *rhtasv1alpha1.CTlog) *int64 { + return rekor.Spec.TreeID + }, + func(rekor *rhtasv1alpha1.CTlog) *int64 { + return rekor.Status.TreeID + }, + func(rekor *rhtasv1alpha1.CTlog, i *int64) { + rekor.Status.TreeID = i + }, + func(rekor *rhtasv1alpha1.CTlog) *rhtasv1alpha1.TrillianService { + return &rekor.Spec.Trillian }) - return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) - } - i.Recorder.Eventf(instance, v1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", tree.TreeId) - instance.Status.TreeID = &tree.TreeId - - // invalidate server config - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: ConfigCondition, - Status: metav1.ConditionFalse, - Reason: TrillianTreeReason, - Message: "Trillian tree changed", - }) - - return i.StatusUpdate(ctx, instance) + return tree.NewResolveTreeAction[*rhtasv1alpha1.CTlog]("ctlog", wrapper) } diff --git a/internal/controller/ctlog/actions/resolve_tree_test.go b/internal/controller/ctlog/actions/resolve_tree_test.go deleted file mode 100644 index 9b0bd684b..000000000 --- a/internal/controller/ctlog/actions/resolve_tree_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package actions - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - - "k8s.io/utils/ptr" - - "github.com/google/trillian" - . "github.com/onsi/gomega" - rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common/action" - "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/ctlog/utils" - "github.com/securesign/operator/internal/controller/trillian/actions" - testAction "github.com/securesign/operator/internal/testing/action" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestResolveTree_CanHandle(t *testing.T) { - tests := []struct { - name string - phase string - canHandle bool - treeID *int64 - statusTreeID *int64 - }{ - { - name: "spec.treeID is not nil and status.treeID is nil", - phase: constants.Creating, - canHandle: true, - treeID: ptr.To(int64(123456)), - }, - { - name: "spec.treeID != status.treeID", - phase: constants.Creating, - canHandle: true, - treeID: ptr.To(int64(123456)), - statusTreeID: ptr.To(int64(654321)), - }, - { - name: "spec.treeID is nil and status.treeID is not nil", - phase: constants.Creating, - canHandle: false, - statusTreeID: ptr.To(int64(654321)), - }, - { - name: "spec.treeID is nil and status.treeID is nil", - phase: constants.Creating, - canHandle: true, - }, - { - name: "no phase condition", - phase: "", - canHandle: false, - }, - { - name: constants.Ready, - phase: constants.Ready, - canHandle: true, - }, - { - name: constants.Pending, - phase: constants.Pending, - canHandle: false, - }, - { - name: constants.Creating, - phase: constants.Creating, - canHandle: true, - }, - { - name: constants.Initialize, - phase: constants.Initialize, - canHandle: false, - }, - { - name: constants.Failure, - phase: constants.Failure, - canHandle: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := testAction.FakeClientBuilder().Build() - a := testAction.PrepareAction(c, NewResolveTreeAction()) - instance := rhtasv1alpha1.CTlog{ - Spec: rhtasv1alpha1.CTlogSpec{ - TreeID: tt.treeID, - }, - Status: rhtasv1alpha1.CTlogStatus{ - TreeID: tt.statusTreeID, - }, - } - if tt.phase != "" { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Reason: tt.phase, - }) - } - - if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { - t.Errorf("CanHandle() = %v, want %v", got, tt.canHandle) - } - }) - } -} - -func TestResolveTree_Handle(t *testing.T) { - g := NewWithT(t) - type env struct { - spec rhtasv1alpha1.CTlogSpec - statusTreeId *int64 - createTree createTree - } - type want struct { - result *action.Result - verify func(Gomega, *rhtasv1alpha1.CTlog) - } - tests := []struct { - name string - env env - want want - }{ - { - name: "create a new tree", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - TreeID: nil, - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, nil), - }, - want: want{ - result: testAction.StatusUpdate(), - verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { - g.Expect(ctlog.Spec.TreeID).Should(BeNil()) - g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically(">", 0))) - g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically("==", 5555555))) - }, - }, - }, - { - name: "update tree", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - TreeID: ptr.To(int64(123456)), - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - statusTreeId: ptr.To(int64(654321)), - }, - want: want{ - result: testAction.StatusUpdate(), - verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { - g.Expect(ctlog.Spec.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) - g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically("==", *ctlog.Status.TreeID))) - }, - }, - }, - { - name: "use tree from spec", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - TreeID: ptr.To(int64(123456)), - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - }, - want: want{ - result: testAction.StatusUpdate(), - verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { - g.Expect(ctlog.Spec.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) - g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically("==", *ctlog.Status.TreeID))) - g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically("==", 123456))) - }, - }, - }, - { - name: "unable to create a new tree", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - TreeID: nil, - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - createTree: mockCreateTree(nil, errors.New("timeout error"), nil), - }, - want: want{ - result: testAction.FailedWithStatusUpdate(fmt.Errorf("could not create trillian tree: timeout error")), - verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { - g.Expect(ctlog.Spec.TreeID).Should(BeNil()) - g.Expect(ctlog.Status.TreeID).Should(BeNil()) - }, - }, - }, - { - name: "resolve trillian address", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("%s.%s.svc:%d", actions.LogserverDeploymentName, "default", 8091))) - }), - }, - want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "custom trillian address", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(1234)), Address: "custom-address.namespace.svc"}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("custom-address.namespace.svc:%d", 1234))) - }), - }, - want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "trillian port not specified", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: nil}, - }, - }, - want: want{ - result: testAction.Failed(fmt.Errorf("resolve treeID: %v", utils.TrillianPortNotSpecified)), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.TODO() - instance := &rhtasv1alpha1.CTlog{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ctlog", - Namespace: "default", - }, - Spec: tt.env.spec, - Status: rhtasv1alpha1.CTlogStatus{ - TreeID: tt.env.statusTreeId, - Conditions: []metav1.Condition{ - { - Type: constants.Ready, - Reason: constants.Creating, - }, - }, - }, - } - - c := testAction.FakeClientBuilder(). - WithObjects(instance). - WithStatusSubresource(instance). - Build() - - a := testAction.PrepareAction(c, NewResolveTreeAction(func(t *resolveTreeAction) { - if tt.env.createTree == nil { - t.createTree = mockCreateTree(nil, errors.New("createTree should not be executed"), nil) - } else { - t.createTree = tt.env.createTree - } - })) - - if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { - t.Errorf("CanHandle() = %v, want %v", got, tt.want.result) - } - if tt.want.verify != nil { - tt.want.verify(g, instance) - } - }) - } -} - -func mockCreateTree(tree *trillian.Tree, err error, verify func(displayName string, trillianURL string, deadline int64)) createTree { - return func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) { - if verify != nil { - verify(displayName, trillianURL, deadline) - } - return tree, err - } -} diff --git a/internal/controller/fulcio/actions/deployment.go b/internal/controller/fulcio/actions/deployment.go index 79b2f7d34..6fd27dd62 100644 --- a/internal/controller/fulcio/actions/deployment.go +++ b/internal/controller/fulcio/actions/deployment.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" "github.com/securesign/operator/internal/images" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" @@ -58,12 +59,6 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Fulcio instance.Spec.Ctlog.Port = &port } - caRef := instance.Spec.TrustedCA - if caRef == nil { - // override if spec.trustedCA is not defined - caRef = ensure.TrustedCAAnnotationToReference(instance.Annotations) - } - if result, err = kubernetes.CreateOrUpdate(ctx, i.Client, &v1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -74,8 +69,8 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Fulcio i.ensureDeployment(instance, RBACName, labels), ensure.ControllerReference[*v1.Deployment](instance, i.Client), ensure.Labels[*v1.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(caRef), + deployment.Proxy(), + deployment.TrustedCA(instance.GetTrustedCA(), "fulcio-server"), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create Fulcio: %w", err), instance) } diff --git a/internal/controller/fulcio/actions/fulcio_deployment_test.go b/internal/controller/fulcio/actions/fulcio_deployment_test.go index bde88b4d9..9a75422b9 100644 --- a/internal/controller/fulcio/actions/fulcio_deployment_test.go +++ b/internal/controller/fulcio/actions/fulcio_deployment_test.go @@ -5,6 +5,7 @@ import ( "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" "github.com/securesign/operator/internal/controller/fulcio/utils" "github.com/securesign/operator/internal/controller/labels" "golang.org/x/exp/maps" @@ -227,27 +228,18 @@ func createInstance() *v1alpha1.Fulcio { func createDeployment(instance *v1alpha1.Fulcio, labels map[string]string) (*v13.Deployment, error) { testAction := deployAction{} - caRef := instance.Spec.TrustedCA - if caRef == nil { - // override if spec.trustedCA is not defined - caRef = ensure.TrustedCAAnnotationToReference(instance.Annotations) - } d := &v13.Deployment{ ObjectMeta: v1.ObjectMeta{ Name: DeploymentName, Namespace: instance.Namespace, }, } - if caRef == nil { - // override if spec.trustedCA is not defined - ensure.TrustedCAAnnotationToReference(instance.Annotations) - } ensures := []func(*v13.Deployment) error{ testAction.ensureDeployment(instance, RBACName, labels), ensure.Labels[*v13.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(caRef), + deployment.Proxy(), + deployment.TrustedCA(instance.GetTrustedCA(), "fulcio-server"), } for _, en := range ensures { err := en(d) diff --git a/internal/controller/rekor/actions/redis/deployment.go b/internal/controller/rekor/actions/redis/deployment.go index 0dde1c230..377ce0b45 100644 --- a/internal/controller/rekor/actions/redis/deployment.go +++ b/internal/controller/rekor/actions/redis/deployment.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" "github.com/securesign/operator/internal/images" "github.com/securesign/operator/internal/controller/common/action" @@ -59,7 +60,7 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) i.ensureRedisDeployment(actions.RBACName, labels), ensure.ControllerReference[*v1.Deployment](instance, i.Client), ensure.Labels[*v1.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), + deployment.Proxy(), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create %s deployment: %w", actions.RedisDeploymentName, err), instance, metav1.Condition{ diff --git a/internal/controller/rekor/actions/server/deployment.go b/internal/controller/rekor/actions/server/deployment.go index 89b739e55..1519b91ac 100644 --- a/internal/controller/rekor/actions/server/deployment.go +++ b/internal/controller/rekor/actions/server/deployment.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/common/utils/tls" "github.com/securesign/operator/internal/images" "github.com/securesign/operator/internal/controller/annotations" @@ -68,8 +70,9 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) i.ensureServerDeployment(insCopy, actions.RBACName, labels), ensure.ControllerReference[*v2.Deployment](instance, i.Client), ensure.Labels[*v2.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(ensure.TrustedCAAnnotationToReference(instance.Annotations)), + deployment.Proxy(), + deployment.TrustedCA(instance.GetTrustedCA(), actions.ServerDeploymentName), + ensure.Optional(tls.UseTlsClient(instance), i.ensureTlsTrillian()), ); err != nil { return i.Error(ctx, fmt.Errorf("could create server Deployment: %w", err), instance) } @@ -234,3 +237,12 @@ func (i deployAction) ensureServerDeployment(instance *rhtasv1alpha1.Rekor, sa s return nil } } + +func (i deployAction) ensureTlsTrillian() func(*v2.Deployment) error { + return func(dp *v2.Deployment) error { + container := kubernetes.FindContainerByNameOrCreate(&dp.Spec.Template.Spec, actions.ServerDeploymentName) + + container.Args = append(container.Args, "--trillian_log_server.tls=true") + return nil + } +} diff --git a/internal/controller/rekor/actions/server/resolve_tree.go b/internal/controller/rekor/actions/server/resolve_tree.go index 122511b8c..9ca00219f 100644 --- a/internal/controller/rekor/actions/server/resolve_tree.go +++ b/internal/controller/rekor/actions/server/resolve_tree.go @@ -1,101 +1,24 @@ package server import ( - "context" - "fmt" - - "github.com/google/trillian" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common" "github.com/securesign/operator/internal/controller/common/action" - "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/rekor/actions" - "github.com/securesign/operator/internal/controller/rekor/utils" - actions2 "github.com/securesign/operator/internal/controller/trillian/actions" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/securesign/operator/internal/controller/common/action/tree" ) -type createTree func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) - -func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtasv1alpha1.Rekor] { - a := &resolveTreeAction{ - createTree: common.CreateTrillianTree, - } - - for _, opt := range opts { - opt(a) - } - return a -} - -type resolveTreeAction struct { - action.BaseAction - createTree createTree -} - -func (i resolveTreeAction) Name() string { - return "resolve treeID" -} - -func (i resolveTreeAction) CanHandle(_ context.Context, instance *rhtasv1alpha1.Rekor) bool { - c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) - switch { - case c == nil: - return false - case c.Reason != constants.Creating && c.Reason != constants.Ready: - return false - case instance.Status.TreeID == nil: - return true - case instance.Spec.TreeID != nil: - return !equality.Semantic.DeepEqual(instance.Spec.TreeID, instance.Status.TreeID) - default: - return false - } -} - -func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) *action.Result { - if instance.Spec.TreeID != nil && *instance.Spec.TreeID != int64(0) { - instance.Status.TreeID = instance.Spec.TreeID - return i.StatusUpdate(ctx, instance) - } - var err error - var tree *trillian.Tree - var trillUrl string - - switch { - case instance.Spec.Trillian.Port == nil: - err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) - case instance.Spec.Trillian.Address == "": - trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.Namespace, *instance.Spec.Trillian.Port) - default: - trillUrl = fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port) - } - if err != nil { - return i.Failed(err) - } - i.Logger.V(1).Info("trillian logserver", "address", trillUrl) - - tree, err = i.createTree(ctx, "rekor-tree", trillUrl, constants.CreateTreeDeadline) - if err != nil { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: actions.ServerCondition, - Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), - }) - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Status: metav1.ConditionFalse, - Reason: constants.Failure, - Message: err.Error(), +func NewResolveTreeAction() action.Action[*rhtasv1alpha1.Rekor] { + wrapper := tree.Wrapper[*rhtasv1alpha1.Rekor]( + func(rekor *rhtasv1alpha1.Rekor) *int64 { + return rekor.Spec.TreeID + }, + func(rekor *rhtasv1alpha1.Rekor) *int64 { + return rekor.Status.TreeID + }, + func(rekor *rhtasv1alpha1.Rekor, i *int64) { + rekor.Status.TreeID = i + }, + func(rekor *rhtasv1alpha1.Rekor) *rhtasv1alpha1.TrillianService { + return &rekor.Spec.Trillian }) - return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) - } - i.Recorder.Eventf(instance, v1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", tree.TreeId) - instance.Status.TreeID = &tree.TreeId - - return i.StatusUpdate(ctx, instance) + return tree.NewResolveTreeAction[*rhtasv1alpha1.Rekor]("rekor", wrapper) } diff --git a/internal/controller/rekor/actions/server/resolve_tree_test.go b/internal/controller/rekor/actions/server/resolve_tree_test.go deleted file mode 100644 index 26917022e..000000000 --- a/internal/controller/rekor/actions/server/resolve_tree_test.go +++ /dev/null @@ -1,292 +0,0 @@ -package server - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - - "github.com/google/trillian" - . "github.com/onsi/gomega" - rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common/action" - "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/rekor/utils" - "github.com/securesign/operator/internal/controller/trillian/actions" - testAction "github.com/securesign/operator/internal/testing/action" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" -) - -func TestResolveTree_CanHandle(t *testing.T) { - tests := []struct { - name string - phase string - canHandle bool - treeID *int64 - statusTreeID *int64 - }{ - { - name: "spec.treeID is not nil and status.treeID is nil", - phase: constants.Creating, - canHandle: true, - treeID: ptr.To(int64(123456)), - }, - { - name: "spec.treeID != status.treeID", - phase: constants.Creating, - canHandle: true, - treeID: ptr.To(int64(123456)), - statusTreeID: ptr.To(int64(654321)), - }, - { - name: "spec.treeID is nil and status.treeID is not nil", - phase: constants.Creating, - canHandle: false, - statusTreeID: ptr.To(int64(654321)), - }, - { - name: "spec.treeID is nil and status.treeID is nil", - phase: constants.Creating, - canHandle: true, - }, - { - name: "no phase condition", - phase: "", - canHandle: false, - }, - { - name: constants.Ready, - phase: constants.Ready, - canHandle: true, - }, - { - name: constants.Pending, - phase: constants.Pending, - canHandle: false, - }, - { - name: constants.Creating, - phase: constants.Creating, - canHandle: true, - }, - { - name: constants.Initialize, - phase: constants.Initialize, - canHandle: false, - }, - { - name: constants.Failure, - phase: constants.Failure, - canHandle: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := testAction.FakeClientBuilder().Build() - a := testAction.PrepareAction(c, NewResolveTreeAction()) - instance := rhtasv1alpha1.Rekor{ - Spec: rhtasv1alpha1.RekorSpec{ - TreeID: tt.treeID, - }, - Status: rhtasv1alpha1.RekorStatus{ - TreeID: tt.statusTreeID, - }, - } - if tt.phase != "" { - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: constants.Ready, - Reason: tt.phase, - }) - } - - if got := a.CanHandle(context.TODO(), &instance); !reflect.DeepEqual(got, tt.canHandle) { - t.Errorf("CanHandle() = %v, want %v", got, tt.canHandle) - } - }) - } -} - -func TestResolveTree_Handle(t *testing.T) { - g := NewWithT(t) - type env struct { - spec rhtasv1alpha1.RekorSpec - statusTreeId *int64 - createTree createTree - } - type want struct { - result *action.Result - verify func(Gomega, *rhtasv1alpha1.Rekor) - } - tests := []struct { - name string - env env - want want - }{ - { - name: "create a new tree", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - TreeID: nil, - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, nil), - }, - want: want{ - result: testAction.StatusUpdate(), - verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { - g.Expect(rekor.Spec.TreeID).Should(BeNil()) - g.Expect(rekor.Status.TreeID).ShouldNot(BeNil()) - g.Expect(rekor.Status.TreeID).To(HaveValue(BeNumerically(">", 0))) - g.Expect(rekor.Status.TreeID).To(HaveValue(BeNumerically("==", 5555555))) - }, - }, - }, - { - name: "update tree", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - TreeID: ptr.To(int64(123456)), - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - statusTreeId: ptr.To(int64(654321)), - }, - want: want{ - result: testAction.StatusUpdate(), - verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { - g.Expect(rekor.Spec.TreeID).ShouldNot(BeNil()) - g.Expect(rekor.Status.TreeID).ShouldNot(BeNil()) - g.Expect(rekor.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) - g.Expect(rekor.Spec.TreeID).To(HaveValue(BeNumerically("==", *rekor.Status.TreeID))) - }, - }, - }, - { - name: "use tree from spec", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - TreeID: ptr.To(int64(123456)), - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - }, - want: want{ - result: testAction.StatusUpdate(), - verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { - g.Expect(rekor.Spec.TreeID).ShouldNot(BeNil()) - g.Expect(rekor.Status.TreeID).ShouldNot(BeNil()) - g.Expect(rekor.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) - g.Expect(rekor.Spec.TreeID).To(HaveValue(BeNumerically("==", *rekor.Status.TreeID))) - g.Expect(rekor.Status.TreeID).To(HaveValue(BeNumerically("==", 123456))) - }, - }, - }, - { - name: "unable to create a new tree", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - TreeID: nil, - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, - }, - createTree: mockCreateTree(nil, errors.New("timeout error"), nil), - }, - want: want{ - result: testAction.FailedWithStatusUpdate(fmt.Errorf("could not create trillian tree: timeout error")), - verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { - g.Expect(rekor.Spec.TreeID).Should(BeNil()) - g.Expect(rekor.Status.TreeID).Should(BeNil()) - }, - }, - }, - { - name: "resolve trillian address", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(1234))}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("%s.%s.svc:%d", actions.LogserverDeploymentName, "default", 1234))) - }), - }, - want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "custom trillian address", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(1234)), Address: "custom-address.namespace.svc"}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("custom-address.namespace.svc:%d", 1234))) - }), - }, - want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "trillian port not specified", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: nil}, - }, - }, - want: want{ - result: testAction.Failed(fmt.Errorf("resolve treeID: %v", utils.TrillianPortNotSpecified)), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.TODO() - instance := &rhtasv1alpha1.Rekor{ - ObjectMeta: metav1.ObjectMeta{ - Name: "rekor", - Namespace: "default", - }, - Spec: tt.env.spec, - Status: rhtasv1alpha1.RekorStatus{ - TreeID: tt.env.statusTreeId, - Conditions: []metav1.Condition{ - { - Type: constants.Ready, - Reason: constants.Creating, - }, - }, - }, - } - - c := testAction.FakeClientBuilder(). - WithObjects(instance). - WithStatusSubresource(instance). - Build() - - a := testAction.PrepareAction(c, NewResolveTreeAction(func(t *resolveTreeAction) { - if tt.env.createTree == nil { - t.createTree = mockCreateTree(nil, errors.New("createTree should not be executed"), nil) - } else { - t.createTree = tt.env.createTree - } - })) - - if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { - t.Errorf("CanHandle() = %v, want %v", got, tt.want.result) - } - if tt.want.verify != nil { - tt.want.verify(g, instance) - } - }) - } -} - -func mockCreateTree(tree *trillian.Tree, err error, verify func(displayName string, trillianURL string, deadline int64)) createTree { - return func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) { - if verify != nil { - verify(displayName, trillianURL, deadline) - } - return tree, err - } -} diff --git a/internal/controller/trillian/actions/constants.go b/internal/controller/trillian/actions/constants.go index 7e47fcd5c..15f88d608 100644 --- a/internal/controller/trillian/actions/constants.go +++ b/internal/controller/trillian/actions/constants.go @@ -12,6 +12,9 @@ const ( LogSignerComponentName = "trillian-logsigner" LogSignerMonitoringName = "prometheus-k8s-logsigner" + LogServerTLSSecret = "%s-trillian-logserver-tls" + LogSignerTLSSecret = "%s-trillian-logsigner-tls" + RBACName = "trillian" DbCondition = "DBAvailable" diff --git a/internal/controller/trillian/actions/db/deployment.go b/internal/controller/trillian/actions/db/deployment.go index cd33453cc..1d40f1aa6 100644 --- a/internal/controller/trillian/actions/db/deployment.go +++ b/internal/controller/trillian/actions/db/deployment.go @@ -6,6 +6,8 @@ import ( "fmt" "slices" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/common/utils/tls" "github.com/securesign/operator/internal/images" "github.com/securesign/operator/internal/controller/common/action" @@ -90,7 +92,7 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli i.ensureDbDeployment(instance, actions.RBACName, scc, labels), ensure.ControllerReference[*v2.Deployment](instance, i.Client), ensure.Labels[*v2.Deployment](maps.Keys(labels), labels), - ensure.Optional(trillianUtils.UseTLS(instance), i.ensureTLS(instance)), + ensure.Optional(trillianUtils.UseTLSDb(instance), i.ensureTLS(instance)), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create Trillian DB: %w", err), instance, metav1.Condition{ Type: actions.DbCondition, @@ -237,7 +239,7 @@ func (i deployAction) ensureDbDeployment(instance *rhtasv1alpha1.Trillian, sa st func (i deployAction) ensureTLS(instance *rhtasv1alpha1.Trillian) func(deployment *v2.Deployment) error { return func(dp *v2.Deployment) error { - if err := ensure.TLS(instance.Status.Db.TLS, actions.DbDeploymentName)(dp); err != nil { + if err := deployment.TLS(instance.Status.Db.TLS, actions.DbDeploymentName)(dp); err != nil { return err } @@ -262,21 +264,21 @@ func (i deployAction) ensureTLS(instance *rhtasv1alpha1.Trillian) func(deploymen container.LivenessProbe.Exec.Command = []string{"bash", "-c", livenessCommand + " --ssl"} if i := slices.Index(container.Args, "--ssl-cert"); i == -1 { - container.Args = append(container.Args, "--ssl-cert", ensure.TLSCertPath) + container.Args = append(container.Args, "--ssl-cert", tls.TLSCertPath) } else { if len(container.Args)-1 < i+1 { - container.Args = append(container.Args, ensure.TLSCertPath) + container.Args = append(container.Args, tls.TLSCertPath) } - container.Args[i+1] = ensure.TLSCertPath + container.Args[i+1] = tls.TLSCertPath } if i := slices.Index(container.Args, "--ssl-key"); i == -1 { - container.Args = append(container.Args, "--ssl-key", ensure.TLSKeyPath) + container.Args = append(container.Args, "--ssl-key", tls.TLSKeyPath) } else { if len(container.Args)-1 < i+1 { - container.Args = append(container.Args, ensure.TLSKeyPath) + container.Args = append(container.Args, tls.TLSKeyPath) } - container.Args[i+1] = ensure.TLSKeyPath + container.Args[i+1] = tls.TLSKeyPath } return nil } diff --git a/internal/controller/trillian/actions/logserver/deployment.go b/internal/controller/trillian/actions/logserver/deployment.go index 3b4deaf82..8bc328e80 100644 --- a/internal/controller/trillian/actions/logserver/deployment.go +++ b/internal/controller/trillian/actions/logserver/deployment.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/common/utils/tls" "github.com/securesign/operator/internal/images" "github.com/securesign/operator/internal/controller/common/action" @@ -48,8 +50,28 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli labels := labels.For(actions.LogServerComponentName, actions.LogserverDeploymentName, instance.Name) insCopy := instance.DeepCopy() - if insCopy.Spec.TrustedCA == nil { - insCopy.Spec.TrustedCA = ensure.TrustedCAAnnotationToReference(instance.Annotations) + // TLS + switch { + case insCopy.Spec.TLS.CertRef != nil: + insCopy.Status.TLS = insCopy.Spec.TLS + case kubernetes.IsOpenShift(): + insCopy.Status.TLS = rhtasv1alpha1.TLS{ + CertRef: &rhtasv1alpha1.SecretKeySelector{ + LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: fmt.Sprintf(actions.LogServerTLSSecret, instance.Name)}, + Key: "tls.crt", + }, + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{ + LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: fmt.Sprintf(actions.LogServerTLSSecret, instance.Name)}, + Key: "tls.key", + }, + } + default: + i.Logger.V(1).Info("Communication to trillian-db is insecure") + } + + caPath, err := tls.CAPath(ctx, i.Client, instance) + if err != nil { + return i.Error(ctx, fmt.Errorf("failed to get CA path: %w", err), instance) } if result, err = kubernetes.CreateOrUpdate(ctx, i.Client, @@ -62,9 +84,10 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli trillianUtils.EnsureServerDeployment(insCopy, images.Registry.Get(images.TrillianServer), actions.LogserverDeploymentName, actions.RBACName, labels), ensure.ControllerReference[*apps.Deployment](insCopy, i.Client), ensure.Labels[*apps.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(insCopy.Spec.TrustedCA), - ensure.Optional(trillianUtils.UseTLS(insCopy), i.withTlsDB(ctx, insCopy)), + deployment.Proxy(), + deployment.TrustedCA(insCopy.GetTrustedCA(), "wait-for-trillian-db", actions.LogserverDeploymentName), + ensure.Optional(trillianUtils.UseTLSDb(insCopy), trillianUtils.WithTlsDB(insCopy, caPath, actions.LogserverDeploymentName)), + ensure.Optional(insCopy.Status.TLS.CertRef != nil, trillianUtils.EnsureTLSServer(insCopy, actions.LogserverDeploymentName)), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create Trillian server: %w", err), instance, metav1.Condition{ Type: actions.ServerCondition, @@ -86,23 +109,3 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli return i.Continue() } } - -func (i deployAction) withTlsDB(ctx context.Context, instance *rhtasv1alpha1.Trillian) func(deployment *apps.Deployment) error { - return func(dp *apps.Deployment) error { - caPath, err := trillianUtils.CAPath(ctx, i.Client, instance) - if err != nil { - return fmt.Errorf("failed to get CA path: %w", err) - } - - c := kubernetes.FindContainerByNameOrCreate(&dp.Spec.Template.Spec, actions.LogserverDeploymentName) - c.Args = append(c.Args, "--mysql_tls_ca", caPath) - - mysqlServerName := "$(MYSQL_HOSTNAME)." + instance.Namespace + ".svc" - if !*instance.Spec.Db.Create { - mysqlServerName = "$(MYSQL_HOSTNAME)" - } - c.Args = append(c.Args, "--mysql_server_name", mysqlServerName) - return nil - } - -} diff --git a/internal/controller/trillian/actions/logserver/service.go b/internal/controller/trillian/actions/logserver/service.go index d89ac516a..69e322960 100644 --- a/internal/controller/trillian/actions/logserver/service.go +++ b/internal/controller/trillian/actions/logserver/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure" @@ -45,6 +46,12 @@ func (i createServiceAction) Handle(ctx context.Context, instance *rhtasv1alpha1 ) labels := labels.For(actions.LogServerComponentName, actions.LogserverDeploymentName, instance.Name) + + tlsAnnotations := map[string]string{} + if instance.Spec.Db.TLS.CertRef == nil { + tlsAnnotations[annotations.TLS] = fmt.Sprintf(actions.LogServerTLSSecret, instance.Name) + } + ports := []v1.ServicePort{ { Name: actions.ServerPortName, @@ -68,6 +75,8 @@ func (i createServiceAction) Handle(ctx context.Context, instance *rhtasv1alpha1 kubernetes.EnsureServiceSpec(labels, ports...), ensure.ControllerReference[*v1.Service](instance, i.Client), ensure.Labels[*v1.Service](maps.Keys(labels), labels), + //TLS: Annotate service + ensure.Optional(kubernetes.IsOpenShift(), ensure.Annotations[*v1.Service]([]string{annotations.TLS}, tlsAnnotations)), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create service: %w", err), instance) } diff --git a/internal/controller/trillian/actions/logsigner/deployment.go b/internal/controller/trillian/actions/logsigner/deployment.go index 986a4792b..48b495123 100644 --- a/internal/controller/trillian/actions/logsigner/deployment.go +++ b/internal/controller/trillian/actions/logsigner/deployment.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/common/utils/tls" "github.com/securesign/operator/internal/images" "github.com/securesign/operator/internal/controller/common/action" @@ -46,11 +48,30 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli ) labels := labels.For(actions.LogSignerComponentName, actions.LogsignerDeploymentName, instance.Name) + insCopy := instance.DeepCopy() - caTrustRef := ensure.TrustedCAAnnotationToReference(instance.Annotations) - // override if spec.trustedCA is defined - if instance.Spec.TrustedCA != nil { - caTrustRef = instance.Spec.TrustedCA + // TLS + switch { + case insCopy.Spec.TLS.CertRef != nil: + insCopy.Status.TLS = insCopy.Spec.TLS + case kubernetes.IsOpenShift(): + insCopy.Status.TLS = rhtasv1alpha1.TLS{ + CertRef: &rhtasv1alpha1.SecretKeySelector{ + LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: fmt.Sprintf(actions.LogSignerTLSSecret, instance.Name)}, + Key: "tls.crt", + }, + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{ + LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: fmt.Sprintf(actions.LogSignerTLSSecret, instance.Name)}, + Key: "tls.key", + }, + } + default: + i.Logger.V(1).Info("Communication to trillian-db is insecure") + } + + caPath, err := tls.CAPath(ctx, i.Client, instance) + if err != nil { + return i.Error(ctx, fmt.Errorf("failed to get CA path: %w", err), instance) } if result, err = kubernetes.CreateOrUpdate(ctx, i.Client, @@ -61,10 +82,12 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli }, }, trillianUtils.EnsureServerDeployment(instance, images.Registry.Get(images.TrillianLogSigner), actions.LogsignerDeploymentName, actions.RBACName, labels, "--force_master=true"), - ensure.ControllerReference[*apps.Deployment](instance, i.Client), + ensure.ControllerReference[*apps.Deployment](insCopy, i.Client), ensure.Labels[*apps.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(caTrustRef), + deployment.Proxy(), + deployment.TrustedCA(insCopy.GetTrustedCA(), "wait-for-trillian-db", actions.LogsignerDeploymentName), + ensure.Optional(trillianUtils.UseTLSDb(insCopy), trillianUtils.WithTlsDB(insCopy, caPath, actions.LogsignerDeploymentName)), + ensure.Optional(insCopy.Status.TLS.CertRef != nil, trillianUtils.EnsureTLSServer(insCopy, actions.LogsignerDeploymentName)), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create Trillian LogSigner: %w", err), instance, metav1.Condition{ Type: actions.SignerCondition, diff --git a/internal/controller/trillian/actions/logsigner/service.go b/internal/controller/trillian/actions/logsigner/service.go index 2abee6488..37345d2c1 100644 --- a/internal/controller/trillian/actions/logsigner/service.go +++ b/internal/controller/trillian/actions/logsigner/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure" @@ -46,6 +47,11 @@ func (i createServiceAction) Handle(ctx context.Context, instance *rhtasv1alpha1 labels := labels.For(actions.LogSignerComponentName, actions.LogsignerDeploymentName, instance.Name) + tlsAnnotations := map[string]string{} + if instance.Spec.Db.TLS.CertRef == nil { + tlsAnnotations[annotations.TLS] = fmt.Sprintf(actions.LogSignerTLSSecret, instance.Name) + } + ports := []v1.ServicePort{ { Name: actions.ServerPortName, @@ -70,6 +76,8 @@ func (i createServiceAction) Handle(ctx context.Context, instance *rhtasv1alpha1 kubernetes.EnsureServiceSpec(labels, ports...), ensure.ControllerReference[*v1.Service](instance, i.Client), ensure.Labels[*v1.Service](maps.Keys(labels), labels), + //TLS: Annotate service + ensure.Optional(kubernetes.IsOpenShift(), ensure.Annotations[*v1.Service]([]string{annotations.TLS}, tlsAnnotations)), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create service: %w", err), instance) } diff --git a/internal/controller/trillian/utils/server-deployment.go b/internal/controller/trillian/utils/server-deployment.go index b2d89ac7c..a79f27711 100644 --- a/internal/controller/trillian/utils/server-deployment.go +++ b/internal/controller/trillian/utils/server-deployment.go @@ -4,15 +4,17 @@ import ( "errors" "strconv" - "github.com/securesign/operator/internal/images" - "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/utils" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/common/utils/tls" "github.com/securesign/operator/internal/controller/trillian/actions" + "github.com/securesign/operator/internal/images" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) func EnsureServerDeployment(instance *v1alpha1.Trillian, image string, name string, sa string, labels map[string]string, args ...string) func(deployment *apps.Deployment) error { @@ -124,14 +126,71 @@ func EnsureServerDeployment(instance *v1alpha1.Trillian, image string, name stri } port := kubernetes.FindPortByNameOrCreate(container, "8091-tcp") - port.ContainerPort = 8091 + port.ContainerPort = actions.ServerPort port.Protocol = core.ProtocolTCP if instance.Spec.Monitoring.Enabled { monitoring := kubernetes.FindPortByNameOrCreate(container, "monitoring") - monitoring.ContainerPort = 8090 + monitoring.ContainerPort = actions.MetricsPort monitoring.Protocol = core.ProtocolTCP } + + if container.LivenessProbe == nil { + container.LivenessProbe = &core.Probe{} + } + if container.LivenessProbe.HTTPGet == nil { + container.LivenessProbe.HTTPGet = &core.HTTPGetAction{} + } + container.LivenessProbe.HTTPGet.Path = "/healthz" + container.LivenessProbe.HTTPGet.Port = intstr.FromInt32(actions.MetricsPort) + + if container.ReadinessProbe == nil { + container.ReadinessProbe = &core.Probe{} + } + if container.ReadinessProbe.HTTPGet == nil { + container.ReadinessProbe.HTTPGet = &core.HTTPGetAction{} + } + container.ReadinessProbe.HTTPGet.Path = "/healthz" + container.ReadinessProbe.HTTPGet.Port = intstr.FromInt32(actions.MetricsPort) + container.ReadinessProbe.InitialDelaySeconds = 10 + return nil + } +} + +func WithTlsDB(instance *v1alpha1.Trillian, caPath string, name string) func(deployment *apps.Deployment) error { + return func(dp *apps.Deployment) error { + c := kubernetes.FindContainerByNameOrCreate(&dp.Spec.Template.Spec, name) + c.Args = append(c.Args, "--mysql_tls_ca", caPath) + + mysqlServerName := "$(MYSQL_HOSTNAME)." + instance.Namespace + ".svc" + if !*instance.Spec.Db.Create { + mysqlServerName = "$(MYSQL_HOSTNAME)" + } + c.Args = append(c.Args, "--mysql_server_name", mysqlServerName) + return nil + } +} + +func EnsureTLSServer(instance *v1alpha1.Trillian, name string) func(deployment *apps.Deployment) error { + return func(dp *apps.Deployment) error { + if err := deployment.TLS(instance.Status.TLS, name)(dp); err != nil { + return err + } + + container := kubernetes.FindContainerByNameOrCreate(&dp.Spec.Template.Spec, name) + + container.Args = append(container.Args, "--tls_cert_file", tls.TLSCertPath) + + if container.ReadinessProbe != nil { + container.ReadinessProbe.HTTPGet.Scheme = core.URISchemeHTTPS + } + + if container.LivenessProbe != nil { + container.LivenessProbe.HTTPGet.Scheme = core.URISchemeHTTPS + } + + container.Args = append(container.Args, "--tls_key_file", tls.TLSKeyPath) + return nil } } diff --git a/internal/controller/trillian/utils/tls.go b/internal/controller/trillian/utils/tls.go index 878cd45f6..b20fca5b1 100644 --- a/internal/controller/trillian/utils/tls.go +++ b/internal/controller/trillian/utils/tls.go @@ -1,18 +1,11 @@ package trillianUtils import ( - "context" - "fmt" - rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/utils" - "github.com/securesign/operator/internal/controller/common/utils/kubernetes" - "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure" - "golang.org/x/exp/maps" - "sigs.k8s.io/controller-runtime/pkg/client" ) -func UseTLS(instance *rhtasv1alpha1.Trillian) bool { +func UseTLSDb(instance *rhtasv1alpha1.Trillian) bool { if instance == nil { return false @@ -24,28 +17,9 @@ func UseTLS(instance *rhtasv1alpha1.Trillian) bool { } // external DB - if !utils.IsEnabled(instance.Spec.Db.Create) && instance.Spec.TrustedCA != nil { + if !utils.IsEnabled(instance.Spec.Db.Create) && instance.GetTrustedCA() != nil { return true } return false } - -func CAPath(ctx context.Context, cli client.Client, instance *rhtasv1alpha1.Trillian) (string, error) { - switch { - case instance.Spec.TrustedCA != nil: - cfgTrust, err := kubernetes.GetConfigMap(ctx, cli, instance.Namespace, instance.Spec.TrustedCA.Name) - if err != nil { - return "", err - } - if len(cfgTrust.Data) != 1 { - err = fmt.Errorf("%s ConfigMap can contain only 1 record", instance.Spec.TrustedCA.Name) - return "", err - } - return ensure.CATRustMountPath + maps.Keys(cfgTrust.Data)[0], nil - case kubernetes.IsOpenShift(): - return "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt", nil - default: - return "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", nil - } -} diff --git a/internal/controller/tsa/actions/deployment.go b/internal/controller/tsa/actions/deployment.go index c022e5176..97938e786 100644 --- a/internal/controller/tsa/actions/deployment.go +++ b/internal/controller/tsa/actions/deployment.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" + "github.com/securesign/operator/internal/controller/rekor/actions" "github.com/securesign/operator/internal/images" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" @@ -70,12 +72,6 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Timest labels := labels.For(ComponentName, DeploymentName, instance.Name) - caRef := ensure.TrustedCAAnnotationToReference(instance.Annotations) - // override if spec.trustedCA is defined - if instance.Spec.TrustedCA != nil { - caRef = instance.Spec.TrustedCA - } - if result, err = kubernetes.CreateOrUpdate(ctx, i.Client, &apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -86,8 +82,8 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Timest i.ensureDeployment(instance, RBACName, labels), ensure.ControllerReference[*apps.Deployment](instance, i.Client), ensure.Labels[*apps.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), - ensure.TrustedCA(caRef), + deployment.Proxy(), + deployment.TrustedCA(instance.GetTrustedCA(), actions.ServerDeploymentName), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create TSA Server: %w", err), instance, metav1.Condition{ Type: TSAServerCondition, diff --git a/internal/controller/tuf/actions/deployment.go b/internal/controller/tuf/actions/deployment.go index d0900c9aa..9ef6de86d 100644 --- a/internal/controller/tuf/actions/deployment.go +++ b/internal/controller/tuf/actions/deployment.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/ensure/deployment" "github.com/securesign/operator/internal/images" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" @@ -57,7 +58,7 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Tuf) * i.createTufDeployment(instance, tufConstants.RBACName, labels), ensure.ControllerReference[*v1.Deployment](instance, i.Client), ensure.Labels[*v1.Deployment](maps.Keys(labels), labels), - ensure.Proxy(), + deployment.Proxy(), ); err != nil { return i.Error(ctx, fmt.Errorf("could not create TUF: %w", err), instance) } diff --git a/internal/images/images.env b/internal/images/images.env index 6a8624502..ae42cfc02 100644 --- a/internal/images/images.env +++ b/internal/images/images.env @@ -2,6 +2,7 @@ RELATED_IMAGE_TRILLIAN_LOG_SIGNER=registry.redhat.io/rhtas/trillian-logsigner-rh RELATED_IMAGE_TRILLIAN_LOG_SERVER=registry.redhat.io/rhtas/trillian-logserver-rhel9@sha256:7af78c7bc4df097ffeeef345f1d13289695f715221957579ee65daeef2fa3f5b RELATED_IMAGE_TRILLIAN_DB=registry.redhat.io/rhtas/trillian-database-rhel9@sha256:501612745e63e5504017079388bec191ffacf00ffdebde7be6ca5b8e4fd9d323 RELATED_IMAGE_TRILLIAN_NETCAT=registry.redhat.io/openshift4/ose-tools-rhel8@sha256:486b4d2dd0d10c5ef0212714c94334e04fe8a3d36cf619881986201a50f123c7 +RELATED_IMAGE_TRILLIAN_CREATE_TREE=registry.redhat.io/rhtas/createtree-rhel9@sha256:f66a707e68fb0cdcfcddc318407fe60d72f50a7b605b5db55743eccc14a422ba RELATED_IMAGE_FULCIO_SERVER=registry.redhat.io/rhtas/fulcio-rhel9@sha256:4b5765bbfd3dac5fa027d2fb3d672b6ebffbc573b9413ab4cb189c50fa6f9a09 RELATED_IMAGE_REKOR_REDIS=registry.redhat.io/rhtas/trillian-redis-rhel9@sha256:18820b1fbdbc2cc3e917822974910332d937b03cfe781628bd986fd6a5ee318e RELATED_IMAGE_REKOR_SERVER=registry.redhat.io/rhtas/rekor-server-rhel9@sha256:81e10e34f02b21bb8295e7b5c93797fc8c0e43a1a0d8304cca1b07415a3ed6f5 diff --git a/internal/images/images.go b/internal/images/images.go index 5d19a8adc..417696cc0 100644 --- a/internal/images/images.go +++ b/internal/images/images.go @@ -11,10 +11,11 @@ import ( type Image string const ( - TrillianLogSigner Image = "RELATED_IMAGE_TRILLIAN_LOG_SIGNER" - TrillianServer Image = "RELATED_IMAGE_TRILLIAN_LOG_SERVER" - TrillianDb Image = "RELATED_IMAGE_TRILLIAN_DB" - TrillianNetcat Image = "RELATED_IMAGE_TRILLIAN_NETCAT" + TrillianLogSigner Image = "RELATED_IMAGE_TRILLIAN_LOG_SIGNER" + TrillianServer Image = "RELATED_IMAGE_TRILLIAN_LOG_SERVER" + TrillianDb Image = "RELATED_IMAGE_TRILLIAN_DB" + TrillianNetcat Image = "RELATED_IMAGE_TRILLIAN_NETCAT" + TrillianCreateTree Image = "RELATED_IMAGE_TRILLIAN_CREATE_TREE" FulcioServer Image = "RELATED_IMAGE_FULCIO_SERVER" diff --git a/internal/testing/action/result.go b/internal/testing/action/result.go index e9ac42724..36af85ebd 100644 --- a/internal/testing/action/result.go +++ b/internal/testing/action/result.go @@ -15,6 +15,13 @@ func StatusUpdate() *action.Result { return &action.Result{Result: reconcile.Result{Requeue: false}} } +func Error(err error) *action.Result { + return &action.Result{ + Result: reconcile.Result{}, + Err: err, + } +} + func Failed(err error) *action.Result { return &action.Result{ Result: reconcile.Result{RequeueAfter: time.Duration(5) * time.Second},