diff --git a/apis/apps/v1alpha1/apimanager_types.go b/apis/apps/v1alpha1/apimanager_types.go index 3be1e98ed..0b575b293 100644 --- a/apis/apps/v1alpha1/apimanager_types.go +++ b/apis/apps/v1alpha1/apimanager_types.go @@ -84,6 +84,10 @@ type APIManagerSpec struct { PodDisruptionBudget *PodDisruptionBudgetSpec `json:"podDisruptionBudget,omitempty"` // +optional Monitoring *MonitoringSpec `json:"monitoring,omitempty"` + // +optional + SystemDatabaseTLSEnabled *bool `json:"systemDatabaseTLSEnabled,omitempty"` + // +optional + ZyncDatabaseTLSEnabled *bool `json:"zyncDatabaseTLSEnabled,omitempty"` } // APIManagerStatus defines the observed state of APIManager @@ -1438,6 +1442,24 @@ func (a *APIManager) GetApicastCustomPoliciesSecretRefs() []*v1.LocalObjectRefer return secretRefs } +func (a *APIManager) GetSystemDatabaseSecretRefs() []*v1.LocalObjectReference { + secretRefs := []*v1.LocalObjectReference{} + systemDatabaseSecretRef := v1.LocalObjectReference{ + Name: "system-database", + } + secretRefs = append(secretRefs, &systemDatabaseSecretRef) + return secretRefs +} + +func (a *APIManager) GetZyncSecretRefs() []*v1.LocalObjectReference { + secretRefs := []*v1.LocalObjectReference{} + zyncSecretRef := v1.LocalObjectReference{ + Name: "zync", + } + secretRefs = append(secretRefs, &zyncSecretRef) + return secretRefs +} + func (apimanager *APIManager) Get3scaleSecretRefs() []*v1.LocalObjectReference { secretRefs := []*v1.LocalObjectReference{} @@ -1463,6 +1485,16 @@ func (apimanager *APIManager) Get3scaleSecretRefs() []*v1.LocalObjectReference { secretRefs = append(secretRefs, apicastCustomPoliciesSecretRefs...) } + systemDatabaseSecretRefs := apimanager.GetSystemDatabaseSecretRefs() + if len(systemDatabaseSecretRefs) > 0 { + secretRefs = append(secretRefs, systemDatabaseSecretRefs...) + } + + zyncSecretRefs := apimanager.GetZyncSecretRefs() + if len(zyncSecretRefs) > 0 { + secretRefs = append(secretRefs, zyncSecretRefs...) + } + secretRefs = removeDuplicateSecretRefs(secretRefs) return secretRefs @@ -1672,3 +1704,11 @@ type APIManagerList struct { func init() { SchemeBuilder.Register(&APIManager{}, &APIManagerList{}) } + +func (apimanager *APIManager) IsSystemDatabaseTLSEnabled() bool { + return apimanager.Spec.SystemDatabaseTLSEnabled != nil && *apimanager.Spec.SystemDatabaseTLSEnabled +} + +func (apimanager *APIManager) IsZyncDatabaseTLSEnabled() bool { + return apimanager.Spec.ZyncDatabaseTLSEnabled != nil && *apimanager.Spec.ZyncDatabaseTLSEnabled +} diff --git a/apis/apps/v1alpha1/apimanager_types_test.go b/apis/apps/v1alpha1/apimanager_types_test.go index 8947cc501..8a8920c9d 100644 --- a/apis/apps/v1alpha1/apimanager_types_test.go +++ b/apis/apps/v1alpha1/apimanager_types_test.go @@ -263,7 +263,14 @@ func TestAPIManager_Get3scaleSecretRefs(t *testing.T) { }, }, }, - want: []*v1.LocalObjectReference{}, + want: []*v1.LocalObjectReference{ + { + Name: "system-database", + }, + { + Name: "zync", + }, + }, }, { name: "Apicast has secret refs", @@ -319,6 +326,12 @@ func TestAPIManager_Get3scaleSecretRefs(t *testing.T) { { Name: "custom-policy-1-secret", }, + { + Name: "system-database", + }, + { + Name: "zync", + }, }, }, } diff --git a/apis/apps/v1alpha1/zz_generated.deepcopy.go b/apis/apps/v1alpha1/zz_generated.deepcopy.go index 621ca8171..6b930ce45 100644 --- a/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -435,6 +435,16 @@ func (in *APIManagerSpec) DeepCopyInto(out *APIManagerSpec) { *out = new(MonitoringSpec) (*in).DeepCopyInto(*out) } + if in.SystemDatabaseTLSEnabled != nil { + in, out := &in.SystemDatabaseTLSEnabled, &out.SystemDatabaseTLSEnabled + *out = new(bool) + **out = **in + } + if in.ZyncDatabaseTLSEnabled != nil { + in, out := &in.ZyncDatabaseTLSEnabled, &out.ZyncDatabaseTLSEnabled + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIManagerSpec. diff --git a/bundle/manifests/apps.3scale.net_apimanagers.yaml b/bundle/manifests/apps.3scale.net_apimanagers.yaml index cf45088c6..4a91c7fdb 100644 --- a/bundle/manifests/apps.3scale.net_apimanagers.yaml +++ b/bundle/manifests/apps.3scale.net_apimanagers.yaml @@ -16603,6 +16603,8 @@ spec: type: array type: object type: object + systemDatabaseTLSEnabled: + type: boolean tenantName: type: string wildcardDomain: @@ -20010,6 +20012,8 @@ spec: type: array type: object type: object + zyncDatabaseTLSEnabled: + type: boolean required: - wildcardDomain type: object diff --git a/config/crd/bases/apps.3scale.net_apimanagers.yaml b/config/crd/bases/apps.3scale.net_apimanagers.yaml index 58db9ea5b..43420ac3a 100644 --- a/config/crd/bases/apps.3scale.net_apimanagers.yaml +++ b/config/crd/bases/apps.3scale.net_apimanagers.yaml @@ -17393,6 +17393,8 @@ spec: type: array type: object type: object + systemDatabaseTLSEnabled: + type: boolean tenantName: type: string wildcardDomain: @@ -20960,6 +20962,8 @@ spec: type: array type: object type: object + zyncDatabaseTLSEnabled: + type: boolean required: - wildcardDomain type: object diff --git a/doc/apimanager-reference.md b/doc/apimanager-reference.md index 5da63ae27..44054f90b 100644 --- a/doc/apimanager-reference.md +++ b/doc/apimanager-reference.md @@ -740,6 +740,11 @@ For Mysql: | URL | URL of the Porta database. Format: `mysql2://:@/`, where `` must be an already existing user in the external database with full permissions on the specified `` logical database and `` must be an already existing logical database in the external database.| Mandatory when the instance is managed externally. A default is only set when database is managed internally.
When managed internally:
`mysql2://root:@system-mysql/mysql`.| | DB_USER | Not used by 3scale components. Only used when the database is managed internally to create a new user granted with superuser permissions for the database specified in the `URL` field. | `mysql` | | DB_PASSWORD | Not used by 3scale components. Only used when the database is managed internally to create a new user granted with superuser permissions for the database specified in the `URL` field. | Autogenerated value | +| DATABASE_SSL_MODE | [Database SSL Mode](https://github.com/brianmario/mysql2?tab=readme-ov-file#ssltls-options) | Required to set TLS Database connection. Only for TLS | +| DB_SSL_CA | SSL CA certificate | Required to set TLS Database connection. Only for TLS | +| DB_SSL_CERT | SSL CERT certificate | Required to set TLS Database connection. Only for TLS | +DB_SSL_KEY | SSL Key | Required to set TLS Database connection. Only for TLS | + For Postgresql: @@ -748,6 +753,11 @@ For Postgresql: | URL | URL of the Porta database. Format: `postgresql://:@/`, where `` must be an already existing user in the external database with full permissions on the specified `` logical database and `` must be an already existing logical database in the external database.| Mandatory when the instance is managed externally. A default is only set when database is managed internally.
When managed internally:
`postgresql://system:@system-postgresql/system`.| | DB_USER | Not used by 3scale components. Only used when the database is managed internally to create a user with superuser power. | `system` | | DB_PASSWORD | Not used by 3scale components. Only used when the database is managed internally to create a user with superuser power. | Autogenerated value | +| DATABASE_SSL_MODE | [Database SSL Mode](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION) | Required to set TLS Database connection. Only for TLS | +| DB_SSL_CA | SSL CA certificate | Required to set TLS Database connection. Only for TLS | +| DB_SSL_CERT | SSL CERT certificate | Required to set TLS Database connection. Only for TLS | +DB_SSL_KEY | SSL Key | Required to set TLS Database connection. Only for TLS | + For Oracle: @@ -814,6 +824,11 @@ For Oracle: | ZYNC_DATABASE_PASSWORD | Database password associated to the user specified in the `DATABASE_URL` parameter | When the database is managed externally, this parameter is mandatory and must have the same value as the password part of the `DATABASE_URL` parameter in this secret. Otherwise the default value is an autogenerated value if not defined | | SECRET_KEY_BASE | Zync's application key generator to encrypt communications | Autogenerated value | | ZYNC_AUTHENTICATION_TOKEN | Authentication token used to authenticate System when calling Zync | Autogenerated value | +| DATABASE_SSL_MODE | [Database SSL Mode](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION) | Required to set TLS Database connection. Only for TLS | +| DB_SSL_CA | SSL CA certificate | Required to set TLS Database connection. Only for TLS | +| DB_SSL_CERT | SSL CERT certificate | Required to set TLS Database connection. Only for TLS | +DB_SSL_KEY | SSL Key | Required to set TLS Database connection. Only for TLS | + ### fileStorage-S3-credentials-secret diff --git a/doc/operator-user-guide.md b/doc/operator-user-guide.md index 6a538c431..11c972475 100644 --- a/doc/operator-user-guide.md +++ b/doc/operator-user-guide.md @@ -292,6 +292,48 @@ Secret name must be `zync`. See [Zync secret](apimanager-reference.md#zync) for reference. +#### TLS database configuration #### + +It is possible to connect to both the system-database and zync database via TLS provided these databases have TLS enabled. To enable TLS communication to these databases you will need to configure the ApiManager and the database secret. + +In ApiManager CR we set the boolean to enable TLS configuration for the respictive databases +- `spec.zyncDatabaseTLSEnabled: true` +- `spec.systemDatabaseTLSEnabled: true` + +We pass the cert files in via the respective secret i.e. system-database & zync + +You set the following values in the secret to connect to the database via TLS + +| Secret Key | Secret Value | +| --- | --- | +| DATABASE_SSL_MODE | string of the SSL mode for database connection | +| DB_SSL_CA | actual ca cert | +| DB_SSL_CERT | actual client cert | +| DB_SSL_KEY | actual client key | + +e.g. for system-database +```bash +oc create secret generic system-database \ + --from-literal=DATABASE_SSL_MODE=verify-ca \ + --from-literal=DATABASE_URL=postgresql://postgres:postgres@postgres-zync.postgres.svc.cluster.local/zync_production \ + --from-literal=ZYNC_DATABASE_PASSWORD=password \ + --from-file=DB_SSL_CA=rootCA.crt \ + --from-file=DB_SSL_CERT=client.crt \ + --from-file=DB_SSL_KEY=client.key +``` +e.g. for zync +```bash +oc create secret generic zync \ + --from-literal=DATABASE_SSL_MODE=verify-ca \ + --from-literal=DATABASE_URL=postgresql://postgres:postgres@postgres-zync.postgres.svc.cluster.local/zync_production \ + --from-literal=ZYNC_DATABASE_PASSWORD=password \ + --from-file=DB_SSL_CA=rootCA.crt \ + --from-file=DB_SSL_CERT=client.crt \ + --from-file=DB_SSL_KEY=client.key +``` + +Once these values have been set and are correct the operator will proceed to mount the certs into the related pods to enable client TLS communication. + #### S3 Filestorage Installation 3scale’s FileStorage being in a S3 service instead of in a PVC. diff --git a/pkg/3scale/amp/component/deployment_annotations.go b/pkg/3scale/amp/component/deployment_annotations.go index 6d0cc40ec..182515076 100644 --- a/pkg/3scale/amp/component/deployment_annotations.go +++ b/pkg/3scale/amp/component/deployment_annotations.go @@ -17,7 +17,7 @@ import ( func ComputeWatchedSecretAnnotations(ctx context.Context, client k8sclient.Client, deploymentName, watchNS string, component interface{}) (map[string]string, error) { // First get the initial annotations - uncheckedAnnotations, err := getWatchedSecretAnnotations(ctx, client, deploymentName, component) + uncheckedAnnotations, err := getWatchedSecretAnnotations(ctx, client, deploymentName, watchNS, component) if err != nil { return nil, err } @@ -74,7 +74,7 @@ func ComputeWatchedSecretAnnotations(ctx context.Context, client k8sclient.Clien return uncheckedAnnotations, nil // No difference with existing annotations so can return uncheckedAnnotations } -func getWatchedSecretAnnotations(ctx context.Context, client k8sclient.Client, deploymentName string, component interface{}) (map[string]string, error) { +func getWatchedSecretAnnotations(ctx context.Context, client k8sclient.Client, deploymentName string, namespace string, component interface{}) (map[string]string, error) { annotations := map[string]string{} switch c := component.(type) { @@ -192,6 +192,53 @@ func getWatchedSecretAnnotations(ctx context.Context, client k8sclient.Client, d } } } + case *System: + system := c + systemDatabase := &corev1.Secret{} + systemDatabaseSecretKey := k8sclient.ObjectKey{ + Name: SystemSecretSystemDatabaseSecretName, + Namespace: system.Options.Namespace, + } + err := client.Get(ctx, systemDatabaseSecretKey, systemDatabase) + if err != nil { + return nil, err + } + if helper.IsSecretWatchedBy3scale(systemDatabase) { + annotationKey := fmt.Sprintf("%s%s", SystemDatabaseSecretResverAnnotationPrefix, systemDatabase.Name) + annotations[annotationKey] = systemDatabase.ResourceVersion + } + + case *SystemSearchd: + systemDatabase := &corev1.Secret{} + systemDatabaseSecretKey := k8sclient.ObjectKey{ + Name: SystemSecretSystemDatabaseSecretName, + Namespace: namespace, + } + err := client.Get(ctx, systemDatabaseSecretKey, systemDatabase) + if err != nil { + return nil, err + } + if helper.IsSecretWatchedBy3scale(systemDatabase) { + annotationKey := fmt.Sprintf("%s%s", SystemDatabaseSecretResverAnnotationPrefix, systemDatabase.Name) + annotations[annotationKey] = systemDatabase.ResourceVersion + } + + case *Zync: + zync := c + zyncSecret := &corev1.Secret{} + zyncSecretKey := k8sclient.ObjectKey{ + Name: ZyncSecretName, + Namespace: zync.Options.Namespace, + } + err := client.Get(ctx, zyncSecretKey, zyncSecret) + if err != nil { + fmt.Printf("failed to find zync secret, yet to be create %s", err) + return nil, nil + } + if helper.IsSecretWatchedBy3scale(zyncSecret) { + annotationKey := fmt.Sprintf("%s%s", ZyncSecretResverAnnotationPrefix, zyncSecret.Name) + annotations[annotationKey] = zyncSecret.ResourceVersion + } default: return nil, fmt.Errorf("unrecognized component %s is not supported", deploymentName) @@ -223,6 +270,27 @@ func HasSecretHashChanged(ctx context.Context, client k8sclient.Client, deployme default: return false } + case *System: + switch { + case strings.HasPrefix(deploymentAnnotation, SystemDatabaseSecretResverAnnotationPrefix): + secretToCheckKey.Name = strings.TrimPrefix(deploymentAnnotation, SystemDatabaseSecretResverAnnotationPrefix) + default: + return false + } + case *SystemSearchd: + switch { + case strings.HasPrefix(deploymentAnnotation, SystemDatabaseSecretResverAnnotationPrefix): + secretToCheckKey.Name = strings.TrimPrefix(deploymentAnnotation, SystemDatabaseSecretResverAnnotationPrefix) + default: + return false + } + case *Zync: + switch { + case strings.HasPrefix(deploymentAnnotation, ZyncSecretResverAnnotationPrefix): + secretToCheckKey.Name = strings.TrimPrefix(deploymentAnnotation, ZyncSecretResverAnnotationPrefix) + default: + return false + } default: logger.Info(fmt.Sprintf("unrecognized component %s is not supported", c)) return false diff --git a/pkg/3scale/amp/component/system.go b/pkg/3scale/amp/component/system.go index df284b6bf..f2b644612 100644 --- a/pkg/3scale/amp/component/system.go +++ b/pkg/3scale/amp/component/system.go @@ -1,6 +1,8 @@ package component import ( + "context" + "sigs.k8s.io/controller-runtime/pkg/client" "sort" "strconv" @@ -23,6 +25,13 @@ const ( SystemSecretSystemDatabaseUserFieldName = "DB_USER" SystemSecretSystemDatabasePasswordFieldName = "DB_PASSWORD" SystemSecretSystemDatabaseRootPasswordFieldName = "DB_ROOT_PASSWORD" + SystemSecretDatabaseSslCa = "DATABASE_SSL_CA" + SystemSecretDatabaseSslCert = "DATABASE_SSL_CERT" + SystemSecretDatabaseSslKey = "DATABASE_SSL_KEY" + SystemSecretDatabaseSslMode = "DATABASE_SSL_MODE" + SystemSecretSslCa = "DB_SSL_CA" + SystemSecretSslCert = "DB_SSL_CERT" + SystemSecretSslKey = "DB_SSL_KEY" ) const ( @@ -114,6 +123,10 @@ const ( SystemAppDeveloperContainerMetricsPortName = "dev-metrics" ) +const ( + SystemDatabaseSecretResverAnnotationPrefix = "apimanager.apps.3scale.net/systemdatabase-secret-resource-version-" +) + const ( S3StsCredentialsSecretName = "s3-credentials" ) @@ -177,7 +190,6 @@ func (system *System) SystemRedisEnvVars() []v1.EnvVar { func (system *System) buildSystemBaseEnv() []v1.EnvVar { result := []v1.EnvVar{} - baseEnvConfigMapEnvs := system.getSystemBaseEnvsFromEnvConfigMap() result = append(result, baseEnvConfigMapEnvs...) @@ -206,6 +218,18 @@ func (system *System) buildSystemBaseEnv() []v1.EnvVar { helper.EnvVarFromSecret("MEMCACHE_SERVERS", SystemSecretSystemMemcachedSecretName, SystemSecretSystemMemcachedServersFieldName), ) + if system.Options.SystemDbTLSEnabled { + result = append(result, + helper.EnvVarFromSecretOptional("DB_SSL_CA", SystemSecretSystemDatabaseSecretName, SystemSecretSslCa), + helper.EnvVarFromSecretOptional("DB_SSL_CERT", SystemSecretSystemDatabaseSecretName, SystemSecretSslCert), + helper.EnvVarFromSecretOptional("DB_SSL_KEY", SystemSecretSystemDatabaseSecretName, SystemSecretSslKey), + helper.EnvVarFromSecretOptional("DATABASE_SSL_MODE", SystemSecretSystemDatabaseSecretName, SystemSecretDatabaseSslMode), + helper.EnvVarFromValue("DATABASE_SSL_CA", helper.TlsCertPresent("DATABASE_SSL_CA", SystemSecretSystemDatabaseSecretName, system.Options.SystemDbTLSEnabled)), + helper.EnvVarFromValue("DATABASE_SSL_CERT", helper.TlsCertPresent("DATABASE_SSL_CERT", SystemSecretSystemDatabaseSecretName, system.Options.SystemDbTLSEnabled)), + helper.EnvVarFromValue("DATABASE_SSL_KEY", helper.TlsCertPresent("DATABASE_SSL_KEY", SystemSecretSystemDatabaseSecretName, system.Options.SystemDbTLSEnabled)), + ) + } + result = append(result, system.SystemRedisEnvVars()...) result = append(result, system.BackendRedisEnvVars()...) bckServiceEndpointEnv := helper.EnvVarFromSecret("BACKEND_URL", BackendSecretBackendListenerSecretName, BackendSecretBackendListenerServiceEndpointFieldName) @@ -509,6 +533,39 @@ func (system *System) appPodVolumes() []v1.Volume { } res = append(res, systemConfigVolume) + if system.Options.SystemDbTLSEnabled { + systemTlsVolume := v1.Volume{ + Name: "tls-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: SystemSecretSystemDatabaseSecretName, // Name of the secret containing the TLS certs + Items: []v1.KeyToPath{ + { + Key: SystemSecretSslCa, + Path: "ca.crt", // Map the secret key to the ca.crt file in the container + }, + { + Key: SystemSecretSslCert, + Path: "tls.crt", // Map the secret key to the tls.crt file in the container + }, + { + Key: SystemSecretSslKey, + Path: "tls.key", // Map the secret key to the tls.key file in the container + }, + }, + }, + }, + } + res = append(res, systemTlsVolume) + + systemWritableTlsVolume := v1.Volume{ + Name: "writable-tls", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + } + res = append(res, systemWritableTlsVolume) + } if system.Options.S3FileStorageOptions != nil && system.Options.S3FileStorageOptions.STSEnabled { s3CredsProjectedVolume := v1.Volume{ @@ -533,7 +590,12 @@ func (system *System) appPodVolumes() []v1.Volume { return res } -func (system *System) AppDeployment(containerImage string) *k8sappsv1.Deployment { +func (system *System) AppDeployment(ctx context.Context, k8sclient client.Client, containerImage string) (*k8sappsv1.Deployment, error) { + watchedSecretAnnotations, err := ComputeWatchedSecretAnnotations(ctx, k8sclient, SystemAppDeploymentName, system.Options.Namespace, system) + if err != nil { + return nil, err + } + return &k8sappsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: reconcilers.DeploymentAPIVersion, Kind: reconcilers.DeploymentKind}, ObjectMeta: metav1.ObjectMeta{ @@ -564,12 +626,13 @@ func (system *System) AppDeployment(containerImage string) *k8sappsv1.Deployment Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: system.Options.AppPodTemplateLabels, - Annotations: system.Options.AppPodTemplateAnnotations, + Annotations: system.appPodAnnotations(watchedSecretAnnotations), }, Spec: v1.PodSpec{ - Affinity: system.Options.AppAffinity, - Tolerations: system.Options.AppTolerations, - Volumes: system.appPodVolumes(), + Affinity: system.Options.AppAffinity, + Tolerations: system.Options.AppTolerations, + Volumes: system.appPodVolumes(), + InitContainers: system.systemInit(containerImage), Containers: []v1.Container{ { Name: SystemAppMasterContainerName, @@ -728,7 +791,7 @@ func (system *System) AppDeployment(containerImage string) *k8sappsv1.Deployment }, }, }, - } + }, nil } func (system *System) AppPreHookJob(containerImage string, currentSystemAppGeneration int64) *batchv1.Job { @@ -750,7 +813,8 @@ func (system *System) AppPreHookJob(containerImage string, currentSystemAppGener Completions: &completions, Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ - Volumes: system.appPodVolumes(), + Volumes: system.appPodVolumes(), + InitContainers: system.systemInit(containerImage), Containers: []v1.Container{ { Name: SystemAppPreHookJobName, @@ -790,7 +854,8 @@ func (system *System) AppPostHookJob(containerImage string, currentSystemAppGene Completions: &completions, Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ - Volumes: system.appPodVolumes(), + Volumes: system.appPodVolumes(), + InitContainers: system.systemInit(containerImage), Containers: []v1.Container{ { Name: SystemAppPostHookJobName, @@ -861,6 +926,39 @@ func (system *System) SidekiqPodVolumes() []v1.Volume { res = append(res, systemConfigVolume) + if system.Options.SystemDbTLSEnabled { + systemTlsVolume := v1.Volume{ + Name: "tls-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: SystemSecretSystemDatabaseSecretName, // Name of the secret containing the TLS certs + Items: []v1.KeyToPath{ + { + Key: SystemSecretSslCa, + Path: "ca.crt", // Map the secret key to the ca.crt file in the container + }, + { + Key: SystemSecretSslCert, + Path: "tls.crt", // Map the secret key to the tls.crt file in the container + }, + { + Key: SystemSecretSslKey, + Path: "tls.key", // Map the secret key to the tls.key file in the container + }, + }, + }, + }, + } + res = append(res, systemTlsVolume) + + systemWritableTlsVolume := v1.Volume{ + Name: "writable-tls", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + } + res = append(res, systemWritableTlsVolume) + } if system.Options.S3FileStorageOptions != nil && system.Options.S3FileStorageOptions.STSEnabled { s3CredsProjectedVolume := v1.Volume{ Name: S3StsCredentialsSecretName, @@ -883,7 +981,12 @@ func (system *System) SidekiqPodVolumes() []v1.Volume { return res } -func (system *System) SidekiqDeployment(containerImage string) *k8sappsv1.Deployment { +func (system *System) SidekiqDeployment(ctx context.Context, k8sclient client.Client, containerImage string) (*k8sappsv1.Deployment, error) { + watchedSecretAnnotations, err := ComputeWatchedSecretAnnotations(ctx, k8sclient, SystemSidekiqName, system.Options.Namespace, system) + if err != nil { + return nil, err + } + return &k8sappsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: reconcilers.DeploymentAPIVersion, Kind: reconcilers.DeploymentKind}, ObjectMeta: metav1.ObjectMeta{ @@ -914,24 +1017,13 @@ func (system *System) SidekiqDeployment(containerImage string) *k8sappsv1.Deploy Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: system.Options.SidekiqPodTemplateLabels, - Annotations: system.Options.SideKiqPodTemplateAnnotations, + Annotations: system.sidekiqPodAnnotations(watchedSecretAnnotations), }, Spec: v1.PodSpec{ - Affinity: system.Options.SidekiqAffinity, - Tolerations: system.Options.SidekiqTolerations, - Volumes: system.SidekiqPodVolumes(), - InitContainers: []v1.Container{ - { - Name: SystemSideKiqInitContainerName, - Image: containerImage, - Command: []string{ - "bash", - "-c", - "bundle exec sh -c \"until rake boot:redis && curl --output /dev/null --silent --fail --head http://system-master:3000/status; do sleep $SLEEP_SECONDS; done\"", - }, - Env: append(system.SystemRedisEnvVars(), helper.EnvVarFromValue("SLEEP_SECONDS", "1")), - }, - }, + Affinity: system.Options.SidekiqAffinity, + Tolerations: system.Options.SidekiqTolerations, + Volumes: system.SidekiqPodVolumes(), + InitContainers: system.sidekiqInit(containerImage), Containers: []v1.Container{ { Name: SystemSidekiqName, @@ -962,7 +1054,7 @@ func (system *System) SidekiqDeployment(containerImage string) *k8sappsv1.Deploy }, }, }, - } + }, nil } func (system *System) systemStorageVolumeMount(readOnly bool) v1.VolumeMount { @@ -980,6 +1072,13 @@ func (system *System) systemConfigVolumeMount() v1.VolumeMount { MountPath: "/opt/system-extra-configs", } } +func (system *System) systemTlsVolumeMount() v1.VolumeMount { + return v1.VolumeMount{ + Name: "writable-tls", + ReadOnly: false, + MountPath: "/tls", + } +} func (system *System) s3CredsProjectedVolumeMount() v1.VolumeMount { return v1.VolumeMount{ @@ -1001,6 +1100,10 @@ func (system *System) appCommonContainerVolumeMounts(systemStorageReadonly bool) res = append(res, system.systemConfigVolumeMount()) + if system.Options.SystemDbTLSEnabled { + res = append(res, system.systemTlsVolumeMount()) + } + return res } @@ -1030,7 +1133,9 @@ func (system *System) sidekiqContainerVolumeMounts() []v1.VolumeMount { } res = append(res, systemTmpVolumeMount) res = append(res, system.systemConfigVolumeMount()) - + if system.Options.SystemDbTLSEnabled { + res = append(res, system.systemTlsVolumeMount()) + } if system.Options.S3FileStorageOptions != nil && system.Options.S3FileStorageOptions.STSEnabled { res = append(res, system.s3CredsProjectedVolumeMount()) } @@ -1341,3 +1446,117 @@ func (system *System) appDeveloperPorts() []v1.ContainerPort { return ports } + +func (system *System) systemInit(containerImage string) []v1.Container { + if system.Options.SystemDbTLSEnabled { + return []v1.Container{ + { + Name: "set-permissions", + Image: containerImage, // Minimal image for chmod + Command: []string{ + "sh", + "-c", + "cp /tls/* /writable-tls/ && chmod 0600 /writable-tls/*", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "tls-secret", + MountPath: "/tls", + ReadOnly: true, + }, + { + Name: "writable-tls", + MountPath: "/writable-tls", + ReadOnly: false, // Writable emptyDir volume + }, + }, + }, + } + } else { + return []v1.Container{} + } +} + +func (system *System) sidekiqInit(containerImage string) []v1.Container { + if system.Options.SystemDbTLSEnabled { + return []v1.Container{ + { + Name: "set-permissions", + Image: containerImage, // Minimal image for chmod + Command: []string{ + "sh", + "-c", + "cp /tls/* /writable-tls/ && chmod 0600 /writable-tls/*", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "tls-secret", + MountPath: "/tls", + ReadOnly: true, + }, + { + Name: "writable-tls", + MountPath: "/writable-tls", + ReadOnly: false, // Writable emptyDir volume + }, + }, + }, + { + Name: SystemSideKiqInitContainerName, + Image: containerImage, + Command: []string{ + "bash", + "-c", + "bundle exec sh -c \"until rake boot:redis && curl --insecure --output /dev/null --silent --fail --head http://system-master:3000/status; do sleep $SLEEP_SECONDS; done\"", + }, + Env: append(system.SystemRedisEnvVars(), helper.EnvVarFromValue("SLEEP_SECONDS", "1")), + }, + } + } else { + return []v1.Container{ + { + Name: SystemSideKiqInitContainerName, + Image: containerImage, + Command: []string{ + "bash", + "-c", + "bundle exec sh -c \"until rake boot:redis && curl --insecure --output /dev/null --silent --fail --head http://system-master:3000/status; do sleep $SLEEP_SECONDS; done\"", + }, + Env: append(system.SystemRedisEnvVars(), helper.EnvVarFromValue("SLEEP_SECONDS", "1")), + }, + } + } +} + +func (system *System) appPodAnnotations(watchedSecretAnnotations map[string]string) map[string]string { + annotations := system.Options.AppPodTemplateAnnotations + if annotations == nil { + annotations = make(map[string]string) + } + for key, val := range watchedSecretAnnotations { + annotations[key] = val + } + + for key, val := range system.Options.AppPodTemplateAnnotations { + annotations[key] = val + } + + return annotations +} + +func (system *System) sidekiqPodAnnotations(watchedSecretAnnotations map[string]string) map[string]string { + annotations := system.Options.SideKiqPodTemplateAnnotations + + if annotations == nil { + annotations = make(map[string]string) + } + for key, val := range watchedSecretAnnotations { + annotations[key] = val + } + + for key, val := range system.Options.SideKiqPodTemplateAnnotations { + annotations[key] = val + } + + return annotations +} diff --git a/pkg/3scale/amp/component/system_options.go b/pkg/3scale/amp/component/system_options.go index 665d37a0b..7fd9df454 100644 --- a/pkg/3scale/amp/component/system_options.go +++ b/pkg/3scale/amp/component/system_options.go @@ -88,6 +88,7 @@ type SystemOptions struct { SMTPLabels map[string]string `validate:"required"` SideKiqMetrics bool AppMetrics bool + SystemDbTLSEnabled bool IncludeOracleOptionalSettings bool diff --git a/pkg/3scale/amp/component/system_searchd.go b/pkg/3scale/amp/component/system_searchd.go index 94ddf5907..6f51569e4 100644 --- a/pkg/3scale/amp/component/system_searchd.go +++ b/pkg/3scale/amp/component/system_searchd.go @@ -1,7 +1,9 @@ package component import ( + "context" "github.com/3scale/3scale-operator/pkg/reconcilers" + "sigs.k8s.io/controller-runtime/pkg/client" k8sappsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -52,8 +54,12 @@ func (s *SystemSearchd) Service() *v1.Service { } } -func (s *SystemSearchd) Deployment(containerImage string) *k8sappsv1.Deployment { +func (s *SystemSearchd) Deployment(ctx context.Context, k8sclient client.Client, operatorNamespace string, containerImage string) (*k8sappsv1.Deployment, error) { var searchdReplicas int32 = 1 + watchedSecretAnnotations, err := ComputeWatchedSecretAnnotations(ctx, k8sclient, SystemSearchdDeploymentName, operatorNamespace, s) + if err != nil { + return nil, err + } return &k8sappsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: reconcilers.DeploymentAPIVersion, Kind: reconcilers.DeploymentKind}, @@ -74,35 +80,21 @@ func (s *SystemSearchd) Deployment(containerImage string) *k8sappsv1.Deployment Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: s.Options.PodTemplateLabels, - Annotations: s.Options.PodTemplateAnnotations, + Annotations: s.searchdPodAnnotations(watchedSecretAnnotations), }, Spec: v1.PodSpec{ + InitContainers: s.searchdInit(containerImage), Affinity: s.Options.Affinity, Tolerations: s.Options.Tolerations, ServiceAccountName: "amp", - Volumes: []v1.Volume{ - { - Name: SystemSearchdDBVolumeName, - VolumeSource: v1.VolumeSource{ - PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ - ClaimName: SystemSearchdPVCName, - ReadOnly: false, - }, - }, - }, - }, + Volumes: s.searchdVolume(), Containers: []v1.Container{ { Name: SystemSearchdDeploymentName, Image: containerImage, ImagePullPolicy: v1.PullIfNotPresent, - VolumeMounts: []v1.VolumeMount{ - { - ReadOnly: false, - Name: SystemSearchdDBVolumeName, - MountPath: "/var/lib/searchd", - }, - }, + //Env: s.commonSearchdEnvVars(), + VolumeMounts: s.searchDVolumeMounts(), LivenessProbe: &v1.Probe{ ProbeHandler: v1.ProbeHandler{ TCPSocket: &v1.TCPSocketAction{ @@ -132,7 +124,7 @@ func (s *SystemSearchd) Deployment(containerImage string) *k8sappsv1.Deployment }, }, }, - } + }, nil } func (s *SystemSearchd) PVC() *v1.PersistentVolumeClaim { @@ -178,6 +170,7 @@ func (s *SystemSearchd) ReindexingJob(containerImage string, system *System) *ba Completions: &completions, Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ + InitContainers: s.searchdInit(containerImage), Containers: []v1.Container{ { Name: SystemSearchdReindexJobName, @@ -186,8 +179,10 @@ func (s *SystemSearchd) ReindexingJob(containerImage string, system *System) *ba Env: system.buildSystemBaseEnv(), Resources: s.Options.ContainerResourceRequirements, ImagePullPolicy: v1.PullIfNotPresent, + VolumeMounts: s.searchdManticoreVolumeMounts(), }, }, + Volumes: s.searchdJobVolume(), RestartPolicy: v1.RestartPolicyNever, ServiceAccountName: "amp", PriorityClassName: s.Options.PriorityClassName, @@ -196,3 +191,182 @@ func (s *SystemSearchd) ReindexingJob(containerImage string, system *System) *ba }, } } + +func (s *SystemSearchd) searchdInit(containerImage string) []v1.Container { + if s.Options.SearchdDbTLSEnabled { + return []v1.Container{ + { + Name: "set-permissions", + Image: containerImage, // Minimal image for chmod + Command: []string{ + "sh", + "-c", + "cp /tls/* /writable-tls/ && chmod 0600 /writable-tls/*", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "tls-secret", + MountPath: "/tls", + ReadOnly: true, + }, + { + Name: "writable-tls", + MountPath: "/writable-tls", + ReadOnly: false, // Writable emptyDir volume + }, + }, + }, + } + } else { + return []v1.Container{} + } +} + +func (s *SystemSearchd) searchDVolumeMounts() []v1.VolumeMount { + if s.Options.SearchdDbTLSEnabled { + return []v1.VolumeMount{ + { + ReadOnly: false, + Name: SystemSearchdDBVolumeName, + MountPath: "/var/lib/searchd", + }, + { + Name: "writable-tls", // Reuse the same volume in the main container if needed + MountPath: "/tls", + ReadOnly: true, + }, + } + } else { + return []v1.VolumeMount{ + { + ReadOnly: false, + Name: SystemSearchdDBVolumeName, + MountPath: "/var/lib/searchd", + }, + } + } +} + +func (s *SystemSearchd) searchdManticoreVolumeMounts() []v1.VolumeMount { + if s.Options.SearchdDbTLSEnabled { + return []v1.VolumeMount{ + { + Name: "writable-tls", // Reuse the same volume in the main container if needed + MountPath: "/tls", + ReadOnly: true, + }, + } + } else { + return []v1.VolumeMount{} + } +} + +func (s *SystemSearchd) searchdJobVolume() []v1.Volume { + if s.Options.SearchdDbTLSEnabled { + return []v1.Volume{ + { + Name: "tls-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: SystemSecretSystemDatabaseSecretName, // Name of the secret containing the TLS certs + Items: []v1.KeyToPath{ + { + Key: SystemSecretSslCa, + Path: "ca.crt", // Map the secret key to the ca.crt file in the container + }, + { + Key: SystemSecretSslCert, + Path: "tls.crt", // Map the secret key to the tls.crt file in the container + }, + { + Key: SystemSecretSslKey, + Path: "tls.key", // Map the secret key to the tls.key file in the container + }, + }, + }, + }, + }, + { + Name: "writable-tls", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + } + } else { + return []v1.Volume{} + } +} + +func (s *SystemSearchd) searchdVolume() []v1.Volume { + if s.Options.SearchdDbTLSEnabled { + return []v1.Volume{ + { + Name: SystemSearchdDBVolumeName, + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: SystemSearchdPVCName, + ReadOnly: false, + }, + }, + }, + { + Name: "tls-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: SystemSecretSystemDatabaseSecretName, // Name of the secret containing the TLS certs + Items: []v1.KeyToPath{ + { + Key: SystemSecretSslCa, + Path: "ca.crt", // Map the secret key to the ca.crt file in the container + }, + { + Key: SystemSecretSslCert, + Path: "tls.crt", // Map the secret key to the tls.crt file in the container + }, + { + Key: SystemSecretSslKey, + Path: "tls.key", // Map the secret key to the tls.key file in the container + }, + }, + }, + }, + }, + { + Name: "writable-tls", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + } + } else { + return []v1.Volume{ + { + Name: SystemSearchdDBVolumeName, + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: SystemSearchdPVCName, + ReadOnly: false, + }, + }, + }, + } + } +} + +func (s *SystemSearchd) searchdPodAnnotations(watchedSecretAnnotations map[string]string) map[string]string { + annotations := s.Options.PodTemplateAnnotations + + if annotations == nil { + annotations = make(map[string]string) + } + for key, val := range watchedSecretAnnotations { + annotations[key] = val + } + + for key, val := range s.Options.PodTemplateAnnotations { + annotations[key] = val + } + + return annotations +} diff --git a/pkg/3scale/amp/component/system_searchd_options.go b/pkg/3scale/amp/component/system_searchd_options.go index 93760ae82..42de1ebe2 100644 --- a/pkg/3scale/amp/component/system_searchd_options.go +++ b/pkg/3scale/amp/component/system_searchd_options.go @@ -26,6 +26,8 @@ type SystemSearchdOptions struct { PriorityClassName string `validate:"-"` TopologySpreadConstraints []v1.TopologySpreadConstraint `validate:"-"` PodTemplateAnnotations map[string]string `validate:"-"` + + SearchdDbTLSEnabled bool } func NewSystemSearchdOptions() *SystemSearchdOptions { diff --git a/pkg/3scale/amp/component/zync.go b/pkg/3scale/amp/component/zync.go index 47a8e8c69..d09437d35 100644 --- a/pkg/3scale/amp/component/zync.go +++ b/pkg/3scale/amp/component/zync.go @@ -1,15 +1,16 @@ package component import ( + "context" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" - k8sappsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -25,6 +26,10 @@ const ( ZyncSecretDatabaseURLFieldName = "DATABASE_URL" ZyncSecretDatabasePasswordFieldName = "ZYNC_DATABASE_PASSWORD" ZyncSecretAuthenticationTokenFieldName = "ZYNC_AUTHENTICATION_TOKEN" + ZyncSecretDatabaseSslMode = "DATABASE_SSL_MODE" + ZyncSecretSslCa = "DB_SSL_CA" + ZyncSecretSslCert = "DB_SSL_CERT" + ZyncSecretSslKey = "DB_SSL_KEY" ) const ( @@ -32,6 +37,10 @@ const ( ZyncQueMetricsPort = 9394 ) +const ( + ZyncSecretResverAnnotationPrefix = "apimanager.apps.3scale.net/zync-secret-resource-version-" +) + type Zync struct { Options *ZyncOptions } @@ -41,6 +50,29 @@ func NewZync(options *ZyncOptions) *Zync { } func (zync *Zync) Secret() *v1.Secret { + if zync.Options.ZyncDbTLSEnabled { + return &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ZyncSecretName, + Labels: zync.Options.CommonZyncSecretLabels, + }, + StringData: map[string]string{ + ZyncSecretKeyBaseFieldName: zync.Options.SecretKeyBase, + ZyncSecretDatabaseURLFieldName: zync.Options.DatabaseURL, + ZyncSecretDatabasePasswordFieldName: zync.Options.DatabasePassword, + ZyncSecretAuthenticationTokenFieldName: zync.Options.AuthenticationToken, + ZyncSecretDatabaseSslMode: zync.Options.DatabaseSslMode, + ZyncSecretSslCa: zync.Options.DatabaseSslCa, + ZyncSecretSslCert: zync.Options.DatabaseSslCert, + ZyncSecretSslKey: zync.Options.DatabaseSslKey, + }, + Type: v1.SecretTypeOpaque, + } + } return &v1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", @@ -48,7 +80,7 @@ func (zync *Zync) Secret() *v1.Secret { }, ObjectMeta: metav1.ObjectMeta{ Name: ZyncSecretName, - Labels: zync.Options.CommonLabels, + Labels: zync.Options.CommonZyncSecretLabels, }, StringData: map[string]string{ ZyncSecretKeyBaseFieldName: zync.Options.SecretKeyBase, @@ -164,7 +196,12 @@ func (zync *Zync) QueRole() *rbacv1.Role { } } -func (zync *Zync) Deployment(containerImage string) *k8sappsv1.Deployment { +func (zync *Zync) Deployment(ctx context.Context, k8sclient client.Client, containerImage string) (*k8sappsv1.Deployment, error) { + watchedSecretAnnotations, err := ComputeWatchedSecretAnnotations(ctx, k8sclient, ZyncName, zync.Options.Namespace, zync) + if err != nil { + return nil, err + } + return &k8sappsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: reconcilers.DeploymentAPIVersion, Kind: reconcilers.DeploymentKind}, ObjectMeta: metav1.ObjectMeta{ @@ -181,40 +218,13 @@ func (zync *Zync) Deployment(containerImage string) *k8sappsv1.Deployment { Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: zync.Options.ZyncPodTemplateLabels, - Annotations: zync.Options.ZyncPodTemplateAnnotations, + Annotations: zync.zyncPodAnnotations(watchedSecretAnnotations), }, Spec: v1.PodSpec{ Affinity: zync.Options.ZyncAffinity, Tolerations: zync.Options.ZyncTolerations, ServiceAccountName: "amp", - InitContainers: []v1.Container{ - { - Name: ZyncInitContainerName, - Image: containerImage, - Command: []string{ - "bash", - "-c", - "bundle exec sh -c \"until rake boot:db; do sleep $SLEEP_SECONDS; done\"", - }, - Env: []v1.EnvVar{ - { - Name: "SLEEP_SECONDS", - Value: "1", - }, - { - Name: "DATABASE_URL", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: ZyncSecretName, - }, - Key: ZyncSecretDatabaseURLFieldName, - }, - }, - }, - }, - }, - }, + InitContainers: zync.zyncInit(containerImage), Containers: []v1.Container{ { Name: ZyncName, @@ -249,18 +259,56 @@ func (zync *Zync) Deployment(containerImage string) *k8sappsv1.Deployment { SuccessThreshold: 1, FailureThreshold: 3, }, - Resources: zync.Options.ContainerResourceRequirements, + Resources: zync.Options.ContainerResourceRequirements, + VolumeMounts: zync.zyncVolumeMount(), }, }, + Volumes: zync.zyncVolume(), PriorityClassName: zync.Options.ZyncPriorityClassName, TopologySpreadConstraints: zync.Options.ZyncTopologySpreadConstraints, }, }, }, - } + }, nil } func (zync *Zync) commonZyncEnvVars() []v1.EnvVar { + if zync.Options.ZyncDbTLSEnabled { + return []v1.EnvVar{ + helper.EnvVarFromValue("RAILS_LOG_TO_STDOUT", "true"), + helper.EnvVarFromValue("RAILS_ENV", "production"), + helper.EnvVarFromSecret("DATABASE_URL", "zync", "DATABASE_URL"), + helper.EnvVarFromSecret("SECRET_KEY_BASE", "zync", "SECRET_KEY_BASE"), + helper.EnvVarFromSecret("ZYNC_AUTHENTICATION_TOKEN", "zync", "ZYNC_AUTHENTICATION_TOKEN"), + // SSL certs from secret + helper.EnvVarFromSecretOptional("DB_SSL_CA", ZyncSecretName, ZyncSecretSslCa), + helper.EnvVarFromSecretOptional("DB_SSL_CERT", ZyncSecretName, ZyncSecretSslCert), + helper.EnvVarFromSecretOptional("DB_SSL_KEY", ZyncSecretName, ZyncSecretSslKey), + helper.EnvVarFromSecretOptional("DATABASE_SSL_MODE", ZyncSecretName, ZyncSecretDatabaseSslMode), + // SSL mount pat env vars + helper.EnvVarFromValue("DATABASE_SSL_CA", helper.TlsCertPresent("DATABASE_SSL_CA", ZyncSecretName, zync.Options.ZyncDbTLSEnabled)), + helper.EnvVarFromValue("DATABASE_SSL_CERT", helper.TlsCertPresent("DATABASE_SSL_CERT", ZyncSecretName, zync.Options.ZyncDbTLSEnabled)), + helper.EnvVarFromValue("DATABASE_SSL_KEY", helper.TlsCertPresent("DATABASE_SSL_KEY", ZyncSecretName, zync.Options.ZyncDbTLSEnabled)), + { + Name: "POD_NAME", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.name", + APIVersion: "v1", + }, + }, + }, + { + Name: "POD_NAMESPACE", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + APIVersion: "v1", + }, + }, + }, + } + } return []v1.EnvVar{ helper.EnvVarFromValue("RAILS_LOG_TO_STDOUT", "true"), helper.EnvVarFromValue("RAILS_ENV", "production"), @@ -287,7 +335,12 @@ func (zync *Zync) commonZyncEnvVars() []v1.EnvVar { }, } } -func (zync *Zync) QueDeployment(containerImage string) *k8sappsv1.Deployment { +func (zync *Zync) QueDeployment(ctx context.Context, k8sclient client.Client, containerImage string) (*k8sappsv1.Deployment, error) { + watchedSecretAnnotations, err := ComputeWatchedSecretAnnotations(ctx, k8sclient, ZyncQueDeploymentName, zync.Options.Namespace, zync) + if err != nil { + return nil, err + } + return &k8sappsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: reconcilers.DeploymentAPIVersion, Kind: reconcilers.DeploymentKind}, ObjectMeta: metav1.ObjectMeta{ @@ -317,7 +370,7 @@ func (zync *Zync) QueDeployment(containerImage string) *k8sappsv1.Deployment { Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: zync.Options.ZyncQuePodTemplateLabels, - Annotations: zync.Options.ZyncQuePodTemplateAnnotations, + Annotations: zync.zyncQuePodAnnotations(watchedSecretAnnotations), }, Spec: v1.PodSpec{ Affinity: zync.Options.ZyncQueAffinity, @@ -325,6 +378,7 @@ func (zync *Zync) QueDeployment(containerImage string) *k8sappsv1.Deployment { ServiceAccountName: "zync-que-sa", RestartPolicy: v1.RestartPolicyAlways, TerminationGracePeriodSeconds: &[]int64{30}[0], + InitContainers: zync.zyncQueInit(containerImage), Containers: []v1.Container{ { Name: "que", @@ -353,16 +407,18 @@ func (zync *Zync) QueDeployment(containerImage string) *k8sappsv1.Deployment { Protocol: v1.ProtocolTCP, }, }, - Resources: zync.Options.QueContainerResourceRequirements, - Env: zync.commonZyncEnvVars(), + Resources: zync.Options.QueContainerResourceRequirements, + Env: zync.commonZyncEnvVars(), + VolumeMounts: zync.zyncVolumeMount(), }, }, + Volumes: zync.zyncVolume(), PriorityClassName: zync.Options.ZyncQuePriorityClassName, TopologySpreadConstraints: zync.Options.ZyncQueTopologySpreadConstraints, }, }, }, - } + }, nil } func (zync *Zync) DatabaseDeployment(containerImage string) *k8sappsv1.Deployment { @@ -568,3 +624,207 @@ func (zync *Zync) zyncPorts() []v1.ContainerPort { return ports } + +func (zync *Zync) zyncInit(containerImage string) []v1.Container { + + if zync.Options.ZyncDbTLSEnabled { + return []v1.Container{ + { + Name: "set-permissions", + Image: containerImage, + Command: []string{ + "sh", + "-c", + "cp /tls/* /writable-tls/ && chmod 0600 /writable-tls/*", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "tls-secret", + MountPath: "/tls", + ReadOnly: true, + }, + { + Name: "writable-tls", + MountPath: "/writable-tls", + ReadOnly: false, // Writable emptyDir volume + }, + }, + }, + { + Name: ZyncInitContainerName, + Image: containerImage, + Command: []string{ + "bash", + "-c", + "bundle exec sh -c \"until rake boot:db; do sleep $SLEEP_SECONDS; done\"", + }, + Env: []v1.EnvVar{ + { + Name: "SLEEP_SECONDS", + Value: "1", + }, + { + Name: "DATABASE_URL", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: ZyncSecretName, + }, + Key: ZyncSecretDatabaseURLFieldName, + }, + }, + }, + helper.EnvVarFromSecretOptional("DATABASE_SSL_MODE", ZyncSecretName, "DATABASE_SSL_MODE"), + helper.EnvVarFromValue("DATABASE_SSL_CA", helper.TlsCertPresent("DATABASE_SSL_CA", ZyncSecretName, zync.Options.ZyncDbTLSEnabled)), + helper.EnvVarFromValue("DATABASE_SSL_CERT", helper.TlsCertPresent("DATABASE_SSL_CERT", ZyncSecretName, zync.Options.ZyncDbTLSEnabled)), + helper.EnvVarFromValue("DATABASE_SSL_KEY", helper.TlsCertPresent("DATABASE_SSL_KEY", ZyncSecretName, zync.Options.ZyncDbTLSEnabled)), + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "writable-tls", // Reuse the same volume in the main container if needed + MountPath: "/tls", + ReadOnly: true, + }, + }, + }, + } + } else { + return []v1.Container{ + { + Name: ZyncInitContainerName, + Image: containerImage, + Command: []string{ + "bash", + "-c", + "bundle exec sh -c \"until rake boot:db; do sleep $SLEEP_SECONDS; done\"", + }, + Env: []v1.EnvVar{ + { + Name: "SLEEP_SECONDS", + Value: "1", + }, + { + Name: "DATABASE_URL", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: ZyncSecretName, + }, + Key: ZyncSecretDatabaseURLFieldName, + }, + }, + }, + }, + }, + } + } +} + +func (zync *Zync) zyncVolumeMount() []v1.VolumeMount { + if zync.Options.ZyncDbTLSEnabled { + return []v1.VolumeMount{ + { + Name: "writable-tls", // Reuse the same volume in the main container if needed + MountPath: "/tls", + ReadOnly: true, + }, + } + } else { + return []v1.VolumeMount{} + } +} + +func (zync *Zync) zyncVolume() []v1.Volume { + if zync.Options.ZyncDbTLSEnabled { + return []v1.Volume{ + { + Name: "tls-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: ZyncSecretName, // Name of the secret containing the TLS certs + Items: []v1.KeyToPath{ + { + Key: ZyncSecretSslCa, + Path: "ca.crt", // Map the secret key to the ca.crt file in the container + }, + { + Key: ZyncSecretSslCert, + Path: "tls.crt", // Map the secret key to the tls.crt file in the container + }, + { + Key: ZyncSecretSslKey, + Path: "tls.key", // Map the secret key to the tls.key file in the container + }, + }, + }, + }, + }, + { + Name: "writable-tls", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + } + } else { + return []v1.Volume{} + } +} + +func (zync *Zync) zyncQueInit(image string) []v1.Container { + if zync.Options.ZyncDbTLSEnabled { + return []v1.Container{ + { + Name: "set-permissions", + Image: "quay.io/openshift/origin-cli:4.7", // Minimal image for chmod + Command: []string{ + "sh", + "-c", + "cp /tls/* /writable-tls/ && chmod 0600 /writable-tls/*", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "tls-secret", + MountPath: "/tls", + ReadOnly: true, + }, + { + Name: "writable-tls", + MountPath: "/writable-tls", + ReadOnly: false, // Writable emptyDir volume + }, + }, + }, + } + } else { + return []v1.Container{} + } +} + +func (zync *Zync) zyncPodAnnotations(watchedSecretAnnotations map[string]string) map[string]string { + annotations := zync.Options.ZyncPodTemplateAnnotations + + for key, val := range watchedSecretAnnotations { + annotations[key] = val + } + + for key, val := range zync.Options.ZyncPodTemplateAnnotations { + annotations[key] = val + } + + return annotations +} + +func (zync *Zync) zyncQuePodAnnotations(watchedSecretAnnotations map[string]string) map[string]string { + annotations := zync.Options.ZyncQuePodTemplateAnnotations + + for key, val := range watchedSecretAnnotations { + annotations[key] = val + } + + for key, val := range zync.Options.ZyncQuePodTemplateAnnotations { + annotations[key] = val + } + + return annotations +} diff --git a/pkg/3scale/amp/component/zync_options.go b/pkg/3scale/amp/component/zync_options.go index acfff3824..e745ef0df 100644 --- a/pkg/3scale/amp/component/zync_options.go +++ b/pkg/3scale/amp/component/zync_options.go @@ -22,6 +22,11 @@ type ZyncOptions struct { SecretKeyBase string `validate:"required"` ZyncReplicas int32 ZyncQueReplicas int32 + DatabaseSslMode string + DatabaseSslCa string + DatabaseSslCert string + DatabaseSslKey string + ZyncDbTLSEnabled bool ZyncAffinity *v1.Affinity `validate:"-"` ZyncTolerations []v1.Toleration `validate:"-"` @@ -57,7 +62,8 @@ type ZyncOptions struct { // Those objects are namespaced. However, objects includes labels, rules and expressions // that need namespace filtering because they are "global" once imported // to the prometheus or grafana services. - Namespace string `validate:"required"` + Namespace string `validate:"required"` + CommonZyncSecretLabels map[string]string } func NewZyncOptions() *ZyncOptions { @@ -85,6 +91,10 @@ func DefaultZyncDatabaseURL(password string) string { return fmt.Sprintf("postgresql://zync:%s@zync-database:5432/zync_production", password) } +func DefaultZyncSslEmpty() string { + return fmt.Sprintf("") +} + func DefaultZyncContainerResourceRequirements() v1.ResourceRequirements { return v1.ResourceRequirements{ Limits: v1.ResourceList{ diff --git a/pkg/3scale/amp/operator/apicast_reconciler.go b/pkg/3scale/amp/operator/apicast_reconciler.go index ef2a40083..806678d35 100644 --- a/pkg/3scale/amp/operator/apicast_reconciler.go +++ b/pkg/3scale/amp/operator/apicast_reconciler.go @@ -768,6 +768,22 @@ func (r *ApicastReconciler) getSecretUIDs(ctx context.Context) (map[string]strin } } + // system-database + + if r.apiManager.IsSystemDatabaseTLSEnabled() { + secretKeys = append(secretKeys, client.ObjectKey{ + Name: component.SystemSecretSystemDatabaseSecretName, + Namespace: r.apiManager.Namespace, + }) + } + // zync + if r.apiManager.IsZyncDatabaseTLSEnabled() { + secretKeys = append(secretKeys, client.ObjectKey{ + Name: component.ZyncSecretName, + Namespace: r.apiManager.Namespace, + }) + } + uidMap := map[string]string{} for idx := range secretKeys { secret := &v1.Secret{} diff --git a/pkg/3scale/amp/operator/system_options_provider.go b/pkg/3scale/amp/operator/system_options_provider.go index 8805fe7da..c3232c141 100644 --- a/pkg/3scale/amp/operator/system_options_provider.go +++ b/pkg/3scale/amp/operator/system_options_provider.go @@ -49,6 +49,7 @@ func (s *SystemOptionsProvider) GetSystemOptions() (*component.SystemOptions, er s.options.DeveloperUILabels = s.developerUILabels() s.options.MemcachedLabels = s.memcachedLabels() s.options.SMTPLabels = s.smtpLabels() + s.setSystemDBTLSEabled() err := s.setSecretBasedOptions() if err != nil { @@ -628,3 +629,7 @@ func (s *SystemOptionsProvider) setPodTemplateAnnotations() { s.options.AppPodTemplateAnnotations = s.apimanager.Spec.System.AppSpec.Annotations s.options.SideKiqPodTemplateAnnotations = s.apimanager.Spec.System.SidekiqSpec.Annotations } + +func (s *SystemOptionsProvider) setSystemDBTLSEabled() { + s.options.SystemDbTLSEnabled = s.apimanager.IsSystemDatabaseTLSEnabled() +} diff --git a/pkg/3scale/amp/operator/system_reconciler.go b/pkg/3scale/amp/operator/system_reconciler.go index 86a8882fd..26a3cafe8 100644 --- a/pkg/3scale/amp/operator/system_reconciler.go +++ b/pkg/3scale/amp/operator/system_reconciler.go @@ -207,7 +207,11 @@ func (r *SystemReconciler) Reconcile() (reconcile.Result, error) { if r.apiManager.Spec.System.AppSpec.Replicas != nil { systemAppDeploymentMutators = append(systemAppDeploymentMutators, reconcilers.DeploymentReplicasMutator) } - err = r.ReconcileDeployment(system.AppDeployment(ampImages.Options.SystemImage), reconcilers.DeploymentMutator(systemAppDeploymentMutators...)) + appDeployment, err := system.AppDeployment(r.Context(), r.Client(), ampImages.Options.SystemImage) + if err != nil { + return reconcile.Result{}, err + } + err = r.ReconcileDeployment(appDeployment, reconcilers.DeploymentMutator(systemAppDeploymentMutators...)) if err != nil { return reconcile.Result{}, err } @@ -263,7 +267,11 @@ func (r *SystemReconciler) Reconcile() (reconcile.Result, error) { sidekiqDeploymentMutators = append(sidekiqDeploymentMutators, reconcilers.DeploymentReplicasMutator) } - err = r.ReconcileDeployment(system.SidekiqDeployment(ampImages.Options.SystemImage), reconcilers.DeploymentMutator(sidekiqDeploymentMutators...)) + sidekiqDeployment, err := system.SidekiqDeployment(r.Context(), r.Client(), ampImages.Options.SystemImage) + if err != nil { + return reconcile.Result{}, err + } + err = r.ReconcileDeployment(sidekiqDeployment, reconcilers.DeploymentMutator(sidekiqDeploymentMutators...)) if err != nil { return reconcile.Result{}, err } diff --git a/pkg/3scale/amp/operator/system_reconciler_test.go b/pkg/3scale/amp/operator/system_reconciler_test.go index a24eaa425..3a1a64d36 100644 --- a/pkg/3scale/amp/operator/system_reconciler_test.go +++ b/pkg/3scale/amp/operator/system_reconciler_test.go @@ -2,6 +2,7 @@ package operator import ( "context" + "fmt" "testing" appsv1alpha1 "github.com/3scale/3scale-operator/apis/apps/v1alpha1" @@ -35,7 +36,6 @@ func TestSystemReconcilerCreate(t *testing.T) { ) ctx := context.TODO() - apimanager := basicApimanagerSpecTestSystemOptions() appPreHookJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{Name: component.SystemAppPreHookJobName, Namespace: apimanager.Namespace}, @@ -59,12 +59,25 @@ func TestSystemReconcilerCreate(t *testing.T) { }, }, } + systemDatabaseSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-database", + Namespace: apimanager.Namespace, + }, + Data: map[string][]byte{}, + } // Objects to track in the fake client. - objs := []runtime.Object{apimanager, appPreHookJob} + objs := []runtime.Object{appPreHookJob, apimanager, systemDatabaseSecret} + fmt.Printf("Tracked objects: %v\n", objs) s := scheme.Scheme - s.AddKnownTypes(appsv1alpha1.GroupVersion, apimanager) - err := k8sappsv1.AddToScheme(s) + s.AddKnownTypes(appsv1alpha1.GroupVersion, &appsv1alpha1.APIManager{}) + s.AddKnownTypes(v1.SchemeGroupVersion, &v1.Secret{}, &v1.SecretList{}) + err := v1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = k8sappsv1.AddToScheme(s) if err != nil { t.Fatal(err) } @@ -93,8 +106,16 @@ func TestSystemReconcilerCreate(t *testing.T) { } // Create a fake client to mock API calls. - cl := fake.NewFakeClient(objs...) - clientAPIReader := fake.NewFakeClient(objs...) + //cl := fake.NewFakeClient(objs...) + cl := fake.NewClientBuilder(). + WithScheme(s). + WithRuntimeObjects(objs...). + Build() + //clientAPIReader := fake.NewFakeClient(objs...) + clientAPIReader := fake.NewClientBuilder(). + WithScheme(s). + WithRuntimeObjects(objs...). + Build() clientset := fakeclientset.NewSimpleClientset() recorder := record.NewFakeRecorder(10000) @@ -112,6 +133,7 @@ func TestSystemReconcilerCreate(t *testing.T) { objName string obj k8sclient.Object }{ + {"systemDatabase", "system-database", &v1.Secret{}}, {"systemPVC", "system-storage", &v1.PersistentVolumeClaim{}}, {"systemProviderService", "system-provider", &v1.Service{}}, {"systemMasterService", "system-master", &v1.Service{}}, @@ -217,7 +239,14 @@ func TestReplicaSystemReconciler(t *testing.T) { for _, tc := range cases { t.Run(tc.testName, func(subT *testing.T) { - objs := []runtime.Object{tc.apimanager, appPreHookJob} + systemDatabaseSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-database", + Namespace: tc.apimanager.Namespace, + }, + Data: map[string][]byte{}, + } + objs := []runtime.Object{tc.apimanager, appPreHookJob, systemDatabaseSecret} // Create a fake client to mock API calls. cl := fake.NewFakeClient(objs...) clientAPIReader := fake.NewFakeClient(objs...) diff --git a/pkg/3scale/amp/operator/system_searchd_options_provider.go b/pkg/3scale/amp/operator/system_searchd_options_provider.go index 91759ac7d..716e257bf 100644 --- a/pkg/3scale/amp/operator/system_searchd_options_provider.go +++ b/pkg/3scale/amp/operator/system_searchd_options_provider.go @@ -35,6 +35,7 @@ func (s *SystemSearchdOptionsProvider) GetOptions() (*component.SystemSearchdOpt s.setPVCOptions() s.setPriorityClassNames() s.setTopologySpreadConstraints() + s.setSerchdDBTLSEnabled() err := s.options.Validate() if err != nil { @@ -129,3 +130,7 @@ func (s *SystemSearchdOptionsProvider) setTopologySpreadConstraints() { s.options.TopologySpreadConstraints = s.apimanager.Spec.System.SearchdSpec.TopologySpreadConstraints } } + +func (s *SystemSearchdOptionsProvider) setSerchdDBTLSEnabled() { + s.options.SearchdDbTLSEnabled = s.apimanager.IsSystemDatabaseTLSEnabled() +} diff --git a/pkg/3scale/amp/operator/system_searchd_options_provider_test.go b/pkg/3scale/amp/operator/system_searchd_options_provider_test.go index 9b787f490..277deadce 100644 --- a/pkg/3scale/amp/operator/system_searchd_options_provider_test.go +++ b/pkg/3scale/amp/operator/system_searchd_options_provider_test.go @@ -19,6 +19,19 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func testSearchdBasicSystemDatabaseSecret() *v1.Secret { + return &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: component.SystemSecretSystemDatabaseSecretName, + Namespace: namespace, + }, + Immutable: nil, + Data: nil, + StringData: nil, + Type: "", + } +} func testSearchdBasicApimanager() *appsv1alpha1.APIManager { apimanager := &appsv1alpha1.APIManager{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/3scale/amp/operator/system_searchd_reconciler.go b/pkg/3scale/amp/operator/system_searchd_reconciler.go index 95162928f..70ea2dcb0 100644 --- a/pkg/3scale/amp/operator/system_searchd_reconciler.go +++ b/pkg/3scale/amp/operator/system_searchd_reconciler.go @@ -75,7 +75,12 @@ func (r *SystemSearchdReconciler) Reconcile() (reconcile.Result, error) { reconcilers.DeploymentArgsMutator, reconcilers.DeploymentPodContainerImageMutator, ) - err = r.ReconcileDeployment(searchd.Deployment(ampImages.Options.SystemSearchdImage), searchdDeploymentMutator) + + searchdDep, err := searchd.Deployment(r.Context(), r.Client(), r.apiManager.Namespace, ampImages.Options.SystemSearchdImage) + if err != nil { + return reconcile.Result{}, err + } + err = r.ReconcileDeployment(searchdDep, searchdDeploymentMutator) if err != nil { return reconcile.Result{}, err } diff --git a/pkg/3scale/amp/operator/system_searchd_reconciler_test.go b/pkg/3scale/amp/operator/system_searchd_reconciler_test.go index 3e85db946..c1b00ed5a 100644 --- a/pkg/3scale/amp/operator/system_searchd_reconciler_test.go +++ b/pkg/3scale/amp/operator/system_searchd_reconciler_test.go @@ -29,8 +29,9 @@ func TestSystemSearchdReconciler(t *testing.T) { ctx := context.TODO() apimanager := testSearchdBasicApimanager() + systemDatabaseSecret := testSearchdBasicSystemDatabaseSecret() // Objects to track in the fake client. - objs := []runtime.Object{apimanager} + objs := []runtime.Object{apimanager, systemDatabaseSecret} s := scheme.Scheme s.AddKnownTypes(appsv1alpha1.GroupVersion, apimanager) err := k8sappsv1.AddToScheme(s) diff --git a/pkg/3scale/amp/operator/zync_options_provider.go b/pkg/3scale/amp/operator/zync_options_provider.go index 130c702a6..6414699c1 100644 --- a/pkg/3scale/amp/operator/zync_options_provider.go +++ b/pkg/3scale/amp/operator/zync_options_provider.go @@ -57,6 +57,7 @@ func (z *ZyncOptionsProvider) GetZyncOptions() (*component.ZyncOptions, error) { z.zyncOptions.ZyncPodTemplateAnnotations = z.zyncPodTemplateAnnotations() z.zyncOptions.ZyncQuePodTemplateAnnotations = z.zyncQuePodTemplateAnnotations() z.zyncOptions.ZyncDatabasePodTemplateAnnotations = z.apimanager.Spec.Zync.DatabaseAnnotations + z.setZyncTLSEnabled() z.zyncOptions.ZyncMetrics = true @@ -121,6 +122,27 @@ func (z *ZyncOptionsProvider) setSecretBasedOptions() error { component.DefaultZyncDatabaseURL(zyncDatabasePassword), z.apimanager.IsExternal(appsv1alpha1.ZyncDatabase), }, + { + &z.zyncOptions.DatabaseSslCa, + component.ZyncSecretName, + component.ZyncSecretSslCa, + component.DefaultZyncSslEmpty(), + false, + }, + { + &z.zyncOptions.DatabaseSslCert, + component.ZyncSecretName, + component.ZyncSecretSslCert, + component.DefaultZyncSslEmpty(), + false, + }, + { + &z.zyncOptions.DatabaseSslKey, + component.ZyncSecretName, + component.ZyncSecretSslKey, + component.DefaultZyncSslEmpty(), + false, + }, } for _, option := range cases { @@ -338,3 +360,7 @@ func (z *ZyncOptionsProvider) zyncQuePodTemplateAnnotations() map[string]string } return annotations } + +func (z *ZyncOptionsProvider) setZyncTLSEnabled() { + z.zyncOptions.ZyncDbTLSEnabled = z.apimanager.IsZyncDatabaseTLSEnabled() +} diff --git a/pkg/3scale/amp/operator/zync_reconciler.go b/pkg/3scale/amp/operator/zync_reconciler.go index edad4bea2..be1d1cfbd 100644 --- a/pkg/3scale/amp/operator/zync_reconciler.go +++ b/pkg/3scale/amp/operator/zync_reconciler.go @@ -74,7 +74,11 @@ func (r *ZyncReconciler) Reconcile() (reconcile.Result, error) { if r.apiManager.Spec.Zync.AppSpec.Replicas != nil { zyncMutators = append(zyncMutators, reconcilers.DeploymentReplicasMutator) } - err = r.ReconcileDeployment(zync.Deployment(ampImages.Options.ZyncImage), reconcilers.DeploymentMutator(zyncMutators...)) + zyncDep, err := zync.Deployment(r.Context(), r.Client(), ampImages.Options.ZyncImage) + if err != nil { + return reconcile.Result{}, err + } + err = r.ReconcileDeployment(zyncDep, reconcilers.DeploymentMutator(zyncMutators...)) if err != nil { return reconcile.Result{}, err } @@ -102,7 +106,11 @@ func (r *ZyncReconciler) Reconcile() (reconcile.Result, error) { if r.apiManager.Spec.Zync.QueSpec.Replicas != nil { zyncQueMutators = append(zyncQueMutators, reconcilers.DeploymentReplicasMutator) } - err = r.ReconcileDeployment(zync.QueDeployment(ampImages.Options.ZyncImage), reconcilers.DeploymentMutator(zyncQueMutators...)) + zyncQueDep, err := zync.QueDeployment(r.Context(), r.Client(), ampImages.Options.ZyncImage) + if err != nil { + return reconcile.Result{}, err + } + err = r.ReconcileDeployment(zyncQueDep, reconcilers.DeploymentMutator(zyncQueMutators...)) if err != nil { return reconcile.Result{}, err } @@ -320,7 +328,10 @@ func (r *ZyncReconciler) deleteZyncComponents(zync *component.Zync, ampImages *c } // ZyncQue Deployment - zyncQueDeployment := zync.QueDeployment(ampImages.Options.ZyncImage) + zyncQueDeployment, err := zync.QueDeployment(r.Context(), r.Client(), ampImages.Options.ZyncImage) + if err != nil { + return err + } common.TagObjectToDelete(zyncQueDeployment) err = r.ReconcileDeployment(zyncQueDeployment, reconcilers.DeleteOnlyMutator) if err != nil { @@ -328,7 +339,10 @@ func (r *ZyncReconciler) deleteZyncComponents(zync *component.Zync, ampImages *c } // Zync Deployment - zyncDeployment := zync.Deployment(ampImages.Options.ZyncImage) + zyncDeployment, err := zync.Deployment(r.Context(), r.Client(), ampImages.Options.ZyncImage) + if err != nil { + return err + } common.TagObjectToDelete(zyncDeployment) err = r.ReconcileDeployment(zyncDeployment, reconcilers.DeleteOnlyMutator) if err != nil { diff --git a/pkg/3scale/amp/operator/zync_reconciler_test.go b/pkg/3scale/amp/operator/zync_reconciler_test.go index c00dd1607..9d3a9e0d5 100644 --- a/pkg/3scale/amp/operator/zync_reconciler_test.go +++ b/pkg/3scale/amp/operator/zync_reconciler_test.go @@ -64,7 +64,7 @@ func TestNewZyncReconciler(t *testing.T) { }, } // Objects to track in the fake client. - objs := []runtime.Object{apimanager} + objs := []runtime.Object{apimanager, testZyncSecret()} s := scheme.Scheme s.AddKnownTypes(appsv1alpha1.GroupVersion, apimanager) err := k8sappsv1.AddToScheme(s) @@ -319,7 +319,8 @@ func TestReplicaZyncReconciler(t *testing.T) { for _, tc := range cases { t.Run(tc.testName, func(subT *testing.T) { - objs := []runtime.Object{tc.apimanager} + zyncSecret := testZyncSecret() + objs := []runtime.Object{tc.apimanager, zyncSecret} // Create a fake client to mock API calls. cl := fake.NewFakeClient(objs...) clientAPIReader := fake.NewFakeClient(objs...) @@ -400,3 +401,18 @@ func testZyncAPIManagerCreator(zyncReplicas, zyncQueReplicas *int64) *appsv1alph }, } } + +func testZyncSecret() *v1.Secret { + var namespace = "operator-unittest" + return &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: component.ZyncSecretName, + Namespace: namespace, + }, + Immutable: nil, + Data: nil, + StringData: nil, + Type: "", + } +} diff --git a/pkg/helper/envvarutils.go b/pkg/helper/envvarutils.go index e5a086c95..25dee11f3 100644 --- a/pkg/helper/envvarutils.go +++ b/pkg/helper/envvarutils.go @@ -1,9 +1,13 @@ package helper import ( - "reflect" - + "context" + "fmt" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" ) func EnvVarFromConfigMap(envVarName string, configMapName, configMapKey string) v1.EnvVar { @@ -157,3 +161,55 @@ func EnvVarReconciler(desired []v1.EnvVar, existing *[]v1.EnvVar, envVar string) } return update } + +// check if the secret ssl certs are populated and sets the path if they are +// system-app and zync use this function +func TlsCertPresent(pathSslEnvVar string, secretName string, databaseTLSEnabled bool) string { + cfg, err := config.GetConfig() + if err != nil { + fmt.Printf("clientTLS error, get config : %v", err) + return "" + } + clientTLS, err := client.New(cfg, client.Options{}) + if err != nil { + fmt.Printf("clientTLS error, client create : %v", err) + return "" + } + namespace, _ := GetOperatorNamespace() + var path string + var sslEnvVar string + + // Determine the paths and corresponding secret keys + switch pathSslEnvVar { + case "DATABASE_SSL_CA": + path = "/tls/ca.crt" + sslEnvVar = "DB_SSL_CA" + case "DATABASE_SSL_CERT": + path = "/tls/tls.crt" + sslEnvVar = "DB_SSL_CERT" + case "DATABASE_SSL_KEY": + path = "/tls/tls.key" + sslEnvVar = "DB_SSL_KEY" + default: + return "" + } + + // check the secret exists + secret := v1.Secret{} + nn := types.NamespacedName{ + Name: secretName, + Namespace: namespace, + } + err = clientTLS.Get(context.TODO(), nn, &secret) + if err != nil { + fmt.Printf("clientTLS error, get secret : %v", err) + return "" + } + + // Check if SSL_KEY, SSL_CERT, and SSL_CA are empty + // checks the cert is populated in the secret, if so populates the path in the volume mount + if sslCert, ok := secret.Data[sslEnvVar]; ok && len(sslCert) > 0 && databaseTLSEnabled { + return path + } + return "" +} diff --git a/pkg/helper/k8s.go b/pkg/helper/k8s.go index 1efc066c4..3da940e4c 100644 --- a/pkg/helper/k8s.go +++ b/pkg/helper/k8s.go @@ -44,7 +44,7 @@ func GetOperatorNamespace() (string, error) { if IsRunLocally() { ns, err := GetWatchNamespace() if err != nil { - return "", fmt.Errorf("running locally but WATCHNAMESPACE not found") + return "", fmt.Errorf("running locally but WATCH_NAMESPACE not found") } return ns, nil } diff --git a/pkg/reconcilers/deployment.go b/pkg/reconcilers/deployment.go index c7b041c94..f7adbc42b 100644 --- a/pkg/reconcilers/deployment.go +++ b/pkg/reconcilers/deployment.go @@ -327,6 +327,13 @@ func DeploymentPodInitContainerImageMutator(desired, existing *k8sappsv1.Deploym updated := false for i, desiredContainer := range desired.Spec.Template.Spec.InitContainers { + if i >= len(existing.Spec.Template.Spec.InitContainers) { + // Add missing containers from desired to existing + existing.Spec.Template.Spec.InitContainers = append(existing.Spec.Template.Spec.InitContainers, desiredContainer) + fmt.Printf("Added missing container: %s\n", desiredContainer.Name) + updated = true + continue + } existingContainer := &existing.Spec.Template.Spec.InitContainers[i] if !reflect.DeepEqual(existingContainer.Image, desiredContainer.Image) {