Add secret mount to operator (#535)

* add secret mount to operator
This commit is contained in:
Markus 2019-06-19 12:40:49 +02:00 committed by Sergey Dudoladov
parent 0ed92ed04e
commit 93bfed3e75
10 changed files with 143 additions and 6 deletions

View File

@ -69,6 +69,8 @@ configAwsOrGcp:
# kube_iam_role: "" # kube_iam_role: ""
# log_s3_bucket: "" # log_s3_bucket: ""
# wal_s3_bucket: "" # wal_s3_bucket: ""
# additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir"
configLogicalBackup: configLogicalBackup:
logical_backup_schedule: "30 00 * * *" logical_backup_schedule: "30 00 * * *"

View File

@ -333,3 +333,19 @@ The operator can manage k8s cron jobs to run logical backups of Postgres cluster
4. You may use your own image by overwriting the relevant field in the operator configuration. Any such image must ensure the logical backup is able to finish [in presence of pod restarts](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#handling-pod-and-container-failures) and [simultaneous invocations](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) of the backup cron job. 4. You may use your own image by overwriting the relevant field in the operator configuration. Any such image must ensure the logical backup is able to finish [in presence of pod restarts](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#handling-pod-and-container-failures) and [simultaneous invocations](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) of the backup cron job.
5. For that feature to work, your RBAC policy must enable operations on the `cronjobs` resource from the `batch` API group for the operator service account. See [example RBAC](../manifests/operator-service-account-rbac.yaml) 5. For that feature to work, your RBAC policy must enable operations on the `cronjobs` resource from the `batch` API group for the operator service account. See [example RBAC](../manifests/operator-service-account-rbac.yaml)
## Access to cloud resources from clusters in non cloud environment
To access cloud resources like S3 from a cluster in a bare metal setup you can use
`additional_secret_mount` and `additional_secret_mount_path` config parameters.
With this you can provision cloud credentials to the containers in the pods of the StatefulSet.
This works this way that it mounts a volume from the given secret in the pod and this can
then accessed in the container over the configured mount path. Via [Custum Pod Environment Variables](#custom-pod-environment-variables)
you can then point the different cloud sdk's (aws, google etc.) to this mounted secret.
With this credentials the cloud sdk can then access cloud resources to upload logs etc.
A secret can be pre provisioned in different ways:
* Generic secret created via `kubectl create secret generic some-cloud-creds --from-file=some-cloud-credentials-file.json`
* Automaticly provisioned via a Controller like [kube-aws-iam-controller](https://github.com/mikkeloscar/kube-aws-iam-controller). This controller would then also rotate the credentials. Please visit the documention for more information.

View File

@ -407,6 +407,12 @@ yet officially supported.
* **aws_region** * **aws_region**
AWS region used to store ESB volumes. The default is `eu-central-1`. AWS region used to store ESB volumes. The default is `eu-central-1`.
* **additional_secret_mount**
Additional Secret (aws or gcp credentials) to mount in the pod. The default is empty.
* **additional_secret_mount_path**
Path to mount the above Secret in the filesystem of the container(s). The default is empty.
## Debugging the operator ## Debugging the operator
Options to aid debugging of the operator itself. Grouped under the `debug` key. Options to aid debugging of the operator itself. Grouped under the `debug` key.

View File

@ -33,6 +33,8 @@ data:
# https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees # https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
# inherited_labels: "" # inherited_labels: ""
aws_region: eu-central-1 aws_region: eu-central-1
# additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir"
db_hosted_zone: db.example.com db_hosted_zone: db.example.com
master_dns_name_format: '{cluster}.{team}.staging.{hostedzone}' master_dns_name_format: '{cluster}.{team}.staging.{hostedzone}'
replica_dns_name_format: '{cluster}-repl.{team}.staging.{hostedzone}' replica_dns_name_format: '{cluster}-repl.{team}.staging.{hostedzone}'

View File

@ -67,6 +67,8 @@ configuration:
# log_s3_bucket: "" # log_s3_bucket: ""
# kube_iam_role: "" # kube_iam_role: ""
aws_region: eu-central-1 aws_region: eu-central-1
# additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir"
debug: debug:
debug_logging: true debug_logging: true
enable_database_access: true enable_database_access: true

View File

@ -101,10 +101,12 @@ type LoadBalancerConfiguration struct {
// AWSGCPConfiguration defines the configuration for AWS // AWSGCPConfiguration defines the configuration for AWS
// TODO complete Google Cloud Platform (GCP) configuration // TODO complete Google Cloud Platform (GCP) configuration
type AWSGCPConfiguration struct { type AWSGCPConfiguration struct {
WALES3Bucket string `json:"wal_s3_bucket,omitempty"` WALES3Bucket string `json:"wal_s3_bucket,omitempty"`
AWSRegion string `json:"aws_region,omitempty"` AWSRegion string `json:"aws_region,omitempty"`
LogS3Bucket string `json:"log_s3_bucket,omitempty"` LogS3Bucket string `json:"log_s3_bucket,omitempty"`
KubeIAMRole string `json:"kube_iam_role,omitempty"` KubeIAMRole string `json:"kube_iam_role,omitempty"`
AdditionalSecretMount string `json:"additional_secret_mount,omitempty"`
AdditionalSecretMountPath string `json:"additional_secret_mount_path" default:"/meta/credentials"`
} }
// OperatorDebugConfiguration defines options for the debug mode // OperatorDebugConfiguration defines options for the debug mode

View File

@ -384,7 +384,7 @@ func generateContainer(
VolumeMounts: volumeMounts, VolumeMounts: volumeMounts,
Env: envVars, Env: envVars,
SecurityContext: &v1.SecurityContext{ SecurityContext: &v1.SecurityContext{
Privileged: &privilegedMode, Privileged: &privilegedMode,
ReadOnlyRootFilesystem: &falseBool, ReadOnlyRootFilesystem: &falseBool,
}, },
} }
@ -445,6 +445,8 @@ func generatePodTemplate(
shmVolume bool, shmVolume bool,
podAntiAffinity bool, podAntiAffinity bool,
podAntiAffinityTopologyKey string, podAntiAffinityTopologyKey string,
additionalSecretMount string,
additionalSecretMountPath string,
) (*v1.PodTemplateSpec, error) { ) (*v1.PodTemplateSpec, error) {
terminateGracePeriodSeconds := terminateGracePeriod terminateGracePeriodSeconds := terminateGracePeriod
@ -479,6 +481,10 @@ func generatePodTemplate(
podSpec.PriorityClassName = priorityClassName podSpec.PriorityClassName = priorityClassName
} }
if additionalSecretMount != "" {
addSecretVolume(&podSpec, additionalSecretMount, additionalSecretMountPath)
}
template := v1.PodTemplateSpec{ template := v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Labels: labels, Labels: labels,
@ -864,7 +870,9 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
effectivePodPriorityClassName, effectivePodPriorityClassName,
mountShmVolumeNeeded(c.OpConfig, spec), mountShmVolumeNeeded(c.OpConfig, spec),
c.OpConfig.EnablePodAntiAffinity, c.OpConfig.EnablePodAntiAffinity,
c.OpConfig.PodAntiAffinityTopologyKey); err != nil { c.OpConfig.PodAntiAffinityTopologyKey,
c.OpConfig.AdditionalSecretMount,
c.OpConfig.AdditionalSecretMountPath); err != nil {
return nil, fmt.Errorf("could not generate pod template: %v", err) return nil, fmt.Errorf("could not generate pod template: %v", err)
} }
@ -1013,6 +1021,28 @@ func addShmVolume(podSpec *v1.PodSpec) {
podSpec.Volumes = volumes podSpec.Volumes = volumes
} }
func addSecretVolume(podSpec *v1.PodSpec, additionalSecretMount string, additionalSecretMountPath string) {
volumes := append(podSpec.Volumes, v1.Volume{
Name: additionalSecretMount,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: additionalSecretMount,
},
},
})
for i := range podSpec.Containers {
mounts := append(podSpec.Containers[i].VolumeMounts,
v1.VolumeMount{
Name: additionalSecretMount,
MountPath: additionalSecretMountPath,
})
podSpec.Containers[i].VolumeMounts = mounts
}
podSpec.Volumes = volumes
}
func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) (*v1.PersistentVolumeClaim, error) { func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) (*v1.PersistentVolumeClaim, error) {
var storageClassName *string var storageClassName *string
@ -1395,6 +1425,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) {
"", "",
false, false,
false, false,
"",
"",
""); err != nil { ""); err != nil {
return nil, fmt.Errorf("could not generate pod template for logical backup pod: %v", err) return nil, fmt.Errorf("could not generate pod template for logical backup pod: %v", err)
} }

