diff --git a/api/v1beta1/cryostat_types.go b/api/v1beta1/cryostat_types.go index 4ff41e87..2afffa27 100644 --- a/api/v1beta1/cryostat_types.go +++ b/api/v1beta1/cryostat_types.go @@ -102,6 +102,14 @@ type CryostatSpec struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec SchedulingOptions *SchedulingConfiguration `json:"schedulingOptions,omitempty"` + // Options to configure the Cryostat application's target discovery mechanisms. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + TargetDiscoveryOptions *TargetDiscoveryOptions `json:"targetDiscoveryOptions,omitempty"` + // Options to configure the Cryostat application's JMX credentials database. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + JmxCredentialsDatabaseOptions *JmxCredentialsDatabaseOptions `json:"jmxCredentialsDatabaseOptions,omitempty"` } type ResourceConfigList struct { @@ -503,3 +511,19 @@ type ReportsSecurityOptions struct { // +operator-sdk:csv:customresourcedefinitions:type=spec ReportsSecurityContext *corev1.SecurityContext `json:"reportsSecurityContext,omitempty"` } + +// TargetDiscoveryOptions provides configuration options to the Cryostat application's target discovery mechanisms. +type TargetDiscoveryOptions struct { + // When true, the Cryostat application will disable the built-in discovery mechanisms. Defaults to false + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Disable Built-in Discovery",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + BuiltInDiscoveryDisabled bool `json:"builtInDiscoveryDisabled,omitempty"` +} + +// JmxCredentialsDatabaseOptions provides configuration options to the Cryostat application's JMX credentials database. +type JmxCredentialsDatabaseOptions struct { + // Name of the secret containing the password to encrypt JMX credentials database. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:io.kubernetes:Secret"} + DatabaseSecretName *string `json:"databaseSecretName,omitempty"` +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8bd25681..b285d284 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -259,6 +259,16 @@ func (in *CryostatSpec) DeepCopyInto(out *CryostatSpec) { *out = new(SchedulingConfiguration) (*in).DeepCopyInto(*out) } + if in.TargetDiscoveryOptions != nil { + in, out := &in.TargetDiscoveryOptions, &out.TargetDiscoveryOptions + *out = new(TargetDiscoveryOptions) + **out = **in + } + if in.JmxCredentialsDatabaseOptions != nil { + in, out := &in.JmxCredentialsDatabaseOptions, &out.JmxCredentialsDatabaseOptions + *out = new(JmxCredentialsDatabaseOptions) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CryostatSpec. @@ -344,6 +354,26 @@ func (in *JmxCacheOptions) DeepCopy() *JmxCacheOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JmxCredentialsDatabaseOptions) DeepCopyInto(out *JmxCredentialsDatabaseOptions) { + *out = *in + if in.DatabaseSecretName != nil { + in, out := &in.DatabaseSecretName, &out.DatabaseSecretName + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JmxCredentialsDatabaseOptions. +func (in *JmxCredentialsDatabaseOptions) DeepCopy() *JmxCredentialsDatabaseOptions { + if in == nil { + return nil + } + out := new(JmxCredentialsDatabaseOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkConfiguration) DeepCopyInto(out *NetworkConfiguration) { *out = *in @@ -690,6 +720,21 @@ func (in *StorageConfiguration) DeepCopy() *StorageConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetDiscoveryOptions) DeepCopyInto(out *TargetDiscoveryOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetDiscoveryOptions. +func (in *TargetDiscoveryOptions) DeepCopy() *TargetDiscoveryOptions { + if in == nil { + return nil + } + out := new(TargetDiscoveryOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplateConfigMap) DeepCopyInto(out *TemplateConfigMap) { *out = *in diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index dbd63c38..159b86b1 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -139,6 +139,16 @@ spec: path: jmxCacheOptions.targetCacheTTL x-descriptors: - urn:alm:descriptor:com.tectonic.ui:number + - description: Options to configure the Cryostat application's JMX credentials + database. + displayName: Jmx Credentials Database Options + path: jmxCredentialsDatabaseOptions + - description: Name of the secret containing the password to encrypt JMX credentials + database. + displayName: Database Secret Name + path: jmxCredentialsDatabaseOptions.databaseSecretName + x-descriptors: + - urn:alm:descriptor:io.kubernetes:Secret - description: The maximum number of WebSocket client connections allowed (minimum 1, default unlimited). displayName: Max WebSocket Connections @@ -380,6 +390,16 @@ spec: has created the PVC, changes to this field have no effect. displayName: Spec path: storageOptions.pvc.spec + - description: Options to configure the Cryostat application's target discovery + mechanisms. + displayName: Target Discovery Options + path: targetDiscoveryOptions + - description: When true, the Cryostat application will disable the built-in + discovery mechanisms. Defaults to false + displayName: Disable Built-in Discovery + path: targetDiscoveryOptions.builtInDiscoveryDisabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch - description: List of TLS certificates to trust when connecting to targets. displayName: Trusted TLS Certificates path: trustedCertSecrets diff --git a/bundle/manifests/operator.cryostat.io_cryostats.yaml b/bundle/manifests/operator.cryostat.io_cryostats.yaml index 0b255e7b..fc193c4f 100644 --- a/bundle/manifests/operator.cryostat.io_cryostats.yaml +++ b/bundle/manifests/operator.cryostat.io_cryostats.yaml @@ -105,6 +105,15 @@ spec: minimum: 1 type: integer type: object + jmxCredentialsDatabaseOptions: + description: Options to configure the Cryostat application's JMX credentials + database. + properties: + databaseSecretName: + description: Name of the secret containing the password to encrypt + JMX credentials database. + type: string + type: object maxWsConnections: description: The maximum number of WebSocket client connections allowed (minimum 1, default unlimited). @@ -4372,6 +4381,15 @@ spec: type: object type: object type: object + targetDiscoveryOptions: + description: Options to configure the Cryostat application's target + discovery mechanisms. + properties: + builtInDiscoveryDisabled: + description: When true, the Cryostat application will disable + the built-in discovery mechanisms. Defaults to false + type: boolean + type: object trustedCertSecrets: description: List of TLS certificates to trust when connecting to targets. diff --git a/config/crd/bases/operator.cryostat.io_cryostats.yaml b/config/crd/bases/operator.cryostat.io_cryostats.yaml index 3655d441..a5da61ab 100644 --- a/config/crd/bases/operator.cryostat.io_cryostats.yaml +++ b/config/crd/bases/operator.cryostat.io_cryostats.yaml @@ -106,6 +106,15 @@ spec: minimum: 1 type: integer type: object + jmxCredentialsDatabaseOptions: + description: Options to configure the Cryostat application's JMX credentials + database. + properties: + databaseSecretName: + description: Name of the secret containing the password to encrypt + JMX credentials database. + type: string + type: object maxWsConnections: description: The maximum number of WebSocket client connections allowed (minimum 1, default unlimited). @@ -4373,6 +4382,15 @@ spec: type: object type: object type: object + targetDiscoveryOptions: + description: Options to configure the Cryostat application's target + discovery mechanisms. + properties: + builtInDiscoveryDisabled: + description: When true, the Cryostat application will disable + the built-in discovery mechanisms. Defaults to false + type: boolean + type: object trustedCertSecrets: description: List of TLS certificates to trust when connecting to targets. diff --git a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml index 3ab0c1ea..f2f96c7d 100644 --- a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml @@ -127,6 +127,16 @@ spec: path: jmxCacheOptions.targetCacheTTL x-descriptors: - urn:alm:descriptor:com.tectonic.ui:number + - description: Options to configure the Cryostat application's JMX credentials + database. + displayName: Jmx Credentials Database Options + path: jmxCredentialsDatabaseOptions + - description: Name of the secret containing the password to encrypt JMX credentials + database. + displayName: Database Secret Name + path: jmxCredentialsDatabaseOptions.databaseSecretName + x-descriptors: + - urn:alm:descriptor:io.kubernetes:Secret - description: The maximum number of WebSocket client connections allowed (minimum 1, default unlimited). displayName: Max WebSocket Connections @@ -368,6 +378,16 @@ spec: has created the PVC, changes to this field have no effect. displayName: Spec path: storageOptions.pvc.spec + - description: Options to configure the Cryostat application's target discovery + mechanisms. + displayName: Target Discovery Options + path: targetDiscoveryOptions + - description: When true, the Cryostat application will disable the built-in + discovery mechanisms. Defaults to false + displayName: Disable Built-in Discovery + path: targetDiscoveryOptions.builtInDiscoveryDisabled + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch - description: List of TLS certificates to trust when connecting to targets. displayName: Trusted TLS Certificates path: trustedCertSecrets diff --git a/docs/config.md b/docs/config.md index 08a51716..3e43b886 100644 --- a/docs/config.md +++ b/docs/config.md @@ -269,6 +269,34 @@ spec: targetCacheTTL: 10 ``` +### JMX Credentials Database + +The Cryostat application must be provided with a password to encrypt saved JMX credentials in database. The user can specify a secret containing the password entry with key `CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD`. The Cryostat application will use this password to encrypt saved JMX credentials in database. + +For example: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: credentials-database-secret +type: Opaque +stringData: + CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD: a-very-good-password +``` + +Then, the property `.spec.jmxCredentialsDatabaseOptions.databaseSecretName` must be set to use this secret for password. + +```yaml +apiVersion: operator.cryostat.io/v1beta1 +kind: Cryostat +metadata: + name: cryostat-sample +spec: + jmxCredentialsDatabaseOptions: + databaseSecretName: credentials-database-secret +``` + +**Note**: If the secret is not provided, a default one is generated for this purpose. However, switching between using provided and generated secret is not allowed to avoid password mismatch that causes the Cryostat application's failure to access the credentials database. ### Authorization Properties @@ -293,7 +321,7 @@ If custom mapping is specified, a ClusterRole must be defined and should contain **Note**: Using [`Secret`](https://kubernetes.io/docs/concepts/configuration/secret/) in mapping can fail with access denied under [security protection](https://kubernetes.io/docs/concepts/configuration/secret/#information-security-for-secrets) against escalations. Find more details about this issue [here](https://docs.openshift.com/container-platform/4.11/authentication/tokens-scoping.html#scoping-tokens-role-scope_configuring-internal-oauth). -The property `.spec.authProperties` can then be set to configure Cryostat to use this mapping instead of the default ones. +The property `spec.authProperties` can then be set to configure Cryostat to use this mapping instead of the default ones. ```yaml apiVersion: operator.cryostat.io/v1beta1 kind: Cryostat @@ -436,3 +464,17 @@ spec: value: ok effect: NoExecute ``` + +### Target Discovery Options + +If you wish to use only Cryostat's [Discovery Plugin API](https://github.com/cryostatio/cryostat/blob/801779d5ddf7fa30f7b230f649220a852b06f27d/docs/DISCOVERY_PLUGINS.md), set the property `spec.targetDiscoveryOptions.builtInDiscoveryDisabled` to `true` to disable Cryostat's built-in discovery mechanisms. + +```yaml +apiVersion: operator.cryostat.io/v1beta1 +kind: Cryostat +metadata: + name: cryostat-sample +spec: + targetDiscoveryOptions: + builtInDiscoveryDisabled: true +``` diff --git a/go.mod b/go.mod index 80dfb7ff..dd7b672d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/go-logr/logr v1.2.0 + github.com/google/go-cmp v0.5.6 github.com/jetstack/cert-manager v1.7.1 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.18.1 @@ -41,7 +42,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.12 // indirect diff --git a/internal/controllers/common/resource_definitions/resource_definitions.go b/internal/controllers/common/resource_definitions/resource_definitions.go index ef5702c3..ed96f62a 100644 --- a/internal/controllers/common/resource_definitions/resource_definitions.go +++ b/internal/controllers/common/resource_definitions/resource_definitions.go @@ -741,6 +741,54 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa } } + disableBuiltInDiscovery := cr.Spec.TargetDiscoveryOptions != nil && cr.Spec.TargetDiscoveryOptions.BuiltInDiscoveryDisabled + if disableBuiltInDiscovery { + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_DISABLE_BUILTIN_DISCOVERY", + Value: "true", + }) + } + + if !useEmptyDir(cr) { + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_JDBC_URL", + Value: "jdbc:h2:file:/opt/cryostat.d/conf.d/h2;INIT=create domain if not exists jsonb as varchar", + }, corev1.EnvVar{ + Name: "CRYOSTAT_HBM2DDL", + Value: "update", + }, corev1.EnvVar{ + Name: "CRYOSTAT_JDBC_DRIVER", + Value: "org.h2.Driver", + }, corev1.EnvVar{ + Name: "CRYOSTAT_HIBERNATE_DIALECT", + Value: "org.hibernate.dialect.H2Dialect", + }, corev1.EnvVar{ + Name: "CRYOSTAT_JDBC_USERNAME", + Value: cr.Name, + }, corev1.EnvVar{ + Name: "CRYOSTAT_JDBC_PASSWORD", + Value: cr.Name, + }) + } + + secretOptional := false + secretName := cr.Name + "-jmx-credentials-db" + if cr.Spec.JmxCredentialsDatabaseOptions != nil && cr.Spec.JmxCredentialsDatabaseOptions.DatabaseSecretName != nil { + secretName = *cr.Spec.JmxCredentialsDatabaseOptions.DatabaseSecretName + } + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD", + Optional: &secretOptional, + }, + }, + }) + if !cr.Spec.Minimal { grafanaVars := []corev1.EnvVar{ { @@ -1030,9 +1078,7 @@ func getPullPolicy(imageTag string) corev1.PullPolicy { func newVolumeForCR(cr *operatorv1beta1.Cryostat) []corev1.Volume { var volumeSource corev1.VolumeSource - deployEmptyDir := cr.Spec.StorageOptions != nil && cr.Spec.StorageOptions.EmptyDir != nil && cr.Spec.StorageOptions.EmptyDir.Enabled - - if deployEmptyDir { + if useEmptyDir(cr) { emptyDir := cr.Spec.StorageOptions.EmptyDir sizeLimit, err := resource.ParseQuantity(emptyDir.SizeLimit) @@ -1073,3 +1119,7 @@ func seccompProfile(openshift bool) *corev1.SeccompProfile { Type: corev1.SeccompProfileTypeRuntimeDefault, } } + +func useEmptyDir(cr *operatorv1beta1.Cryostat) bool { + return cr.Spec.StorageOptions != nil && cr.Spec.StorageOptions.EmptyDir != nil && cr.Spec.StorageOptions.EmptyDir.Enabled +} diff --git a/internal/controllers/cryostat_controller_test.go b/internal/controllers/cryostat_controller_test.go index 88aa0385..d8e99596 100644 --- a/internal/controllers/cryostat_controller_test.go +++ b/internal/controllers/cryostat_controller_test.go @@ -109,7 +109,7 @@ var _ = Describe("CryostatController", func() { t = &cryostatTestInput{ TestReconcilerConfig: test.TestReconcilerConfig{ TLS: true, - GeneratedPasswords: []string{"grafana", "jmx", "keystore"}, + GeneratedPasswords: []string{"grafana", "credentials_database", "jmx", "keystore"}, }, externalTLS: true, } @@ -143,6 +143,9 @@ var _ = Describe("CryostatController", func() { It("should create Grafana secret and set owner", func() { t.expectGrafanaSecret() }) + It("should create Credentials Database secret and set owner", func() { + t.expectCredentialsDatabaseSecret() + }) It("should create JMX secret and set owner", func() { t.expectJMXSecret() }) @@ -214,7 +217,7 @@ var _ = Describe("CryostatController", func() { BeforeEach(func() { t.objs = append(t.objs, test.NewMinimalCryostat()) t.minimal = true - t.GeneratedPasswords = []string{"jmx", "keystore"} + t.GeneratedPasswords = []string{"credentials_database", "jmx", "keystore"} }) It("should create certificates", func() { t.expectCertificates() @@ -228,6 +231,9 @@ var _ = Describe("CryostatController", func() { It("should create persistent volume claim and set owner", func() { t.expectPVC(test.NewDefaultPVC()) }) + It("should create Credentials Database secret and set owner", func() { + t.expectCredentialsDatabaseSecret() + }) It("should create JMX secret and set owner", func() { t.expectJMXSecret() }) @@ -262,7 +268,7 @@ var _ = Describe("CryostatController", func() { }) }) Context("Cryostat does not exist", func() { - It("Should do nothing", func() { + It("should do nothing", func() { req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "does-not-exist", Namespace: "default"}} result, err := t.controller.Reconcile(context.Background(), req) Expect(err).ToNot(HaveOccurred()) @@ -472,6 +478,25 @@ var _ = Describe("CryostatController", func() { Expect(secret.StringData["CRYOSTAT_RJMX_PASS"]).To(Equal(oldSecret.StringData["CRYOSTAT_RJMX_PASS"])) }) }) + Context("with an existing Credentials Database Secret", func() { + var cr *operatorv1beta1.Cryostat + var oldSecret *corev1.Secret + BeforeEach(func() { + cr = test.NewCryostat() + oldSecret = test.OtherCredentialsDatabaseSecret() + t.objs = append(t.objs, cr, oldSecret) + }) + It("should not update password", func() { + t.reconcileCryostatFully() + + secret := &corev1.Secret{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: oldSecret.Name, Namespace: "default"}, secret) + Expect(err).ToNot(HaveOccurred()) + + Expect(metav1.IsControlledBy(secret, cr)).To(BeTrue()) + Expect(secret.StringData["CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD"]).To(Equal(oldSecret.StringData["CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD"])) + }) + }) Context("with existing Routes", func() { var cr *operatorv1beta1.Cryostat var oldCoreRoute *openshiftv1.Route @@ -493,7 +518,7 @@ var _ = Describe("CryostatController", func() { BeforeEach(func() { t.objs = append(t.objs, test.NewMinimalCryostat()) t.minimal = true - t.GeneratedPasswords = []string{"jmx", "keystore", "grafana"} + t.GeneratedPasswords = []string{"credentials_database", "jmx", "keystore", "grafana"} }) JustBeforeEach(func() { t.reconcileCryostatFully() @@ -963,6 +988,9 @@ var _ = Describe("CryostatController", func() { It("should create the EmptyDir with default specs", func() { t.expectEmptyDir(test.NewDefaultEmptyDir()) }) + It("should set Cryostat database to h2:mem", func() { + t.expectInMemoryDatabase() + }) }) Context("with custom EmptyDir config with requested spec", func() { BeforeEach(func() { @@ -971,6 +999,9 @@ var _ = Describe("CryostatController", func() { It("should create the EmptyDir with requested specs", func() { t.expectEmptyDir(test.NewEmptyDirWithSpec()) }) + It("should set Cryostat database to h2:file", func() { + t.expectInMemoryDatabase() + }) }) Context("with overriden image tags", func() { var mainDeploy, reportsDeploy *appsv1.Deployment @@ -1505,7 +1536,6 @@ var _ = Describe("CryostatController", func() { }) }) }) - Context("with resource requirements", func() { BeforeEach(func() { t.objs = append(t.objs, test.NewCryostatWithResources()) @@ -1590,6 +1620,41 @@ var _ = Describe("CryostatController", func() { }) }) + Context("with built-in target discovery mechanism disabled", func() { + BeforeEach(func() { + t.objs = append(t.objs, test.NewCryostatWithBuiltInDiscoveryDisabled()) + }) + It("should configure deployment appropriately", func() { + t.expectDeployment() + }) + }) + Context("with secret provided for database password", func() { + BeforeEach(func() { + t.objs = append(t.objs, test.NewCryostatWithDatabaseSecretProvided()) + }) + It("should configure deployment appropriately", func() { + t.expectDeployment() + }) + It("should not generate default secret", func() { + t.reconcileCryostatFully() + + secret := &corev1.Secret{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat-jmx-credentials-db", Namespace: "default"}, secret) + Expect(kerrors.IsNotFound(err)).To(BeTrue()) + }) + Context("with an existing Credentials Database Secret", func() { + BeforeEach(func() { + t.objs = append(t.objs, test.NewCredentialsDatabaseSecret()) + }) + It("should not delete the existing Credentials Database Secret", func() { + t.reconcileCryostatFully() + + secret := &corev1.Secret{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat-jmx-credentials-db", Namespace: "default"}, secret) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) }) Describe("reconciling a request in Kubernetes", func() { @@ -2223,6 +2288,18 @@ func (t *cryostatTestInput) expectEmptyDir(expectedEmptyDir *corev1.EmptyDirVolu Expect(emptyDir.SizeLimit).To(Equal(expectedEmptyDir.SizeLimit)) } +func (t *cryostatTestInput) expectInMemoryDatabase() { + t.reconcileCryostatFully() + + deployment := &appsv1.Deployment{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat", Namespace: "default"}, deployment) + Expect(err).ToNot(HaveOccurred()) + + containers := deployment.Spec.Template.Spec.Containers + coreContainer := containers[0] + Expect(coreContainer.Env).ToNot(ContainElements(test.DatabaseConfigEnvironmentVariables())) +} + func (t *cryostatTestInput) expectGrafanaSecret() { secret := &corev1.Secret{} err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat-grafana-basic", Namespace: "default"}, secret) @@ -2243,6 +2320,26 @@ func (t *cryostatTestInput) checkGrafanaSecret() { Expect(secret.StringData).To(Equal(expectedSecret.StringData)) } +func (t *cryostatTestInput) expectCredentialsDatabaseSecret() { + secret := &corev1.Secret{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat-jmx-credentials-db", Namespace: "default"}, secret) + Expect(kerrors.IsNotFound(err)).To(BeTrue()) + + t.reconcileCryostatFully() + t.checkCredentialsDatabaseSecret() +} + +func (t *cryostatTestInput) checkCredentialsDatabaseSecret() { + secret := &corev1.Secret{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat-jmx-credentials-db", Namespace: "default"}, secret) + Expect(err).ToNot(HaveOccurred()) + + // Compare to desired spec + expectedSecret := test.NewCredentialsDatabaseSecret() + checkMetadata(secret, expectedSecret) + Expect(secret.StringData).To(Equal(expectedSecret.StringData)) +} + func (t *cryostatTestInput) expectJMXSecret() { secret := &corev1.Secret{} err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat-jmx-auth", Namespace: "default"}, secret) @@ -2493,7 +2590,14 @@ func (t *cryostatTestInput) checkMainPodTemplate(deployment *appsv1.Deployment, } ingress := !t.controller.IsOpenShift && cr.Spec.NetworkOptions != nil && cr.Spec.NetworkOptions.CoreConfig != nil && cr.Spec.NetworkOptions.CoreConfig.IngressSpec != nil - checkCoreContainer(&coreContainer, t.minimal, t.TLS, t.externalTLS, t.EnvCoreImageTag, t.controller.IsOpenShift, ingress, reportsUrl, cr.Spec.AuthProperties != nil, cr.Spec.Resources.CoreResources, test.NewCoreSecurityContext(cr)) + emptyDir := cr.Spec.StorageOptions != nil && cr.Spec.StorageOptions.EmptyDir != nil && cr.Spec.StorageOptions.EmptyDir.Enabled + builtInDiscoveryDisabled := cr.Spec.TargetDiscoveryOptions != nil && cr.Spec.TargetDiscoveryOptions.BuiltInDiscoveryDisabled + dbSecretProvided := cr.Spec.JmxCredentialsDatabaseOptions != nil && cr.Spec.JmxCredentialsDatabaseOptions.DatabaseSecretName != nil + checkCoreContainer(&coreContainer, + t.minimal, t.TLS, t.externalTLS, t.EnvCoreImageTag, + t.controller.IsOpenShift, ingress, reportsUrl, + cr.Spec.AuthProperties != nil, emptyDir, builtInDiscoveryDisabled, dbSecretProvided, + cr.Spec.Resources.CoreResources, test.NewCoreSecurityContext(cr)) if !t.minimal { // Check that Grafana is configured properly, depending on the environment @@ -2607,7 +2711,7 @@ func (t *cryostatTestInput) checkDeploymentHasAuthProperties() { volumeMounts := coreContainer.VolumeMounts expectedVolumeMounts := test.NewVolumeMountsWithAuthProperties(t.TLS) Expect(volumeMounts).To(ConsistOf(expectedVolumeMounts)) - Expect(coreContainer.Env).To(ConsistOf(test.NewCoreEnvironmentVariables(t.minimal, t.TLS, t.externalTLS, t.controller.IsOpenShift, "", true, false))) + Expect(coreContainer.Env).To(ConsistOf(test.NewCoreEnvironmentVariables(t.minimal, t.TLS, t.externalTLS, t.controller.IsOpenShift, "", true, false, false, false, false))) } func (t *cryostatTestInput) checkDeploymentHasNoAuthProperties() { @@ -2629,7 +2733,11 @@ func (t *cryostatTestInput) checkDeploymentHasNoAuthProperties() { } func checkCoreContainer(container *corev1.Container, minimal bool, tls bool, externalTLS bool, - tag *string, openshift bool, ingress bool, reportsUrl string, authProps bool, resources corev1.ResourceRequirements, securityContext *corev1.SecurityContext) { + tag *string, openshift bool, ingress bool, + reportsUrl string, authProps bool, + emptyDir bool, builtInDiscoveryDisabled bool, dbSecretProvided bool, + resources corev1.ResourceRequirements, + securityContext *corev1.SecurityContext) { Expect(container.Name).To(Equal("cryostat")) if tag == nil { Expect(container.Image).To(HavePrefix("quay.io/cryostat/cryostat:")) @@ -2637,7 +2745,7 @@ func checkCoreContainer(container *corev1.Container, minimal bool, tls bool, ext Expect(container.Image).To(Equal(*tag)) } Expect(container.Ports).To(ConsistOf(test.NewCorePorts())) - Expect(container.Env).To(ConsistOf(test.NewCoreEnvironmentVariables(minimal, tls, externalTLS, openshift, reportsUrl, authProps, ingress))) + Expect(container.Env).To(ConsistOf(test.NewCoreEnvironmentVariables(minimal, tls, externalTLS, openshift, reportsUrl, authProps, ingress, emptyDir, builtInDiscoveryDisabled, dbSecretProvided))) Expect(container.EnvFrom).To(ConsistOf(test.NewCoreEnvFromSource(tls))) Expect(container.VolumeMounts).To(ConsistOf(test.NewCoreVolumeMounts(tls))) Expect(container.LivenessProbe).To(Equal(test.NewCoreLivenessProbe(tls))) diff --git a/internal/controllers/secrets.go b/internal/controllers/secrets.go index add49ae6..2ee2e771 100644 --- a/internal/controllers/secrets.go +++ b/internal/controllers/secrets.go @@ -49,8 +49,10 @@ import ( ) func (r *CryostatReconciler) reconcileSecrets(ctx context.Context, cr *operatorv1beta1.Cryostat) error { - err := r.reconcileGrafanaSecret(ctx, cr) - if err != nil { + if err := r.reconcileGrafanaSecret(ctx, cr); err != nil { + return err + } + if err := r.reconcileDatabaseSecret(ctx, cr); err != nil { return err } return r.reconcileJMXSecret(ctx, cr) @@ -127,6 +129,39 @@ func (r *CryostatReconciler) reconcileJMXSecret(ctx context.Context, cr *operato }) } +// databaseSecretNameSuffix is the suffix to be appended to the name of a +// Cryostat CR to name its JMX credentials database secret +const databaseSecretNameSuffix = "-jmx-credentials-db" + +// dbSecretUserKey indexes the password within the Cryostat JMX credentials database Secret +const databaseSecretPassKey = "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD" + +func (r *CryostatReconciler) reconcileDatabaseSecret(ctx context.Context, cr *operatorv1beta1.Cryostat) error { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cr.Name + databaseSecretNameSuffix, + Namespace: cr.Namespace, + }, + } + + secretProvided := cr.Spec.JmxCredentialsDatabaseOptions != nil && cr.Spec.JmxCredentialsDatabaseOptions.DatabaseSecretName != nil + if secretProvided { + return nil // Do not delete default secret to allow reverting to use default if needed + } + + return r.createOrUpdateSecret(ctx, secret, cr, func() error { + if secret.StringData == nil { + secret.StringData = map[string]string{} + } + + // Password is generated, so don't regenerate it when updating + if secret.CreationTimestamp.IsZero() { + secret.StringData[databaseSecretPassKey] = r.GenPasswd(32) + } + return nil + }) +} + func (r *CryostatReconciler) createOrUpdateSecret(ctx context.Context, secret *corev1.Secret, owner metav1.Object, delegate controllerutil.MutateFn) error { op, err := controllerutil.CreateOrUpdate(ctx, r.Client, secret, func() error { diff --git a/internal/test/resources.go b/internal/test/resources.go index 700d0c2b..8230f047 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -454,6 +454,14 @@ func NewCryostatWithAuthProperties() *operatorv1beta1.Cryostat { return cr } +func NewCryostatWithBuiltInDiscoveryDisabled() *operatorv1beta1.Cryostat { + cr := NewCryostat() + cr.Spec.TargetDiscoveryOptions = &operatorv1beta1.TargetDiscoveryOptions{ + BuiltInDiscoveryDisabled: true, + } + return cr +} + func newPVCSpec(storageClass string, storageRequest string, accessModes ...corev1.PersistentVolumeAccessMode) *corev1.PersistentVolumeClaimSpec { return &corev1.PersistentVolumeClaimSpec{ @@ -560,6 +568,15 @@ func NewCryostatWithReportSecurityOptions() *operatorv1beta1.Cryostat { return cr } +var providedDatabaseSecretName string = "credentials-database-secret" + +func NewCryostatWithDatabaseSecretProvided() *operatorv1beta1.Cryostat { + cr := NewCryostat() + cr.Spec.JmxCredentialsDatabaseOptions = &operatorv1beta1.JmxCredentialsDatabaseOptions{ + DatabaseSecretName: &providedDatabaseSecretName, + } + return cr +} func NewCryostatService() *corev1.Service { c := true return &corev1.Service{ @@ -766,6 +783,30 @@ func OtherGrafanaSecret() *corev1.Secret { } } +func NewCredentialsDatabaseSecret() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cryostat-jmx-credentials-db", + Namespace: "default", + }, + StringData: map[string]string{ + "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD": "credentials_database", + }, + } +} + +func OtherCredentialsDatabaseSecret() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cryostat-jmx-credentials-db", + Namespace: "default", + }, + StringData: map[string]string{ + "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD": "other-pass", + }, + } +} + func NewJMXSecret() *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -1077,7 +1118,10 @@ func NewReportsPorts() []corev1.ContainerPort { } } -func NewCoreEnvironmentVariables(minimal bool, tls bool, externalTLS bool, openshift bool, reportsUrl string, authProps bool, ingress bool) []corev1.EnvVar { +func NewCoreEnvironmentVariables( + minimal bool, tls bool, externalTLS bool, openshift bool, + reportsUrl string, authProps bool, ingress bool, + emptyDir bool, builtInDiscoveryDisabled bool, dbSecretProvided bool) []corev1.EnvVar { envs := []corev1.EnvVar{ { Name: "CRYOSTAT_WEB_PORT", @@ -1116,6 +1160,36 @@ func NewCoreEnvironmentVariables(minimal bool, tls bool, externalTLS bool, opens Value: "10", }, } + + if builtInDiscoveryDisabled { + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_DISABLE_BUILTIN_DISCOVERY", + Value: "true", + }) + } + + if !emptyDir { + envs = append(envs, DatabaseConfigEnvironmentVariables()...) + } + + optional := false + secretName := NewCredentialsDatabaseSecret().Name + if dbSecretProvided { + secretName = providedDatabaseSecretName + } + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "CRYOSTAT_JMX_CREDENTIALS_DB_PASSWORD", + Optional: &optional, + }, + }, + }) + if !minimal { envs = append(envs, corev1.EnvVar{ @@ -1189,6 +1263,35 @@ func NewCoreEnvironmentVariables(minimal bool, tls bool, externalTLS bool, opens return envs } +func DatabaseConfigEnvironmentVariables() []corev1.EnvVar { + return []corev1.EnvVar{ + { + Name: "CRYOSTAT_JDBC_URL", + Value: "jdbc:h2:file:/opt/cryostat.d/conf.d/h2;INIT=create domain if not exists jsonb as varchar", + }, + { + Name: "CRYOSTAT_HBM2DDL", + Value: "update", + }, + { + Name: "CRYOSTAT_JDBC_DRIVER", + Value: "org.h2.Driver", + }, + { + Name: "CRYOSTAT_HIBERNATE_DIALECT", + Value: "org.hibernate.dialect.H2Dialect", + }, + { + Name: "CRYOSTAT_JDBC_USERNAME", + Value: "cryostat", + }, + { + Name: "CRYOSTAT_JDBC_PASSWORD", + Value: "cryostat", + }, + } +} + func newNetworkEnvironmentVariables(minimal, tls, externalTLS bool) []corev1.EnvVar { envs := []corev1.EnvVar{ { @@ -1830,7 +1933,7 @@ func commonDefaultSecurityContext() *corev1.SecurityContext { } func NewPodSecurityContext(cr *operatorv1beta1.Cryostat, openshift bool) *corev1.PodSecurityContext { - if cr.Spec.SecurityOptions != nil { + if cr.Spec.SecurityOptions != nil && cr.Spec.SecurityOptions.PodSecurityContext != nil { return cr.Spec.SecurityOptions.PodSecurityContext } fsGroup := int64(18500) @@ -1838,35 +1941,35 @@ func NewPodSecurityContext(cr *operatorv1beta1.Cryostat, openshift bool) *corev1 } func NewReportPodSecurityContext(cr *operatorv1beta1.Cryostat, openshift bool) *corev1.PodSecurityContext { - if cr.Spec.ReportOptions != nil && cr.Spec.ReportOptions.SecurityOptions != nil { + if cr.Spec.ReportOptions != nil && cr.Spec.ReportOptions.SecurityOptions != nil && cr.Spec.ReportOptions.SecurityOptions.PodSecurityContext != nil { return cr.Spec.ReportOptions.SecurityOptions.PodSecurityContext } return commonDefaultPodSecurityContext(openshift, nil) } func NewCoreSecurityContext(cr *operatorv1beta1.Cryostat) *corev1.SecurityContext { - if cr.Spec.SecurityOptions != nil { + if cr.Spec.SecurityOptions != nil && cr.Spec.SecurityOptions.CoreSecurityContext != nil { return cr.Spec.SecurityOptions.CoreSecurityContext } return commonDefaultSecurityContext() } func NewGrafanaSecurityContext(cr *operatorv1beta1.Cryostat) *corev1.SecurityContext { - if cr.Spec.SecurityOptions != nil { + if cr.Spec.SecurityOptions != nil && cr.Spec.SecurityOptions.GrafanaSecurityContext != nil { return cr.Spec.SecurityOptions.GrafanaSecurityContext } return commonDefaultSecurityContext() } func NewDatasourceSecurityContext(cr *operatorv1beta1.Cryostat) *corev1.SecurityContext { - if cr.Spec.SecurityOptions != nil { + if cr.Spec.SecurityOptions != nil && cr.Spec.SecurityOptions.DataSourceSecurityContext != nil { return cr.Spec.SecurityOptions.DataSourceSecurityContext } return commonDefaultSecurityContext() } func NewReportSecurityContext(cr *operatorv1beta1.Cryostat) *corev1.SecurityContext { - if cr.Spec.ReportOptions != nil && cr.Spec.ReportOptions.SecurityOptions != nil { + if cr.Spec.ReportOptions != nil && cr.Spec.ReportOptions.SecurityOptions != nil && cr.Spec.ReportOptions.SecurityOptions.ReportsSecurityContext != nil { return cr.Spec.ReportOptions.SecurityOptions.ReportsSecurityContext } return commonDefaultSecurityContext() @@ -2150,38 +2253,39 @@ func OtherServiceAccount() *corev1.ServiceAccount { } func NewRole() *rbacv1.Role { + rules := []rbacv1.PolicyRule{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{""}, + Resources: []string{"endpoints"}, + }, + { + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"pods", "replicationcontrollers"}, + }, + { + Verbs: []string{"get"}, + APIGroups: []string{"apps"}, + Resources: []string{"replicasets", "deployments", "daemonsets", "statefulsets"}, + }, + { + Verbs: []string{"get"}, + APIGroups: []string{"apps.openshift.io"}, + Resources: []string{"deploymentconfigs"}, + }, + { + Verbs: []string{"get", "list"}, + APIGroups: []string{"route.openshift.io"}, + Resources: []string{"routes"}, + }, + } return &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: "cryostat", Namespace: "default", }, - Rules: []rbacv1.PolicyRule{ - { - Verbs: []string{"get", "list", "watch"}, - APIGroups: []string{""}, - Resources: []string{"endpoints"}, - }, - { - Verbs: []string{"get"}, - APIGroups: []string{""}, - Resources: []string{"pods", "replicationcontrollers"}, - }, - { - Verbs: []string{"get"}, - APIGroups: []string{"apps"}, - Resources: []string{"replicasets", "deployments", "daemonsets", "statefulsets"}, - }, - { - Verbs: []string{"get"}, - APIGroups: []string{"apps.openshift.io"}, - Resources: []string{"deploymentconfigs"}, - }, - { - Verbs: []string{"get", "list"}, - APIGroups: []string{"route.openshift.io"}, - Resources: []string{"routes"}, - }, - }, + Rules: rules, } }