diff --git a/api/v1beta1/cryostat_types.go b/api/v1beta1/cryostat_types.go index 5237ba1c8..cd98313f6 100644 --- a/api/v1beta1/cryostat_types.go +++ b/api/v1beta1/cryostat_types.go @@ -90,6 +90,10 @@ type CryostatSpec struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec Resources ResourceConfigList `json:"resources,omitempty"` + // Override default authorization properties for Cryostat on OpenShift. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Authorization Properties",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced"} + AuthProperties *AuthorizationProperties `json:"authProperties,omitempty"` } type ResourceConfigList struct { @@ -151,7 +155,6 @@ type StorageConfiguration struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec PVC *PersistentVolumeClaimConfig `json:"pvc,omitempty"` - // Configuration for an EmptyDir to be created // by the operator instead of a PVC. // +optional @@ -402,3 +405,19 @@ type TemplateConfigMap struct { // Filename within config map containing the template file Filename string `json:"filename"` } + +// Authorization properties provide custom permission mapping between Cryostat resources to Kubernetes resources. +// If the mapping is updated, Cryostat must be manually restarted. +type AuthorizationProperties struct { + // Name of the ClusterRole to use when Cryostat requests a role-scoped OAuth token. + // This ClusterRole should contain permissions for all Kubernetes objects listed in custom permission mapping. + // More details: https://docs.openshift.com/container-platform/4.11/authentication/tokens-scoping.html#scoping-tokens-role-scope_configuring-internal-oauth + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="ClusterRole Name",xDescriptors={"urn:alm:descriptor:io.kubernetes:ClusterRole"} + ClusterRoleName string `json:"clusterRoleName"` + // Name of config map in the local namespace. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="ConfigMap Name",xDescriptors={"urn:alm:descriptor:io.kubernetes:ConfigMap"} + ConfigMapName string `json:"configMapName"` + // Filename within config map containing the resource mapping. + // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text"} + Filename string `json:"filename"` +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index f4b4543d3..51a99e176 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -48,6 +48,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationProperties) DeepCopyInto(out *AuthorizationProperties) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationProperties. +func (in *AuthorizationProperties) DeepCopy() *AuthorizationProperties { + if in == nil { + return nil + } + out := new(AuthorizationProperties) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CertificateSecret) DeepCopyInto(out *CertificateSecret) { *out = *in @@ -199,6 +214,11 @@ func (in *CryostatSpec) DeepCopyInto(out *CryostatSpec) { **out = **in } in.Resources.DeepCopyInto(&out.Resources) + if in.AuthProperties != nil { + in, out := &in.AuthProperties, &out.AuthProperties + *out = new(AuthorizationProperties) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CryostatSpec. diff --git a/bundle/manifests/cryostat-operator-oauth-client_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/cryostat-operator-oauth-client_rbac.authorization.k8s.io_v1_clusterrole.yaml index feea14b58..4cd821915 100644 --- a/bundle/manifests/cryostat-operator-oauth-client_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/bundle/manifests/cryostat-operator-oauth-client_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -5,37 +5,32 @@ metadata: name: cryostat-operator-oauth-client rules: - apiGroups: - - "" + - operator.cryostat.io resources: - - pods + - cryostats verbs: - create + - patch + - delete - get - apiGroups: - "" resources: - - replicationcontrollers - - endpoints - verbs: - - get -- apiGroups: - - operator.cryostat.io - resources: - - cryostats + - pods + - pods/exec + - services verbs: - create + - patch - delete - get - apiGroups: - - operator.cryostat.io + - "" resources: - - flightrecorders - - recordings + - replicationcontrollers + - endpoints verbs: - - create - - delete - get - - patch - apiGroups: - apps resources: diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 1737168ad..5814676d8 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -120,6 +120,28 @@ spec: name: "" version: v1 specDescriptors: + - description: Override default authorization properties for Cryostat on OpenShift. + displayName: Authorization Properties + path: authProperties + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:advanced + - description: 'Name of the ClusterRole to use when Cryostat requests a role-scoped + OAuth token. This ClusterRole should contain permissions for all Kubernetes + objects listed in custom permission mapping. More details: https://docs.openshift.com/container-platform/4.11/authentication/tokens-scoping.html#scoping-tokens-role-scope_configuring-internal-oauth' + displayName: ClusterRole Name + path: authProperties.clusterRoleName + x-descriptors: + - urn:alm:descriptor:io.kubernetes:ClusterRole + - description: Name of config map in the local namespace. + displayName: ConfigMap Name + path: authProperties.configMapName + x-descriptors: + - urn:alm:descriptor:io.kubernetes:ConfigMap + - description: Filename within config map containing the resource mapping. + displayName: Filename + path: authProperties.filename + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:text - description: Use cert-manager to secure in-cluster communication between Cryostat components. Requires cert-manager to be installed. displayName: Enable cert-manager Integration diff --git a/bundle/manifests/operator.cryostat.io_cryostats.yaml b/bundle/manifests/operator.cryostat.io_cryostats.yaml index fe4780ee3..9bbefed44 100644 --- a/bundle/manifests/operator.cryostat.io_cryostats.yaml +++ b/bundle/manifests/operator.cryostat.io_cryostats.yaml @@ -43,6 +43,28 @@ spec: spec: description: CryostatSpec defines the desired state of Cryostat properties: + authProperties: + description: Override default authorization properties for Cryostat + on OpenShift. + properties: + clusterRoleName: + description: 'Name of the ClusterRole to use when Cryostat requests + a role-scoped OAuth token. This ClusterRole should contain permissions + for all Kubernetes objects listed in custom permission mapping. + More details: https://docs.openshift.com/container-platform/4.11/authentication/tokens-scoping.html#scoping-tokens-role-scope_configuring-internal-oauth' + type: string + configMapName: + description: Name of config map in the local namespace. + type: string + filename: + description: Filename within config map containing the resource + mapping. + type: string + required: + - clusterRoleName + - configMapName + - filename + type: object enableCertManager: description: Use cert-manager to secure in-cluster communication between Cryostat components. Requires cert-manager to be installed. diff --git a/config/crd/bases/operator.cryostat.io_cryostats.yaml b/config/crd/bases/operator.cryostat.io_cryostats.yaml index 5ad43b84b..2971d1bda 100644 --- a/config/crd/bases/operator.cryostat.io_cryostats.yaml +++ b/config/crd/bases/operator.cryostat.io_cryostats.yaml @@ -44,6 +44,28 @@ spec: spec: description: CryostatSpec defines the desired state of Cryostat properties: + authProperties: + description: Override default authorization properties for Cryostat + on OpenShift. + properties: + clusterRoleName: + description: 'Name of the ClusterRole to use when Cryostat requests + a role-scoped OAuth token. This ClusterRole should contain permissions + for all Kubernetes objects listed in custom permission mapping. + More details: https://docs.openshift.com/container-platform/4.11/authentication/tokens-scoping.html#scoping-tokens-role-scope_configuring-internal-oauth' + type: string + configMapName: + description: Name of config map in the local namespace. + type: string + filename: + description: Filename within config map containing the resource + mapping. + type: string + required: + - clusterRoleName + - configMapName + - filename + type: object enableCertManager: description: Use cert-manager to secure in-cluster communication between Cryostat components. Requires cert-manager to be installed. diff --git a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml index 84d42dabd..95487ac50 100644 --- a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml @@ -108,6 +108,28 @@ spec: name: "" version: v1 specDescriptors: + - description: Override default authorization properties for Cryostat on OpenShift. + displayName: Authorization Properties + path: authProperties + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:advanced + - description: 'Name of the ClusterRole to use when Cryostat requests a role-scoped + OAuth token. This ClusterRole should contain permissions for all Kubernetes + objects listed in custom permission mapping. More details: https://docs.openshift.com/container-platform/4.11/authentication/tokens-scoping.html#scoping-tokens-role-scope_configuring-internal-oauth' + displayName: ClusterRole Name + path: authProperties.clusterRoleName + x-descriptors: + - urn:alm:descriptor:io.kubernetes:ClusterRole + - description: Name of config map in the local namespace. + displayName: ConfigMap Name + path: authProperties.configMapName + x-descriptors: + - urn:alm:descriptor:io.kubernetes:ConfigMap + - description: Filename within config map containing the resource mapping. + displayName: Filename + path: authProperties.filename + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:text - description: Use cert-manager to secure in-cluster communication between Cryostat components. Requires cert-manager to be installed. displayName: Enable cert-manager Integration diff --git a/config/rbac/oauth_client.yaml b/config/rbac/oauth_client.yaml index cb58d7caf..d8c6693c8 100644 --- a/config/rbac/oauth_client.yaml +++ b/config/rbac/oauth_client.yaml @@ -7,37 +7,32 @@ metadata: name: oauth-client rules: - apiGroups: - - "" + - operator.cryostat.io resources: - - pods + - cryostats verbs: - create + - patch + - delete - get - apiGroups: - "" resources: - - replicationcontrollers - - endpoints - verbs: - - get -- apiGroups: - - operator.cryostat.io - resources: - - cryostats + - pods + - pods/exec + - services verbs: - create + - patch - delete - get - apiGroups: - - operator.cryostat.io + - "" resources: - - flightrecorders - - recordings + - replicationcontrollers + - endpoints verbs: - - create - - delete - get - - patch - apiGroups: - apps resources: diff --git a/docs/config.md b/docs/config.md index dd368186e..ebb576399 100644 --- a/docs/config.md +++ b/docs/config.md @@ -133,9 +133,10 @@ metadata: spec: reportOptions: replicas: 1 - requests: - cpu: 1000m - memory: 512Mi + resources: + requests: + cpu: 1000m + memory: 512Mi ``` If zero sidecar replicas are configured, SubProcessMaxHeapSize configures the maximum heap size of the main Cryostat container's subprocess report generator in MiB. @@ -267,3 +268,44 @@ spec: targetCacheSize: -1 targetCacheTTL: 10 ``` + + +### Authorization Properties + +When running on OpenShift, the user is required to have sufficient permissions for certain Kubernetes resources that are mapped into Cryostat-managed resources for authorization. + +The mappings can be specified using a ConfigMap that is compatible with [`OpenShiftAuthManager.properties`](https://github.com/cryostatio/cryostat/blob/bd95e1a11e9e29cc39559f4a5fdeaae77e81b4c6/src/main/resources/io/cryostat/net/openshift/OpenShiftAuthManager.properties). For example: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: auth-properties +data: + auth.properties: | + TARGET=pods,deployments.apps + RECORDING=pods,pods/exec + CERTIFICATE=deployments.apps,pods,cryostats.operator.cryostat.io + CREDENTIALS=cryostats.operator.cryostat.io +``` + +If custom mapping is specified, a ClusterRole must be defined and should contain permissions for all Kubernetes objects listed in custom permission mapping. This ClusterRole will give additional rules on top of [default rules](placeholder). + + +**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. +```yaml +apiVersion: operator.cryostat.io/v1beta1 +kind: Cryostat +metadata: + name: cryostat-sample +spec: + authProperties: + configMapName: auth-properties + filename: auth.properties + clusterRoleName: oauth-cluster-role +``` + +Each `configMapName` must refer to the name of a Config Map in the same namespace as Cryostat. The corresponding `filename` must be a key within that Config Map containing resource mappings. The `clusterRoleName` must be a valid name of an existing Cluster Role. + +**Note:** If the mapping is updated, Cryostat must be manually restarted. diff --git a/internal/controllers/common/resource_definitions/resource_definitions.go b/internal/controllers/common/resource_definitions/resource_definitions.go index 289dd3bc1..e5b9383a8 100644 --- a/internal/controllers/common/resource_definitions/resource_definitions.go +++ b/internal/controllers/common/resource_definitions/resource_definitions.go @@ -96,6 +96,7 @@ const ( datasourceContainerPort int32 = 8080 reportsContainerPort int32 = 10000 loopbackAddress string = "127.0.0.1" + operatorNamePrefix string = "cryostat-operator-" ) func NewPersistentVolumeClaimForCR(cr *operatorv1beta1.Cryostat) *corev1.PersistentVolumeClaim { @@ -351,6 +352,28 @@ func NewPodForCR(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTags *I volumes = append(volumes, eventTemplateVolume) } + // Add Auth properties as a volume if specified (on Openshift) + if openshift && cr.Spec.AuthProperties != nil { + authResourceVolume := corev1.Volume{ + Name: "auth-properties-" + cr.Spec.AuthProperties.ConfigMapName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cr.Spec.AuthProperties.ConfigMapName, + }, + Items: []corev1.KeyToPath{ + { + Key: cr.Spec.AuthProperties.Filename, + Path: "OpenShiftAuthManager.properties", + Mode: &readOnlyMode, + }, + }, + }, + }, + } + volumes = append(volumes, authResourceVolume) + } + // Ensure PV mounts are writable sc := &corev1.PodSecurityContext{ FSGroup: &fsGroup, @@ -498,6 +521,8 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa templatesPath := "/opt/cryostat.d/templates.d" clientlibPath := "/opt/cryostat.d/clientlib.d" probesPath := "/opt/cryostat.d/probes.d" + authPropertiesPath := "/app/resources/io/cryostat/net/openshift/OpenShiftAuthManager.properties" + envs := []corev1.EnvVar{ { Name: "CRYOSTAT_WEB_PORT", @@ -528,6 +553,46 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa Value: "false", }, } + + mounts := []corev1.VolumeMount{ + { + Name: cr.Name, + MountPath: configPath, + SubPath: "config", + }, + { + Name: cr.Name, + MountPath: archivePath, + SubPath: "flightrecordings", + }, + { + Name: cr.Name, + MountPath: templatesPath, + SubPath: "templates", + }, + { + Name: cr.Name, + MountPath: clientlibPath, + SubPath: "clientlib", + }, + { + Name: cr.Name, + MountPath: probesPath, + SubPath: "probes", + }, + { + Name: cr.Name, + MountPath: "truststore", + SubPath: "truststore", + }, + { + // Mount the CA cert and user certificates in the expected /truststore location + Name: "cert-secrets", + MountPath: "/truststore/operator", + ReadOnly: true, + }, + } + if specs.CoreURL != nil { coreEnvs := []corev1.EnvVar{ { @@ -597,6 +662,15 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa }, } envs = append(envs, jmxCacheEnvs...) + envsFrom := []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cr.Name + "-jmx-auth", + }, + }, + }, + } if openshift { // Force OpenShift platform strategy @@ -614,59 +688,25 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa Value: cr.Name, }, { - Name: "CRYOSTAT_OAUTH_ROLE", - Value: "cryostat-operator-oauth-client", + Name: "CRYOSTAT_BASE_OAUTH_ROLE", + Value: operatorNamePrefix + "oauth-client", }, } envs = append(envs, openshiftEnvs...) - } - envsFrom := []corev1.EnvFromSource{ - { - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: cr.Name + "-jmx-auth", - }, - }, - }, - } - mounts := []corev1.VolumeMount{ - { - Name: cr.Name, - MountPath: configPath, - SubPath: "config", - }, - { - Name: cr.Name, - MountPath: archivePath, - SubPath: "flightrecordings", - }, - { - Name: cr.Name, - MountPath: templatesPath, - SubPath: "templates", - }, - { - Name: cr.Name, - MountPath: clientlibPath, - SubPath: "clientlib", - }, - { - Name: cr.Name, - MountPath: probesPath, - SubPath: "probes", - }, - { - Name: cr.Name, - MountPath: "truststore", - SubPath: "truststore", - }, - { - // Mount the CA cert and user certificates in the expected /truststore location - Name: "cert-secrets", - MountPath: "/truststore/operator", - ReadOnly: true, - }, + if cr.Spec.AuthProperties != nil { + // Mount Auth properties if specified (on Openshift) + mounts = append(mounts, corev1.VolumeMount{ + Name: "auth-properties-" + cr.Spec.AuthProperties.ConfigMapName, + MountPath: authPropertiesPath, + SubPath: "OpenShiftAuthManager.properties", + ReadOnly: true, + }) + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_CUSTOM_OAUTH_ROLE", + Value: cr.Spec.AuthProperties.ClusterRoleName, + }) + } } if !cr.Spec.Minimal { diff --git a/internal/controllers/cryostat_controller_test.go b/internal/controllers/cryostat_controller_test.go index c1f4d8a2c..d3117931c 100644 --- a/internal/controllers/cryostat_controller_test.go +++ b/internal/controllers/cryostat_controller_test.go @@ -658,11 +658,11 @@ var _ = Describe("CryostatController", func() { volumes := deployment.Spec.Template.Spec.Volumes expectedVolumes := test.NewVolumesWithSecrets(t.TLS) - Expect(volumes).To(Equal(expectedVolumes)) + Expect(volumes).To(ConsistOf(expectedVolumes)) volumeMounts := deployment.Spec.Template.Spec.Containers[0].VolumeMounts expectedVolumeMounts := test.NewCoreVolumeMounts(t.TLS) - Expect(volumeMounts).To(Equal(expectedVolumeMounts)) + Expect(volumeMounts).To(ConsistOf(expectedVolumeMounts)) }) }) Context("Cryostat CR has list of event templates", func() { @@ -1297,6 +1297,17 @@ var _ = Describe("CryostatController", func() { }) }) }) + Context("Cryostat CR has authorization properties", func() { + BeforeEach(func() { + t.objs = append(t.objs, test.NewCryostatWithAuthProperties(), test.NewAuthPropertiesConfigMap()) + }) + JustBeforeEach(func() { + t.reconcileCryostatFully() + }) + It("Should add volumes and volumeMounts to deployment", func() { + t.checkDeploymentHasAuthProperties() + }) + }) }) Describe("reconciling a request in Kubernetes", func() { JustBeforeEach(func() { @@ -1418,6 +1429,17 @@ var _ = Describe("CryostatController", func() { Expect(kerrors.IsNotFound(err)).To(BeTrue()) }) }) + Context("Cryostat CR has authorization properties", func() { + BeforeEach(func() { + t.objs = append(t.objs, test.NewCryostatWithAuthProperties(), test.NewAuthPropertiesConfigMap()) + }) + JustBeforeEach(func() { + t.reconcileCryostatFully() + }) + It("Should not add volumes and volumeMounts to deployment", func() { + t.checkDeploymentHasNoAuthProperties() + }) + }) }) }) @@ -1802,11 +1824,11 @@ func (t *cryostatTestInput) expectDeploymentHasCertSecrets() { volumes := deployment.Spec.Template.Spec.Volumes expectedVolumes := test.NewVolumesWithSecrets(t.TLS) - Expect(volumes).To(Equal(expectedVolumes)) + Expect(volumes).To(ConsistOf(expectedVolumes)) volumeMounts := deployment.Spec.Template.Spec.Containers[0].VolumeMounts expectedVolumeMounts := test.NewCoreVolumeMounts(t.TLS) - Expect(volumeMounts).To(Equal(expectedVolumeMounts)) + Expect(volumeMounts).To(ConsistOf(expectedVolumeMounts)) } func (t *cryostatTestInput) expectIdempotence() { @@ -1960,7 +1982,7 @@ func (t *cryostatTestInput) checkMainPodTemplate(deployment *appsv1.Deployment, "kind": "cryostat", "component": "cryostat", })) - Expect(template.Spec.Volumes).To(Equal(test.NewVolumes(t.minimal, t.TLS))) + Expect(template.Spec.Volumes).To(ConsistOf(test.NewVolumes(t.minimal, t.TLS))) Expect(template.Spec.SecurityContext).To(Equal(test.NewPodSecurityContext())) // Check that the networking environment variables are set correctly @@ -1979,7 +2001,7 @@ func (t *cryostatTestInput) checkMainPodTemplate(deployment *appsv1.Deployment, reportsUrl = "http://cryostat-reports:" + port } - checkCoreContainer(&coreContainer, t.minimal, t.TLS, t.externalTLS, t.EnvCoreImageTag, t.controller.IsOpenShift, reportsUrl, cr.Spec.Resources.CoreResources) + checkCoreContainer(&coreContainer, t.minimal, t.TLS, t.externalTLS, t.EnvCoreImageTag, t.controller.IsOpenShift, reportsUrl, cr.Spec.AuthProperties != nil, cr.Spec.Resources.CoreResources) if !t.minimal { // Check that Grafana is configured properly, depending on the environment @@ -2028,7 +2050,7 @@ func (t *cryostatTestInput) checkReportsDeployment() { "kind": "cryostat", "component": "reports", })) - Expect(template.Spec.Volumes).To(Equal(test.NewReportsVolumes(t.TLS))) + Expect(template.Spec.Volumes).To(ConsistOf(test.NewReportsVolumes(t.TLS))) var resources corev1.ResourceRequirements if cr.Spec.ReportOptions != nil { @@ -2046,15 +2068,50 @@ func (t *cryostatTestInput) checkDeploymentHasTemplates() { volumes := deployment.Spec.Template.Spec.Volumes expectedVolumes := test.NewVolumesWithTemplates(t.TLS) - Expect(volumes).To(Equal(expectedVolumes)) + Expect(volumes).To(ConsistOf(expectedVolumes)) volumeMounts := deployment.Spec.Template.Spec.Containers[0].VolumeMounts expectedVolumeMounts := test.NewVolumeMountsWithTemplates(t.TLS) - Expect(volumeMounts).To(Equal(expectedVolumeMounts)) + Expect(volumeMounts).To(ConsistOf(expectedVolumeMounts)) +} + +func (t *cryostatTestInput) checkDeploymentHasAuthProperties() { + deployment := &appsv1.Deployment{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat", Namespace: "default"}, deployment) + Expect(err).ToNot(HaveOccurred()) + + volumes := deployment.Spec.Template.Spec.Volumes + expectedVolumes := test.NewVolumeWithAuthProperties(t.TLS) + Expect(volumes).To(ConsistOf(expectedVolumes)) + + coreContainer := deployment.Spec.Template.Spec.Containers[0] + + 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))) +} + +func (t *cryostatTestInput) checkDeploymentHasNoAuthProperties() { + deployment := &appsv1.Deployment{} + err := t.Client.Get(context.Background(), types.NamespacedName{Name: "cryostat", Namespace: "default"}, deployment) + Expect(err).ToNot(HaveOccurred()) + + volumes := deployment.Spec.Template.Spec.Volumes + expectedVolumes := test.NewVolumes(t.minimal, t.TLS) + Expect(volumes).ToNot(ContainElements(test.NewAuthPropertiesVolume())) + Expect(volumes).To(ConsistOf(expectedVolumes)) + + coreContainer := deployment.Spec.Template.Spec.Containers[0] + + volumeMounts := coreContainer.VolumeMounts + expectedVolumeMounts := test.NewCoreVolumeMounts(t.TLS) + Expect(volumeMounts).ToNot(ContainElement(test.NewAuthPropertiesVolumeMount())) + Expect(volumeMounts).To(ConsistOf(expectedVolumeMounts)) } func checkCoreContainer(container *corev1.Container, minimal bool, tls bool, externalTLS bool, - tag *string, openshift bool, reportsUrl string, resources corev1.ResourceRequirements) { + tag *string, openshift bool, reportsUrl string, authProps bool, resources corev1.ResourceRequirements) { Expect(container.Name).To(Equal("cryostat")) if tag == nil { Expect(container.Image).To(HavePrefix("quay.io/cryostat/cryostat:")) @@ -2062,7 +2119,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))) + Expect(container.Env).To(ConsistOf(test.NewCoreEnvironmentVariables(minimal, tls, externalTLS, openshift, reportsUrl, authProps))) 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/test/resources.go b/internal/test/resources.go index 641200b6b..3d528812f 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -369,6 +369,16 @@ func NewCryostatWithResources() *operatorv1beta1.Cryostat { return cr } +func NewCryostatWithAuthProperties() *operatorv1beta1.Cryostat { + cr := NewCryostat() + cr.Spec.AuthProperties = &operatorv1beta1.AuthorizationProperties{ + ConfigMapName: "authConfigMapName", + Filename: "auth.properties", + ClusterRoleName: "oauth-cluster-role", + } + return cr +} + func newPVCSpec(storageClass string, storageRequest string, accessModes ...corev1.PersistentVolumeAccessMode) *corev1.PersistentVolumeClaimSpec { return &corev1.PersistentVolumeClaimSpec{ @@ -1199,7 +1209,7 @@ func NewReportsPorts() []corev1.ContainerPort { } } -func NewCoreEnvironmentVariables(minimal bool, tls bool, externalTLS bool, openshift bool, reportsUrl string) []corev1.EnvVar { +func NewCoreEnvironmentVariables(minimal bool, tls bool, externalTLS bool, openshift bool, reportsUrl string, authProps bool) []corev1.EnvVar { envs := []corev1.EnvVar{ { Name: "CRYOSTAT_WEB_PORT", @@ -1324,9 +1334,16 @@ func NewCoreEnvironmentVariables(minimal bool, tls bool, externalTLS bool, opens Value: "cryostat", }, corev1.EnvVar{ - Name: "CRYOSTAT_OAUTH_ROLE", + Name: "CRYOSTAT_BASE_OAUTH_ROLE", Value: "cryostat-operator-oauth-client", }) + + if authProps { + envs = append(envs, corev1.EnvVar{ + Name: "CRYOSTAT_CUSTOM_OAUTH_ROLE", + Value: "oauth-cluster-role", + }) + } } if reportsUrl != "" { envs = append(envs, @@ -1576,6 +1593,19 @@ func NewVolumeMountsWithTemplates(tls bool) []corev1.VolumeMount { }) } +func NewVolumeMountsWithAuthProperties(tls bool) []corev1.VolumeMount { + return append(NewCoreVolumeMounts(tls), NewAuthPropertiesVolumeMount()) +} + +func NewAuthPropertiesVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{ + Name: "auth-properties-authConfigMapName", + ReadOnly: true, + MountPath: "/app/resources/io/cryostat/net/openshift/OpenShiftAuthManager.properties", + SubPath: "OpenShiftAuthManager.properties", + } +} + func NewCoreLivenessProbe(tls bool) *corev1.Probe { return &corev1.Probe{ ProbeHandler: newCoreProbeHandler(tls), @@ -1781,6 +1811,31 @@ func NewVolumesWithTemplates(tls bool) []corev1.Volume { }) } +func NewVolumeWithAuthProperties(tls bool) []corev1.Volume { + return append(NewVolumes(false, tls), NewAuthPropertiesVolume()) +} + +func NewAuthPropertiesVolume() corev1.Volume { + readOnlyMode := int32(0440) + return corev1.Volume{ + Name: "auth-properties-authConfigMapName", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "authConfigMapName", + }, + Items: []corev1.KeyToPath{ + { + Key: "auth.properties", + Path: "OpenShiftAuthManager.properties", + Mode: &readOnlyMode, + }, + }, + }, + }, + } +} + func newVolumes(minimal bool, tls bool, certProjections []corev1.VolumeProjection) []corev1.Volume { readOnlymode := int32(0440) volumes := []corev1.Volume{ @@ -2254,6 +2309,18 @@ func NewOtherTemplateConfigMap() *corev1.ConfigMap { } } +func NewAuthPropertiesConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "authConfigMapName", + Namespace: "default", + }, + Data: map[string]string{ + "auth.properties": "CRYOSTAT_RESOURCE=K8S_RESOURCE\nANOTHER_CRYOSTAT_RESOURCE=ANOTHER_K8S_RESOURCE", + }, + } +} + func NewNamespace() *corev1.Namespace { return &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{