View File

@ -389,3 +389,74 @@ func TestCloneEnv(t *testing.T) {
} }
} }
} }
func TestSecretVolume(t *testing.T) {
testName := "TestSecretVolume"
tests := []struct {
subTest string
podSpec *v1.PodSpec
secretPos int
}{
{
subTest: "empty PodSpec",
podSpec: &v1.PodSpec{
Volumes: []v1.Volume{},
Containers: []v1.Container{
{
VolumeMounts: []v1.VolumeMount{},
},
},
},
secretPos: 0,
},
{
subTest: "non empty PodSpec",
podSpec: &v1.PodSpec{
Volumes: []v1.Volume{{}},
Containers: []v1.Container{
{
VolumeMounts: []v1.VolumeMount{
{
Name: "data",
ReadOnly: false,
MountPath: "/data",
},
},
},
},
},
secretPos: 1,
},
}
for _, tt := range tests {
additionalSecretMount := "aws-iam-s3-role"
additionalSecretMountPath := "/meta/credentials"
numMounts := len(tt.podSpec.Containers[0].VolumeMounts)
addSecretVolume(tt.podSpec, additionalSecretMount, additionalSecretMountPath)
volumeName := tt.podSpec.Volumes[tt.secretPos].Name
if volumeName != additionalSecretMount {
t.Errorf("%s %s: Expected volume %s was not created, have %s instead",
testName, tt.subTest, additionalSecretMount, volumeName)
}
for i := range tt.podSpec.Containers {
volumeMountName := tt.podSpec.Containers[i].VolumeMounts[tt.secretPos].Name
if volumeMountName != additionalSecretMount {
t.Errorf("%s %s: Expected mount %s was not created, have %s instead",
testName, tt.subTest, additionalSecretMount, volumeMountName)
}
}
numMountsCheck := len(tt.podSpec.Containers[0].VolumeMounts)
if numMountsCheck != numMounts+1 {
t.Errorf("Unexpected number of VolumeMounts: got %v instead of %v",
numMountsCheck, numMounts+1)
}
}
}

