From 3d96c869233cf3da589a4cc87af1382ae1929b93 Mon Sep 17 00:00:00 2001 From: Firas Ghanmi Date: Thu, 8 Aug 2024 12:03:00 +0200 Subject: [PATCH] updates: Create Tree Jobs, enable TLS on Trillian, Rekor and Ctlog --- api/v1alpha1/ctlog_types.go | 4 + api/v1alpha1/ctlog_types_test.go | 10 + api/v1alpha1/zz_generated.deepcopy.go | 6 + .../rhtas-operator.clusterserviceversion.yaml | 6 +- bundle/manifests/rhtas.redhat.com_ctlogs.yaml | 111 ++++++++++ .../manifests/rhtas.redhat.com_fulcios.yaml | 4 +- .../rhtas.redhat.com_securesigns.yaml | 121 ++++++----- .../manifests/rhtas.redhat.com_trillians.yaml | 61 ------ config/crd/bases/rhtas.redhat.com_ctlogs.yaml | 111 ++++++++++ .../crd/bases/rhtas.redhat.com_fulcios.yaml | 4 +- .../bases/rhtas.redhat.com_securesigns.yaml | 60 +++++- config/manager/kustomization.yaml | 6 +- config/samples/rhtas_v1alpha1_securesign.yaml | 4 +- internal/controller/common/create_tree.go | 98 --------- internal/controller/constants/images.go | 6 +- .../controller/ctlog/actions/config_map.go | 79 +++++++ .../controller/ctlog/actions/constants.go | 16 +- .../ctlog/actions/create_tree_job.go | 191 +++++++++++++++++ .../controller/ctlog/actions/deployment.go | 102 ++++++++++ internal/controller/ctlog/actions/rbac.go | 2 +- .../controller/ctlog/actions/resolve_tree.go | 69 +++++-- .../ctlog/actions/resolve_tree_test.go | 88 ++++---- internal/controller/ctlog/actions/service.go | 27 ++- internal/controller/ctlog/ctlog_controller.go | 5 +- .../controller/ctlog/ctlog_controller_test.go | 12 ++ internal/controller/rekor/actions/rbac.go | 2 +- .../rekor/actions/server/create_tree_job.go | 192 ++++++++++++++++++ .../rekor/actions/server/deployment.go | 2 +- .../rekor/actions/server/resolve_tree.go | 69 +++++-- .../rekor/actions/server/resolve_tree_test.go | 90 ++++---- internal/controller/rekor/rekor_controller.go | 1 + 31 files changed, 1164 insertions(+), 395 deletions(-) delete mode 100644 internal/controller/common/create_tree.go create mode 100644 internal/controller/ctlog/actions/config_map.go create mode 100644 internal/controller/ctlog/actions/create_tree_job.go create mode 100644 internal/controller/rekor/actions/server/create_tree_job.go diff --git a/api/v1alpha1/ctlog_types.go b/api/v1alpha1/ctlog_types.go index 9a37dd3e7..f7aee835d 100644 --- a/api/v1alpha1/ctlog_types.go +++ b/api/v1alpha1/ctlog_types.go @@ -42,6 +42,9 @@ type CTlogSpec struct { // Trillian service configuration //+kubebuilder:default:={port: 8091} Trillian TrillianService `json:"trillian,omitempty"` + // Reference to TLS server certificate, private key and CA certificate + //+optional + TLSCertificate TLSCert `json:"tls"` } // CTlogStatus defines the observed state of CTlog component @@ -51,6 +54,7 @@ type CTlogStatus struct { PrivateKeyPasswordRef *SecretKeySelector `json:"privateKeyPasswordRef,omitempty"` PublicKeyRef *SecretKeySelector `json:"publicKeyRef,omitempty"` RootCertificates []SecretKeySelector `json:"rootCertificates,omitempty"` + TLSCertificate *TLSCert `json:"tls,omitempty"` // The ID of a Trillian tree that stores the log data. TreeID *int64 `json:"treeID,omitempty"` // +listType=map diff --git a/api/v1alpha1/ctlog_types_test.go b/api/v1alpha1/ctlog_types_test.go index 3026af7d8..55ad7618b 100644 --- a/api/v1alpha1/ctlog_types_test.go +++ b/api/v1alpha1/ctlog_types_test.go @@ -136,6 +136,16 @@ var _ = Describe("CTlog", func() { Address: "trillian-system.default.svc", Port: &port, }, + TLSCertificate: TLSCert{ + CertRef: &SecretKeySelector{ + Key: "cert", + LocalObjectReference: LocalObjectReference{Name: "secret"}, + }, + PrivateKeyRef: &SecretKeySelector{ + Key: "key", + LocalObjectReference: LocalObjectReference{Name: "secret"}, + }, + }, }, } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6a275ba62..4350e7121 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -134,6 +134,7 @@ func (in *CTlogSpec) DeepCopyInto(out *CTlogSpec) { } out.Monitoring = in.Monitoring in.Trillian.DeepCopyInto(&out.Trillian) + in.TLSCertificate.DeepCopyInto(&out.TLSCertificate) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CTlogSpec. @@ -174,6 +175,11 @@ func (in *CTlogStatus) DeepCopyInto(out *CTlogStatus) { *out = make([]SecretKeySelector, len(*in)) copy(*out, *in) } + if in.TLSCertificate != nil { + in, out := &in.TLSCertificate, &out.TLSCertificate + *out = new(TLSCert) + (*in).DeepCopyInto(*out) + } if in.TreeID != nil { in, out := &in.TreeID, &out.TreeID *out = new(int64) diff --git a/bundle/manifests/rhtas-operator.clusterserviceversion.yaml b/bundle/manifests/rhtas-operator.clusterserviceversion.yaml index 610bc37e5..b2f3b1c0c 100644 --- a/bundle/manifests/rhtas-operator.clusterserviceversion.yaml +++ b/bundle/manifests/rhtas-operator.clusterserviceversion.yaml @@ -92,8 +92,8 @@ metadata: "OIDCIssuers": [ { "ClientID": "trusted-artifact-signer", - "Issuer": "https://your-oidc-issuer-url", - "IssuerURL": "https://your-oidc-issuer-url", + "Issuer": "https://keycloak-keycloak-system.apps.rosa.iduhn-ah6m6-dk9.o468.p3.openshiftapps.com/auth/realms/trusted-artifact-signer", + "IssuerURL": "https://keycloak-keycloak-system.apps.rosa.iduhn-ah6m6-dk9.o468.p3.openshiftapps.com/auth/realms/trusted-artifact-signer", "Type": "email" } ] @@ -192,7 +192,7 @@ metadata: ] capabilities: Seamless Upgrades containerImage: registry.redhat.io/rhtas/rhtas-rhel9-operator@sha256:a21f7128694a64989bf0d84a7a7da4c1ffc89edf62d594dc8bea7bcfe9ac08d3 - createdAt: "2024-07-09T08:45:46Z" + createdAt: "2024-08-08T10:01:11Z" features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" features.operators.openshift.io/csi: "false" diff --git a/bundle/manifests/rhtas.redhat.com_ctlogs.yaml b/bundle/manifests/rhtas.redhat.com_ctlogs.yaml index 1638b1241..0d008806f 100644 --- a/bundle/manifests/rhtas.redhat.com_ctlogs.yaml +++ b/bundle/manifests/rhtas.redhat.com_ctlogs.yaml @@ -137,6 +137,62 @@ spec: type: object x-kubernetes-map-type: atomic type: array + tls: + description: Reference to TLS server certificate, private key and + CA certificate + properties: + caCertRef: + description: Reference to CA certificate + 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 + certRef: + description: Reference to service certificate + 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 + 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.certRef) || has(self.privateKeyRef)) treeID: description: |- The ID of a Trillian tree that stores the log data. @@ -328,6 +384,61 @@ spec: - name type: object x-kubernetes-map-type: atomic + tls: + description: TLSCert defines fields for TLS certificate + properties: + caCertRef: + description: Reference to CA certificate + 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 + certRef: + description: Reference to service certificate + 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 + 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.certRef) || has(self.privateKeyRef)) treeID: description: The ID of a Trillian tree that stores the log data. format: int64 diff --git a/bundle/manifests/rhtas.redhat.com_fulcios.yaml b/bundle/manifests/rhtas.redhat.com_fulcios.yaml index f7d448581..a52eedb3a 100644 --- a/bundle/manifests/rhtas.redhat.com_fulcios.yaml +++ b/bundle/manifests/rhtas.redhat.com_fulcios.yaml @@ -230,11 +230,11 @@ spec: description: Address to Ctlog Log Server End point type: string port: - default: 80 + default: 0 description: Port of Ctlog Log Server End point format: int32 maximum: 65535 - minimum: 1 + minimum: 0 type: integer type: object externalAccess: diff --git a/bundle/manifests/rhtas.redhat.com_securesigns.yaml b/bundle/manifests/rhtas.redhat.com_securesigns.yaml index 8d50e0a3b..dd08bf1c4 100644 --- a/bundle/manifests/rhtas.redhat.com_securesigns.yaml +++ b/bundle/manifests/rhtas.redhat.com_securesigns.yaml @@ -153,6 +153,62 @@ spec: type: object x-kubernetes-map-type: atomic type: array + tls: + description: Reference to TLS server certificate, private key + and CA certificate + properties: + caCertRef: + description: Reference to CA certificate + 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 + certRef: + description: Reference to service certificate + 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 + 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.certRef) || has(self.privateKeyRef)) treeID: description: |- The ID of a Trillian tree that stores the log data. @@ -367,11 +423,11 @@ spec: description: Address to Ctlog Log Server End point type: string port: - default: 80 + default: 0 description: Port of Ctlog Log Server End point format: int32 maximum: 65535 - minimum: 1 + minimum: 0 type: integer type: object externalAccess: @@ -844,67 +900,6 @@ spec: required: - tls type: object - signer: - properties: - tls: - description: Secret with TLS server certificate, private key - and CA certificate - properties: - caCertRef: - description: Reference to CA certificate - 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 - certRef: - description: Reference to service certificate - 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 - 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.certRef) || has(self.privateKeyRef)) - required: - - tls - type: object type: object tuf: default: diff --git a/bundle/manifests/rhtas.redhat.com_trillians.yaml b/bundle/manifests/rhtas.redhat.com_trillians.yaml index d016b46a2..1761bd84e 100644 --- a/bundle/manifests/rhtas.redhat.com_trillians.yaml +++ b/bundle/manifests/rhtas.redhat.com_trillians.yaml @@ -195,67 +195,6 @@ spec: required: - tls type: object - signer: - properties: - tls: - description: Secret with TLS server certificate, private key and - CA certificate - properties: - caCertRef: - description: Reference to CA certificate - 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 - certRef: - description: Reference to service certificate - 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 - 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.certRef) || has(self.privateKeyRef)) - required: - - tls - type: object type: object status: description: TrillianStatus defines the observed state of Trillian diff --git a/config/crd/bases/rhtas.redhat.com_ctlogs.yaml b/config/crd/bases/rhtas.redhat.com_ctlogs.yaml index d3263662a..6efa6abd7 100644 --- a/config/crd/bases/rhtas.redhat.com_ctlogs.yaml +++ b/config/crd/bases/rhtas.redhat.com_ctlogs.yaml @@ -137,6 +137,62 @@ spec: type: object x-kubernetes-map-type: atomic type: array + tls: + description: Reference to TLS server certificate, private key and + CA certificate + properties: + caCertRef: + description: Reference to CA certificate + 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 + certRef: + description: Reference to service certificate + 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 + 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.certRef) || has(self.privateKeyRef)) treeID: description: |- The ID of a Trillian tree that stores the log data. @@ -328,6 +384,61 @@ spec: - name type: object x-kubernetes-map-type: atomic + tls: + description: TLSCert defines fields for TLS certificate + properties: + caCertRef: + description: Reference to CA certificate + 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 + certRef: + description: Reference to service certificate + 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 + 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.certRef) || has(self.privateKeyRef)) treeID: description: The ID of a Trillian tree that stores the log data. format: int64 diff --git a/config/crd/bases/rhtas.redhat.com_fulcios.yaml b/config/crd/bases/rhtas.redhat.com_fulcios.yaml index d31cbbc5f..902aea72e 100644 --- a/config/crd/bases/rhtas.redhat.com_fulcios.yaml +++ b/config/crd/bases/rhtas.redhat.com_fulcios.yaml @@ -230,11 +230,11 @@ spec: description: Address to Ctlog Log Server End point type: string port: - default: 80 + default: 0 description: Port of Ctlog Log Server End point format: int32 maximum: 65535 - minimum: 1 + minimum: 0 type: integer type: object externalAccess: diff --git a/config/crd/bases/rhtas.redhat.com_securesigns.yaml b/config/crd/bases/rhtas.redhat.com_securesigns.yaml index 7f8e7e162..7c824efb1 100644 --- a/config/crd/bases/rhtas.redhat.com_securesigns.yaml +++ b/config/crd/bases/rhtas.redhat.com_securesigns.yaml @@ -153,6 +153,62 @@ spec: type: object x-kubernetes-map-type: atomic type: array + tls: + description: Reference to TLS server certificate, private key + and CA certificate + properties: + caCertRef: + description: Reference to CA certificate + 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 + certRef: + description: Reference to service certificate + 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 + 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.certRef) || has(self.privateKeyRef)) treeID: description: |- The ID of a Trillian tree that stores the log data. @@ -367,11 +423,11 @@ spec: description: Address to Ctlog Log Server End point type: string port: - default: 80 + default: 0 description: Port of Ctlog Log Server End point format: int32 maximum: 65535 - minimum: 1 + minimum: 0 type: integer type: object externalAccess: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 089212d22..7598478b5 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -3,6 +3,6 @@ resources: apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: -- name: controller - newName: quay.io/fghanmi/my_operator - newTag: v3.8.0 +- digest: sha256:a21f7128694a64989bf0d84a7a7da4c1ffc89edf62d594dc8bea7bcfe9ac08d3 + name: controller + newName: registry.redhat.io/rhtas/rhtas-rhel9-operator diff --git a/config/samples/rhtas_v1alpha1_securesign.yaml b/config/samples/rhtas_v1alpha1_securesign.yaml index 7ca1001c1..09259ebbf 100644 --- a/config/samples/rhtas_v1alpha1_securesign.yaml +++ b/config/samples/rhtas_v1alpha1_securesign.yaml @@ -23,8 +23,8 @@ spec: config: OIDCIssuers: - ClientID: "trusted-artifact-signer" - IssuerURL: "https://keycloak-keycloak-system.apps.rosa.av42p-79zot-u82.x8pi.p3.openshiftapps.com/auth/realms/trusted-artifact-signer" - Issuer: "https://keycloak-keycloak-system.apps.rosa.av42p-79zot-u82.x8pi.p3.openshiftapps.com/auth/realms/trusted-artifact-signer" + IssuerURL: "https://keycloak-keycloak-system.apps.rosa.iduhn-ah6m6-dk9.o468.p3.openshiftapps.com/auth/realms/trusted-artifact-signer" + Issuer: "https://keycloak-keycloak-system.apps.rosa.iduhn-ah6m6-dk9.o468.p3.openshiftapps.com/auth/realms/trusted-artifact-signer" Type: "email" certificate: organizationName: Red Hat 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/constants/images.go b/internal/controller/constants/images.go index 325f9d606..27142914b 100644 --- a/internal/controller/constants/images.go +++ b/internal/controller/constants/images.go @@ -12,16 +12,18 @@ var ( RekorRedisImage = "registry.redhat.io/rhtas/trillian-redis-rhel9@sha256:5f0630c7aa29eeee28668f7ad451f129c9fb2feb86ec21b6b1b0b5cc42b44f4a" // RekorServerImage = "registry.redhat.io/rhtas/rekor-server-rhel9@sha256:d4ea970447f3b4c18c309d2f0090a5d02260dd5257a0d41f87fefc4f014a9526" - RekorServerImage = "quay.io/securesign/rekor-server_test:latest" + RekorServerImage = "quay.io/securesign/rekor-server@sha256:885aa7f45c10bf2f6102f4e51981e54525f2600c10889ae1078c0eb0b29221fb" RekorSearchUiImage = "registry.redhat.io/rhtas/rekor-search-ui-rhel9@sha256:5eabf561c0549d81862e521ddc1f0ab91a3f2c9d99dcd83ab5a2cf648a95dd19" BackfillRedisImage = "registry.redhat.io/rhtas/rekor-backfill-redis-rhel9@sha256:5c7460ab3cd13b2ecf2b979f5061cb384174d6714b7630879e53d063e4cb69d2" TufImage = "registry.redhat.io/rhtas/tuf-server-rhel9@sha256:8c229e2c7f9d6cc0ebf4f23dd944373d497be2ed31960f0383b1bb43f16de0db" - CTLogImage = "registry.redhat.io/rhtas/certificate-transparency-rhel9@sha256:44906b1e52b0b5e324f23cae088837caf15444fd34679e6d2f3cc018d4e093fe" + // CTLogImage = "registry.redhat.io/rhtas/certificate-transparency-rhel9@sha256:44906b1e52b0b5e324f23cae088837caf15444fd34679e6d2f3cc018d4e093fe" + CTLogImage = "quay.io/securesign/certificate-transparency-go@sha256:7b746b96e534ec8d6cee0b3b12b36c0b313a547d0335cd78f0df4b939d12c09a" ClientServerImage = "registry.access.redhat.com/ubi9/httpd-24@sha256:7874b82335a80269dcf99e5983c2330876f5fe8bdc33dc6aa4374958a2ffaaee" ClientServerImage_cg = "registry.redhat.io/rhtas/client-server-cg-rhel9@sha256:046029a9a2028efa9dcbf8eff9b41fe5ac4e9ad64caf0241f5680a5cb36bf36b" ClientServerImage_re = "registry.redhat.io/rhtas/client-server-re-rhel9@sha256:7254f4c94182d21159162ea850e1ed14332fa5dee987103d54e7e4096a6fea31" SegmentBackupImage = "registry.redhat.io/rhtas/segment-reporting-rhel9@sha256:54be793ea9e2af996e5e5c6f9156ee02d5d915adb53b4ab028cb3030f64b1496" + CreateTreeImage = "quay.io/redhat-user-workloads/rhtas-tenant/trillian/createtree@sha256:d4d3a99e8da94a89312babe65545634fa91fdf14dd2a6f8da4be489a7ef52d90" ) diff --git a/internal/controller/ctlog/actions/config_map.go b/internal/controller/ctlog/actions/config_map.go new file mode 100644 index 000000000..354b76878 --- /dev/null +++ b/internal/controller/ctlog/actions/config_map.go @@ -0,0 +1,79 @@ +package actions + +import ( + "context" + "fmt" + + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/constants" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func NewCAConfigMapAction() action.Action[*rhtasv1alpha1.CTlog] { + return &configMapAction{} +} + +type configMapAction struct { + action.BaseAction +} + +func (i configMapAction) Name() string { + return "create CA configMap" +} + +func (i configMapAction) CanHandle(ctx context.Context, instance *rhtasv1alpha1.CTlog) bool { + c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) + cm, _ := k8sutils.GetConfigMap(ctx, i.Client, instance.Namespace, "ca-configmap") + return (c.Reason == constants.Creating || c.Reason == constants.Ready) && cm == nil && instance.Spec.TLSCertificate.CACertRef == nil +} + +func (i configMapAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) *action.Result { + var ( + err error + updated bool + ) + + labels := constants.LabelsFor(ComponentName, DeploymentName, instance.Name) + + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-configmap", + Namespace: instance.Namespace, + Labels: labels, + }, + Data: map[string]string{}, + } + + if err = controllerutil.SetControllerReference(instance, configMap, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for configMap: %w", err)) + } + if updated, err = i.Ensure(ctx, configMap); err != nil { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create configMap: %w", err), instance) + } + + //TLS: Annotate configMap + configMap.Annotations = map[string]string{"service.beta.openshift.io/inject-cabundle": "true"} + err = i.Client.Update(ctx, configMap) + if err != nil { + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not annotate configMap: %w", err), instance) + } + + if updated { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: constants.Ready, + Status: metav1.ConditionFalse, Reason: constants.Creating, Message: "ConfigMap created"}) + return i.StatusUpdate(ctx, instance) + } else { + return i.Continue() + } +} diff --git a/internal/controller/ctlog/actions/constants.go b/internal/controller/ctlog/actions/constants.go index ede9d218f..077501f7b 100644 --- a/internal/controller/ctlog/actions/constants.go +++ b/internal/controller/ctlog/actions/constants.go @@ -6,11 +6,13 @@ const ( RBACName = "ctlog" MonitoringRoleName = "prometheus-k8s-ctlog" - CertCondition = "FulcioCertAvailable" - ServerPortName = "http" - ServerPort = 80 - ServerTargetPort = 6962 - MetricsPortName = "metrics" - MetricsPort = 6963 - ServerCondition = "ServerAvailable" + CertCondition = "FulcioCertAvailable" + ServerPortName = "http" + ServerPort = 80 + HttpsServerPortName = "https" + HttpsServerPort = 443 + ServerTargetPort = 6962 + MetricsPortName = "metrics" + MetricsPort = 6963 + ServerCondition = "ServerAvailable" ) diff --git a/internal/controller/ctlog/actions/create_tree_job.go b/internal/controller/ctlog/actions/create_tree_job.go new file mode 100644 index 000000000..ce9059f01 --- /dev/null +++ b/internal/controller/ctlog/actions/create_tree_job.go @@ -0,0 +1,191 @@ +package actions + +import ( + "context" + "fmt" + + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + cutils "github.com/securesign/operator/internal/controller/common/utils" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/constants" + "github.com/securesign/operator/internal/controller/ctlog/utils" + actions2 "github.com/securesign/operator/internal/controller/trillian/actions" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func NewCreateTreeJobAction() action.Action[*rhtasv1alpha1.CTlog] { + return &createTreeJobAction{} +} + +type createTreeJobAction struct { + action.BaseAction +} + +func (i createTreeJobAction) Name() string { + return "create tree job" +} + +func (i createTreeJobAction) CanHandle(ctx context.Context, instance *rhtasv1alpha1.CTlog) bool { + cm, _ := kubernetes.GetConfigMap(ctx, i.Client, instance.Namespace, "ctlog-tree-id-config") + c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) + return (c.Reason == constants.Creating || c.Reason == constants.Ready) && cm == nil && instance.Status.TreeID == nil +} + +func (i createTreeJobAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) *action.Result { + var ( + err error + updated bool + ) + + CtlogTreeJobName := "ctlog-create-tree" + configMapName := "ctlog-tree-id-config" + 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) + + if c := meta.FindStatusCondition(instance.Status.Conditions, CtlogTreeJobName); c == nil { + instance.SetCondition(metav1.Condition{ + Type: CtlogTreeJobName, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: "Creating ctlog tree Job", + }) + } + + labels := constants.LabelsFor(ComponentName, ComponentName, instance.Name) + + // Needed for configMap clean-up + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: instance.Namespace, + Labels: labels, + }, + Data: map[string]string{}, + } + if err = controllerutil.SetControllerReference(instance, configMap, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for configMap: %w", err)) + } + if updated, err = i.Ensure(ctx, configMap); err != nil { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + } + if updated { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: constants.Ready, + Status: metav1.ConditionFalse, Reason: constants.Creating, Message: "ConfigMap created"}) + } + + parallelism := int32(1) + completions := int32(1) + activeDeadlineSeconds := int64(600) + backoffLimit := int32(5) + + signingKeySecret, _ := kubernetes.GetSecret(i.Client, "openshift-service-ca", "signing-key") + trustedCAAnnotation := cutils.TrustedCAAnnotationToReference(instance.Annotations) + cmd := "" + switch { + case trustedCAAnnotation != nil: + cmd = fmt.Sprintf("./createtree --admin_server=%s --display_name=ctlog-tree --tls_cert_file=/var/run/configs/tas/ca-trust/ca-bundle.crt", trillUrl) + case signingKeySecret != nil: + cmd = fmt.Sprintf("./createtree --admin_server=%s --display_name=ctlog-tree --tls_cert_file=/etc/ssl/certs/tls.crt", trillUrl) + default: + cmd = fmt.Sprintf("./createtree --admin_server=%s --display_name=ctlog-tree", trillUrl) + } + command := []string{ + "/bin/sh", + "-c", + fmt.Sprintf(` + TREE_ID=$(%s) + if [ $? -eq 0 ]; then + echo "TREE_ID=$TREE_ID" + # Read the service account token + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + # Read the namespace + NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + # OpenShift API server URL + API_SERVER=https://openshift.default.svc + # Create or update the ConfigMap + curl -k -X PATCH $API_SERVER/api/v1/namespaces/$NAMESPACE/configmaps/"%s" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/merge-patch+json" \ + -d '{ + "data": { + "tree_id": "'$TREE_ID'" + } + }' + if [ $? -ne 0 ]; then + echo "Failed to update ConfigMap" >&2 + exit 1 + fi + else + echo "Failed to create tree" >&2 + exit 1 + fi + `, cmd, configMapName), + } + env := []corev1.EnvVar{} + + job := kubernetes.CreateJob(instance.Namespace, CtlogTreeJobName, labels, constants.CreateTreeImage, RBACName, parallelism, completions, activeDeadlineSeconds, backoffLimit, command, env) + if err = ctrl.SetControllerReference(instance, job, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for Job: %w", err)) + } + + if trustedCAAnnotation != nil { + err = cutils.SetTrustedCA(&job.Spec.Template, cutils.TrustedCAAnnotationToReference(instance.Annotations)) + if err != nil { + return i.Failed(err) + } + } + + if signingKeySecret != nil && trustedCAAnnotation == nil { + job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: "tls-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: instance.Name + "-trillian-log-server-tls-secret", + }, + }, + }) + job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: "tls-cert", + MountPath: "/etc/ssl/certs", + ReadOnly: true, + }) + } + + _, err = i.Ensure(ctx, job) + if err != nil { + return i.Failed(fmt.Errorf("failed to Ensure the job: %w", err)) + } + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: CtlogTreeJobName, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + Message: "ctlog tree Job Created", + }) + + return i.Continue() +} diff --git a/internal/controller/ctlog/actions/deployment.go b/internal/controller/ctlog/actions/deployment.go index ad6462398..2b3fc56a1 100644 --- a/internal/controller/ctlog/actions/deployment.go +++ b/internal/controller/ctlog/actions/deployment.go @@ -8,9 +8,11 @@ import ( rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/action" + k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/constants" "github.com/securesign/operator/internal/controller/ctlog/utils" trillian "github.com/securesign/operator/internal/controller/trillian/actions" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -41,6 +43,7 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) labels := constants.LabelsFor(ComponentName, DeploymentName, instance.Name) + signingKeySecret, _ := k8sutils.GetSecret(i.Client, "openshift-service-ca", "signing-key") switch { case instance.Spec.Trillian.Address == "": instance.Spec.Trillian.Address = fmt.Sprintf("%s.%s.svc", trillian.LogserverDeploymentName, instance.Namespace) @@ -61,6 +64,105 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) return i.Failed(err) } + // TLS certificate + if instance.Spec.TLSCertificate.CertRef != nil && instance.Spec.TLSCertificate.CACertRef != nil { + dp.Spec.Template.Spec.Volumes = append(dp.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: "tls-cert", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: instance.Spec.TLSCertificate.CertRef.Name, + }, + Items: []corev1.KeyToPath{ + { + Key: instance.Spec.TLSCertificate.CertRef.Key, + Path: "tls.crt", + }, + }, + }, + }, + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: instance.Spec.TLSCertificate.PrivateKeyRef.Name, + }, + Items: []corev1.KeyToPath{ + { + Key: instance.Spec.TLSCertificate.PrivateKeyRef.Key, + Path: "tls.key", + }, + }, + }, + }, + { + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: instance.Spec.TLSCertificate.CACertRef.Name, + }, + Items: []corev1.KeyToPath{ + { + Key: "ca.crt", // User should use this key. + Path: "ca.crt", + }, + }, + }, + }, + }, + }, + }, + }) + } else if signingKeySecret != nil { + i.Logger.V(1).Info("TLS: Using secrets/signing-key secret") + dp.Spec.Template.Spec.Volumes = append(dp.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: "tls-cert", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: instance.Name + "-ctlog-tls-secret", + }, + }, + }, + { + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "ca-configmap", + }, + Items: []corev1.KeyToPath{ + { + Key: "service-ca.crt", + Path: "ca.crt", + }, + }, + }, + }, + }, + }, + }, + }) + } else { + i.Logger.V(1).Info("Communication between services is insecure") + } + + if instance.Spec.TLSCertificate.CertRef != nil && instance.Spec.TLSCertificate.CACertRef != nil || signingKeySecret != nil { + dp.Spec.Template.Spec.Containers[0].VolumeMounts = append(dp.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: "tls-cert", + MountPath: "/etc/ssl/certs", + ReadOnly: true, + }) + // dp.Spec.Template.Spec.Containers[0].Args = append(dp.Spec.Template.Spec.Containers[0].Args, "--tls_certificate", "/etc/ssl/certs/tls.crt") + // dp.Spec.Template.Spec.Containers[0].Args = append(dp.Spec.Template.Spec.Containers[0].Args, "--tls_key", "/etc/ssl/certs/tls.key") + dp.Spec.Template.Spec.Containers[0].Args = append(dp.Spec.Template.Spec.Containers[0].Args, "--trillian_tls_ca_cert_file", "/etc/ssl/certs/ca.crt") + } + if err = controllerutil.SetControllerReference(instance, dp, i.Client.Scheme()); err != nil { return i.Failed(fmt.Errorf("could not set controller reference for Deployment: %w", err)) } diff --git a/internal/controller/ctlog/actions/rbac.go b/internal/controller/ctlog/actions/rbac.go index b5d0028e3..cb097b96d 100644 --- a/internal/controller/ctlog/actions/rbac.go +++ b/internal/controller/ctlog/actions/rbac.go @@ -64,7 +64,7 @@ func (i rbacAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) * { APIGroups: []string{""}, Resources: []string{"configmaps"}, - Verbs: []string{"create", "get", "update"}, + Verbs: []string{"create", "get", "update", "patch"}, }, { APIGroups: []string{""}, diff --git a/internal/controller/ctlog/actions/resolve_tree.go b/internal/controller/ctlog/actions/resolve_tree.go index 0c885551a..678abc6ba 100644 --- a/internal/controller/ctlog/actions/resolve_tree.go +++ b/internal/controller/ctlog/actions/resolve_tree.go @@ -3,25 +3,23 @@ package actions import ( "context" "fmt" + "strconv" + "time" "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" + "k8s.io/apimachinery/pkg/types" ) -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, + timeout: time.Duration(constants.CreateTreeDeadline) * time.Second, } for _, opt := range opts { @@ -32,7 +30,7 @@ func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtas type resolveTreeAction struct { action.BaseAction - createTree createTree + timeout time.Duration } func (i resolveTreeAction) Name() string { @@ -62,23 +60,58 @@ func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.C } 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) + cm := &v1.ConfigMap{} + deadline := time.Now().Add(i.timeout) + for time.Now().Before(deadline) { + err = i.Client.Get(ctx, types.NamespacedName{Name: "ctlog-tree-id-config", Namespace: instance.Namespace}, cm) + if err == nil && cm.Data != nil { + break + } + if err != nil { + i.Logger.V(1).Error(fmt.Errorf("waiting for the ConfigMap"), err.Error()) + } + time.Sleep(2 * time.Second) } + if err != nil { + i.Logger.V(1).Error(fmt.Errorf("timed out waiting for the ConfigMap"), err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: fmt.Sprintf("timed out waiting for the ConfigMap: %v", err), + }) + return i.Failed(fmt.Errorf("timed out waiting for the ConfigMap: %s", "configmap not found")) + } + + if cm.Data == nil { + err = fmt.Errorf("ConfigMap data is empty") + i.Logger.V(1).Error(err, err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) 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 { + treeId, exists := cm.Data["tree_id"] + treeIdInt, err := strconv.ParseInt(treeId, 10, 64) + tree = &trillian.Tree{TreeId: treeIdInt} + if !exists { + i.Logger.V(1).Error(err, err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.Failed(err) + } + + if !exists { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: ServerCondition, Status: metav1.ConditionFalse, diff --git a/internal/controller/ctlog/actions/resolve_tree_test.go b/internal/controller/ctlog/actions/resolve_tree_test.go index 9b0bd684b..106587c63 100644 --- a/internal/controller/ctlog/actions/resolve_tree_test.go +++ b/internal/controller/ctlog/actions/resolve_tree_test.go @@ -2,20 +2,18 @@ package actions import ( "context" - "errors" "fmt" "reflect" "testing" + "time" + v1 "k8s.io/api/core/v1" "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" @@ -115,7 +113,7 @@ func TestResolveTree_Handle(t *testing.T) { type env struct { spec rhtasv1alpha1.CTlogSpec statusTreeId *int64 - createTree createTree + configMap *v1.ConfigMap } type want struct { result *action.Result @@ -133,7 +131,15 @@ func TestResolveTree_Handle(t *testing.T) { TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ctlog-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{ + "tree_id": "5555555", + }, + }, }, want: want{ result: testAction.StatusUpdate(), @@ -146,7 +152,7 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "update tree", + name: "update tree from spec", env: env{ spec: rhtasv1alpha1.CTlogSpec{ TreeID: ptr.To(int64(123456)), @@ -184,16 +190,22 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "unable to create a new tree", + name: "ConfigMap data is empty", env: env{ spec: rhtasv1alpha1.CTlogSpec{ TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(nil, errors.New("timeout error"), nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ctlog-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{}, + }, }, want: want{ - result: testAction.FailedWithStatusUpdate(fmt.Errorf("could not create trillian tree: timeout error")), + result: testAction.Failed(fmt.Errorf("ConfigMap data is empty")), verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { g.Expect(ctlog.Spec.TreeID).Should(BeNil()) g.Expect(ctlog.Status.TreeID).Should(BeNil()) @@ -201,43 +213,20 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "resolve trillian address", + name: "ConfigMap not found", 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}, + result: testAction.Failed(fmt.Errorf("timed out waiting for the ConfigMap: configmap not found")), + verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { + g.Expect(ctlog.Status.Conditions).To(ContainElement( + WithTransform(func(c metav1.Condition) string { return c.Message }, ContainSubstring("timed out waiting for the ConfigMap:")), + )) }, }, - want: want{ - result: testAction.Failed(fmt.Errorf("resolve treeID: %v", utils.TrillianPortNotSpecified)), - }, }, } for _, tt := range tests { @@ -265,12 +254,12 @@ func TestResolveTree_Handle(t *testing.T) { 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 tt.env.configMap != nil { + c.Create(ctx, tt.env.configMap) + } + + a := testAction.PrepareAction(c, NewResolveTreeAction(func(a *resolveTreeAction) { + a.timeout = 5 * time.Second // Reduced timeout for testing })) if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { @@ -282,12 +271,3 @@ func TestResolveTree_Handle(t *testing.T) { }) } } - -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/ctlog/actions/service.go b/internal/controller/ctlog/actions/service.go index b1f35e895..5ddc8c7c9 100644 --- a/internal/controller/ctlog/actions/service.go +++ b/internal/controller/ctlog/actions/service.go @@ -7,6 +7,7 @@ import ( rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + k8sutils "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/constants" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -40,7 +41,19 @@ func (i serviceAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog labels := constants.LabelsFor(ComponentName, ComponentName, instance.Name) - svc := kubernetes.CreateService(instance.Namespace, ComponentName, ServerPortName, ServerPort, ServerTargetPort, labels) + signingKeySecret, _ := k8sutils.GetSecret(i.Client, "openshift-service-ca", "signing-key") + var port int + var portName string + if instance.Spec.TLSCertificate.CertRef != nil || signingKeySecret != nil { + // port = HttpsServerPort // TODO + // portName = HttpsServerPortName + port = ServerPort + portName = ServerPortName + } else { + port = ServerPort + portName = ServerPortName + } + svc := kubernetes.CreateService(instance.Namespace, ComponentName, portName, port, ServerTargetPort, labels) if instance.Spec.Monitoring.Enabled { svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ Name: MetricsPortName, @@ -62,6 +75,18 @@ func (i serviceAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create service: %w", err), instance) } + //TLS: Annotate service + if signingKeySecret != nil && instance.Spec.TLSCertificate.CertRef == nil { + if svc.Annotations == nil { + svc.Annotations = make(map[string]string) + } + svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = instance.Name + "-ctlog-tls-secret" + err := i.Client.Update(ctx, svc) + if err != nil { + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not annotate service: %w", err), instance) + } + } + if updated { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: constants.Ready, Status: metav1.ConditionFalse, Reason: constants.Creating, Message: "Service created"}) diff --git a/internal/controller/ctlog/ctlog_controller.go b/internal/controller/ctlog/ctlog_controller.go index 4e4fa80ec..e58d80312 100644 --- a/internal/controller/ctlog/ctlog_controller.go +++ b/internal/controller/ctlog/ctlog_controller.go @@ -96,12 +96,14 @@ func (r *CTlogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl transitions.NewToCreatePhaseAction[*rhtasv1alpha1.CTlog](), + actions.NewRBACAction(), + actions.NewCAConfigMapAction(), actions.NewHandleFulcioCertAction(), actions.NewHandleKeysAction(), + actions.NewCreateTreeJobAction(), actions.NewResolveTreeAction(), actions.NewServerConfigAction(), - actions.NewRBACAction(), actions.NewDeployAction(), actions.NewServiceAction(), actions.NewCreateMonitorAction(), @@ -158,6 +160,7 @@ func (r *CTlogReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&rhtasv1alpha1.CTlog{}). Owns(&v1.Deployment{}). Owns(&v12.Service{}). + Owns(&v12.ConfigMap{}). WatchesMetadata(partialSecret, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { val, ok := object.GetLabels()["app.kubernetes.io/instance"] if ok { diff --git a/internal/controller/ctlog/ctlog_controller_test.go b/internal/controller/ctlog/ctlog_controller_test.go index 6ba599d75..7f5d4d966 100644 --- a/internal/controller/ctlog/ctlog_controller_test.go +++ b/internal/controller/ctlog/ctlog_controller_test.go @@ -95,6 +95,17 @@ var _ = Describe("CTlog controller", func() { Spec: v1alpha1.CTlogSpec{ TreeID: &ptr, + TLSCertificate: v1alpha1.TLSCert{ + CertRef: &v1alpha1.SecretKeySelector{ + Key: "cert", + LocalObjectReference: v1alpha1.LocalObjectReference{Name: "secret-crt"}, + }, + PrivateKeyRef: &v1alpha1.SecretKeySelector{ + Key: "key", + LocalObjectReference: v1alpha1.LocalObjectReference{Name: "secret-key"}, + }, + CACertRef: &v1alpha1.LocalObjectReference{Name: "ca-configmap"}, + }, }, } err = k8sClient.Create(ctx, instance) @@ -164,6 +175,7 @@ var _ = Describe("CTlog controller", func() { return k8sClient.Get(ctx, types.NamespacedName{Name: actions.ComponentName, Namespace: Namespace}, service) }).Should(Succeed()) Expect(service.Spec.Ports[0].Port).Should(Equal(int32(80))) + // Expect(service.Spec.Ports[0].Port).Should(Equal(int32(443))) // TODO By("Move to Ready phase") // Workaround to succeed condition for Ready phase diff --git a/internal/controller/rekor/actions/rbac.go b/internal/controller/rekor/actions/rbac.go index 22144f21e..daba4e9dd 100644 --- a/internal/controller/rekor/actions/rbac.go +++ b/internal/controller/rekor/actions/rbac.go @@ -64,7 +64,7 @@ func (i rbacAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) * { APIGroups: []string{""}, Resources: []string{"configmaps"}, - Verbs: []string{"create", "get", "update"}, + Verbs: []string{"create", "get", "update", "patch"}, }, { APIGroups: []string{""}, diff --git a/internal/controller/rekor/actions/server/create_tree_job.go b/internal/controller/rekor/actions/server/create_tree_job.go new file mode 100644 index 000000000..8488aa747 --- /dev/null +++ b/internal/controller/rekor/actions/server/create_tree_job.go @@ -0,0 +1,192 @@ +package server + +import ( + "context" + "fmt" + + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/action" + cutils "github.com/securesign/operator/internal/controller/common/utils" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "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" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func NewCreateTreeJobAction() action.Action[*rhtasv1alpha1.Rekor] { + return &createTreeJobAction{} +} + +type createTreeJobAction struct { + action.BaseAction +} + +func (i createTreeJobAction) Name() string { + return "create tree job" +} + +func (i createTreeJobAction) CanHandle(ctx context.Context, instance *rhtasv1alpha1.Rekor) bool { + cm, _ := kubernetes.GetConfigMap(ctx, i.Client, instance.Namespace, "rekor-tree-id-config") + c := meta.FindStatusCondition(instance.Status.Conditions, constants.Ready) + return (c.Reason == constants.Creating || c.Reason == constants.Ready) && cm == nil && instance.Status.TreeID == nil +} + +func (i createTreeJobAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) *action.Result { + var ( + err error + updated bool + ) + + RekorTreeJobName := "rekor-create-tree" + configMapName := "rekor-tree-id-config" + 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) + + if c := meta.FindStatusCondition(instance.Status.Conditions, RekorTreeJobName); c == nil { + instance.SetCondition(metav1.Condition{ + Type: RekorTreeJobName, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: "Creating rekor tree Job", + }) + } + + labels := constants.LabelsFor(actions.ServerComponentName, actions.ServerDeploymentName, instance.Name) + + // Needed for configMap clean-up + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: instance.Namespace, + Labels: labels, + }, + Data: map[string]string{}, + } + if err = controllerutil.SetControllerReference(instance, configMap, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for configMap: %w", err)) + } + if updated, err = i.Ensure(ctx, configMap); err != nil { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + } + if updated { + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{Type: constants.Ready, + Status: metav1.ConditionFalse, Reason: constants.Creating, Message: "ConfigMap created"}) + } + + parallelism := int32(1) + completions := int32(1) + activeDeadlineSeconds := int64(600) + backoffLimit := int32(5) + + signingKeySecret, _ := kubernetes.GetSecret(i.Client, "openshift-service-ca", "signing-key") + trustedCAAnnotation := cutils.TrustedCAAnnotationToReference(instance.Annotations) + cmd := "" + switch { + case trustedCAAnnotation != nil: + cmd = fmt.Sprintf("./createtree --admin_server=%s --display_name=rekor-tree --tls_cert_file=/var/run/configs/tas/ca-trust/ca-bundle.crt", trillUrl) + case signingKeySecret != nil: + cmd = fmt.Sprintf("./createtree --admin_server=%s --display_name=rekor-tree --tls_cert_file=/etc/ssl/certs/tls.crt", trillUrl) + default: + cmd = fmt.Sprintf("./createtree --admin_server=%s --display_name=rekor-tree", trillUrl) + } + command := []string{ + "/bin/sh", + "-c", + fmt.Sprintf(` + TREE_ID=$(%s) + if [ $? -eq 0 ]; then + echo "TREE_ID=$TREE_ID" + # Read the service account token + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + # Read the namespace + NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + # OpenShift API server URL + API_SERVER=https://openshift.default.svc + # Create or update the ConfigMap + curl -k -X PATCH $API_SERVER/api/v1/namespaces/$NAMESPACE/configmaps/"%s" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/merge-patch+json" \ + -d '{ + "data": { + "tree_id": "'$TREE_ID'" + } + }' + if [ $? -ne 0 ]; then + echo "Failed to update ConfigMap" >&2 + exit 1 + fi + else + echo "Failed to create tree" >&2 + exit 1 + fi + `, cmd, configMapName), + } + env := []corev1.EnvVar{} + + job := kubernetes.CreateJob(instance.Namespace, RekorTreeJobName, labels, constants.CreateTreeImage, actions.RBACName, parallelism, completions, activeDeadlineSeconds, backoffLimit, command, env) + if err = ctrl.SetControllerReference(instance, job, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for Job: %w", err)) + } + + if trustedCAAnnotation != nil { + err = cutils.SetTrustedCA(&job.Spec.Template, cutils.TrustedCAAnnotationToReference(instance.Annotations)) + if err != nil { + return i.Failed(err) + } + } + + if signingKeySecret != nil && trustedCAAnnotation == nil { + job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: "tls-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: instance.Name + "-trillian-log-server-tls-secret", + }, + }, + }) + job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: "tls-cert", + MountPath: "/etc/ssl/certs", + ReadOnly: true, + }) + } + + _, err = i.Ensure(ctx, job) + if err != nil { + return i.Failed(fmt.Errorf("failed to Ensure the job: %w", err)) + } + + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: RekorTreeJobName, + Status: metav1.ConditionTrue, + Reason: constants.Ready, + Message: "rekor tree Job Created", + }) + + return i.Continue() +} diff --git a/internal/controller/rekor/actions/server/deployment.go b/internal/controller/rekor/actions/server/deployment.go index 6b871334b..8f28297f1 100644 --- a/internal/controller/rekor/actions/server/deployment.go +++ b/internal/controller/rekor/actions/server/deployment.go @@ -132,7 +132,7 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) MountPath: "/etc/ssl/certs", ReadOnly: true, }) - dp.Spec.Template.Spec.Containers[0].Args = append(dp.Spec.Template.Spec.Containers[0].Args, "--tls_ca_cert", "/etc/ssl/certs/ca.crt") + dp.Spec.Template.Spec.Containers[0].Args = append(dp.Spec.Template.Spec.Containers[0].Args, "--trillian_log_server.tls_ca_cert", "/etc/ssl/certs/ca.crt") } if err = controllerutil.SetControllerReference(instance, dp, i.Client.Scheme()); err != nil { diff --git a/internal/controller/rekor/actions/server/resolve_tree.go b/internal/controller/rekor/actions/server/resolve_tree.go index 122511b8c..7e6aa703d 100644 --- a/internal/controller/rekor/actions/server/resolve_tree.go +++ b/internal/controller/rekor/actions/server/resolve_tree.go @@ -3,26 +3,24 @@ package server import ( "context" "fmt" + "strconv" + "time" "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" + "k8s.io/apimachinery/pkg/types" ) -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, + timeout: time.Duration(constants.CreateTreeDeadline) * time.Second, } for _, opt := range opts { @@ -33,7 +31,7 @@ func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtas type resolveTreeAction struct { action.BaseAction - createTree createTree + timeout time.Duration } func (i resolveTreeAction) Name() string { @@ -63,23 +61,58 @@ func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.R } 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) + cm := &v1.ConfigMap{} + deadline := time.Now().Add(i.timeout) + for time.Now().Before(deadline) { + err = i.Client.Get(ctx, types.NamespacedName{Name: "rekor-tree-id-config", Namespace: instance.Namespace}, cm) + if err == nil && cm.Data != nil { + break + } + if err != nil { + i.Logger.V(1).Error(fmt.Errorf("waiting for the ConfigMap"), err.Error()) + } + time.Sleep(2 * time.Second) } + if err != nil { + i.Logger.V(1).Error(fmt.Errorf("timed out waiting for the ConfigMap"), err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: fmt.Sprintf("timed out waiting for the ConfigMap: %v", err), + }) + return i.Failed(fmt.Errorf("timed out waiting for the ConfigMap: %s", "configmap not found")) + } + + if cm.Data == nil { + err = fmt.Errorf("ConfigMap data is empty") + i.Logger.V(1).Error(err, err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) 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 { + treeId, exists := cm.Data["tree_id"] + treeIdInt, err := strconv.ParseInt(treeId, 10, 64) + tree = &trillian.Tree{TreeId: treeIdInt} + if !exists { + i.Logger.V(1).Error(err, err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.Failed(err) + } + + if !exists { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: actions.ServerCondition, Status: metav1.ConditionFalse, diff --git a/internal/controller/rekor/actions/server/resolve_tree_test.go b/internal/controller/rekor/actions/server/resolve_tree_test.go index 26917022e..9769e9e96 100644 --- a/internal/controller/rekor/actions/server/resolve_tree_test.go +++ b/internal/controller/rekor/actions/server/resolve_tree_test.go @@ -2,19 +2,17 @@ package server import ( "context" - "errors" "fmt" "reflect" "testing" + "time" - "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" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -114,7 +112,7 @@ func TestResolveTree_Handle(t *testing.T) { type env struct { spec rhtasv1alpha1.RekorSpec statusTreeId *int64 - createTree createTree + configMap *v1.ConfigMap } type want struct { result *action.Result @@ -132,7 +130,15 @@ func TestResolveTree_Handle(t *testing.T) { TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rekor-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{ + "tree_id": "5555555", + }, + }, }, want: want{ result: testAction.StatusUpdate(), @@ -145,7 +151,7 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "update tree", + name: "update tree from spec", env: env{ spec: rhtasv1alpha1.RekorSpec{ TreeID: ptr.To(int64(123456)), @@ -183,16 +189,22 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "unable to create a new tree", + name: "ConfigMap data is empty", env: env{ spec: rhtasv1alpha1.RekorSpec{ TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(nil, errors.New("timeout error"), nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rekor-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{}, + }, }, want: want{ - result: testAction.FailedWithStatusUpdate(fmt.Errorf("could not create trillian tree: timeout error")), + result: testAction.Failed(fmt.Errorf("ConfigMap data is empty")), verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { g.Expect(rekor.Spec.TreeID).Should(BeNil()) g.Expect(rekor.Status.TreeID).Should(BeNil()) @@ -200,43 +212,20 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "resolve trillian address", + name: "ConfigMap not found", 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"}, + 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("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}, + result: testAction.Failed(fmt.Errorf("timed out waiting for the ConfigMap: configmap not found")), + verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { + g.Expect(rekor.Status.Conditions).To(ContainElement( + WithTransform(func(c metav1.Condition) string { return c.Message }, ContainSubstring("timed out waiting for the ConfigMap:")), + )) }, }, - want: want{ - result: testAction.Failed(fmt.Errorf("resolve treeID: %v", utils.TrillianPortNotSpecified)), - }, }, } for _, tt := range tests { @@ -264,12 +253,12 @@ func TestResolveTree_Handle(t *testing.T) { 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 tt.env.configMap != nil { + c.Create(ctx, tt.env.configMap) + } + + a := testAction.PrepareAction(c, NewResolveTreeAction(func(a *resolveTreeAction) { + a.timeout = 5 * time.Second // Reduced timeout for testing })) if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { @@ -281,12 +270,3 @@ func TestResolveTree_Handle(t *testing.T) { }) } } - -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/rekor/rekor_controller.go b/internal/controller/rekor/rekor_controller.go index b75d0df2c..b52de8d6e 100644 --- a/internal/controller/rekor/rekor_controller.go +++ b/internal/controller/rekor/rekor_controller.go @@ -109,6 +109,7 @@ func (r *RekorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl actions2.NewRBACAction(), server.NewShardingConfigAction(), + server.NewCreateTreeJobAction(), server.NewResolveTreeAction(), server.NewCAConfigMapAction(), server.NewCreatePvcAction(),