View File

@ -86,6 +86,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.AWSRegion = fromCRD.AWSGCP.AWSRegion result.AWSRegion = fromCRD.AWSGCP.AWSRegion
result.LogS3Bucket = fromCRD.AWSGCP.LogS3Bucket result.LogS3Bucket = fromCRD.AWSGCP.LogS3Bucket
result.KubeIAMRole = fromCRD.AWSGCP.KubeIAMRole result.KubeIAMRole = fromCRD.AWSGCP.KubeIAMRole
result.AdditionalSecretMount = fromCRD.AWSGCP.AdditionalSecretMount
result.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath
result.DebugLogging = fromCRD.OperatorDebug.DebugLogging result.DebugLogging = fromCRD.OperatorDebug.DebugLogging
result.EnableDBAccess = fromCRD.OperatorDebug.EnableDBAccess result.EnableDBAccess = fromCRD.OperatorDebug.EnableDBAccess

View File

@ -98,6 +98,8 @@ type Config struct {
WALES3Bucket string `name:"wal_s3_bucket"` WALES3Bucket string `name:"wal_s3_bucket"`
LogS3Bucket string `name:"log_s3_bucket"` LogS3Bucket string `name:"log_s3_bucket"`
KubeIAMRole string `name:"kube_iam_role"` KubeIAMRole string `name:"kube_iam_role"`
AdditionalSecretMount string `name:"additional_secret_mount"`
AdditionalSecretMountPath string `name:"additional_secret_mount_path" default:"/meta/credentials"`
DebugLogging bool `name:"debug_logging" default:"true"` DebugLogging bool `name:"debug_logging" default:"true"`
EnableDBAccess bool `name:"enable_database_access" default:"true"` EnableDBAccess bool `name:"enable_database_access" default:"true"`
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`