Merge branch 'master' into coderanger-disruption-budget

This commit is contained in:
Felix Kunde 2019-06-17 16:31:33 +02:00
commit 8285a3b7b7
16 changed files with 177 additions and 46 deletions

View File

@ -1,2 +1,2 @@
# global owners # global owners
* @alexeyklyukin @erthalion @sdudoladov @Jan-M @CyberDem0n @avaczi @FxKu * @alexeyklyukin @erthalion @sdudoladov @Jan-M @CyberDem0n @avaczi @FxKu @RafiaSabih

View File

@ -6,4 +6,9 @@ RUN apk --no-cache add ca-certificates
COPY build/* / COPY build/* /
RUN addgroup -g 1000 pgo
RUN adduser -D -u 1000 -G pgo -g 'Postgres operator' pgo
USER 1000:1000
ENTRYPOINT ["/postgres-operator"] ENTRYPOINT ["/postgres-operator"]

View File

@ -254,6 +254,19 @@ under the `clone` top-level key and do not affect the already running cluster.
timestamp. When this parameter is set the operator will not consider cloning timestamp. When this parameter is set the operator will not consider cloning
from the live cluster, even if it is running, and instead goes to S3. Optional. from the live cluster, even if it is running, and instead goes to S3. Optional.
* **s3_endpoint**
the url of the S3-compatible service should be set when cloning from non AWS S3. Optional.
* **s3_access_key_id**
the access key id, used for authentication on S3 service. Optional.
* **s3_secret_access_key**
the secret access key, used for authentication on S3 service. Optional.
* **s3_force_path_style**
to enable path-style addressing(i.e., http://s3.amazonaws.com/BUCKET/KEY) when connecting to an S3-compatible service
that lack of support for sub-domain style bucket URLs (i.e., http://BUCKET.s3.amazonaws.com/KEY). Optional.
### EBS volume resizing ### EBS volume resizing
Those parameters are grouped under the `volume` top-level key and define the Those parameters are grouped under the `volume` top-level key and define the

View File

@ -32,7 +32,7 @@ configuration.
kubectl create -f manifests/postgresql-operator-default-configuration.yaml kubectl create -f manifests/postgresql-operator-default-configuration.yaml
kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml
``` ```
Note that the operator first registers the CRD of the `OperatorConfiguration` Note that the operator first attempts to register the CRD of the `OperatorConfiguration`
and then waits for an instance to be created. In between these two event the and then waits for an instance to be created. In between these two event the
operator pod may be failing since it cannot fetch the not-yet-existing operator pod may be failing since it cannot fetch the not-yet-existing
`OperatorConfiguration` instance. `OperatorConfiguration` instance.

View File

@ -41,6 +41,15 @@ $ kubectl create -f manifests/minimal-postgres-manifest.yaml
$ kubectl get pods -w --show-labels $ kubectl get pods -w --show-labels
``` ```
## Give K8S users access to create/list postgresqls
```bash
$ kubectl create -f manifests/user-facing-clusterroles.yaml
```
Creates zalando-postgres-operator:users:view, :edit and :admin clusterroles that are
aggregated into the default roles.
## Connect to PostgreSQL ## Connect to PostgreSQL
With a `port-forward` on one of the database pods (e.g. the master) you can With a `port-forward` on one of the database pods (e.g. the master) you can
@ -254,6 +263,24 @@ metadata:
Note that timezone is required for `timestamp`. Otherwise, offset is relative Note that timezone is required for `timestamp`. Otherwise, offset is relative
to UTC, see [RFC 3339 section 5.6) 3339 section 5.6](https://www.ietf.org/rfc/rfc3339.txt). to UTC, see [RFC 3339 section 5.6) 3339 section 5.6](https://www.ietf.org/rfc/rfc3339.txt).
For non AWS S3 following settings can be set to support cloning from other S3 implementations:
```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-test-cluster
spec:
clone:
uid: "efd12e58-5786-11e8-b5a7-06148230260c"
cluster: "acid-batman"
timestamp: "2017-12-19T12:40:33+01:00"
s3_endpoint: https://s3.acme.org
s3_access_key_id: 0123456789abcdef0123456789abcdef
s3_secret_access_key: 0123456789abcdef0123456789abcdef
s3_force_path_style: true
```
## Sidecar Support ## Sidecar Support
Each cluster can specify arbitrary sidecars to run. These containers could be used for Each cluster can specify arbitrary sidecars to run. These containers could be used for

View File

@ -11,6 +11,7 @@ spec:
teamId: "ACID" teamId: "ACID"
volume: volume:
size: 1Gi size: 1Gi
#storageClass: my-sc
numberOfInstances: 2 numberOfInstances: 2
users: #Application/Robot users users: #Application/Robot users
zalando: zalando:
@ -46,13 +47,13 @@ spec:
pg_hba: pg_hba:
- hostssl all all 0.0.0.0/0 md5 - hostssl all all 0.0.0.0/0 md5
- host all all 0.0.0.0/0 md5 - host all all 0.0.0.0/0 md5
slots: #slots:
permanent_physical_1: # permanent_physical_1:
type: physical # type: physical
permanent_logical_1: # permanent_logical_1:
type: logical # type: logical
database: foo # database: foo
plugin: pgoutput # plugin: pgoutput
ttl: 30 ttl: 30
loop_wait: &loop_wait 10 loop_wait: &loop_wait 10
retry_timeout: 10 retry_timeout: 10

View File

@ -21,6 +21,10 @@ spec:
limits: limits:
cpu: 2000m cpu: 2000m
memory: 500Mi memory: 500Mi
securityContext:
runAsUser: 1000
runAsNonRoot: true
readOnlyRootFilesystem: true
env: env:
# provided additional ENV vars can overwrite individual config map entries # provided additional ENV vars can overwrite individual config map entries
- name: CONFIG_MAP_NAME - name: CONFIG_MAP_NAME

View File

@ -0,0 +1,51 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
rbac.authorization.k8s.io/aggregate-to-admin: "true"
name: zalando-postgres-operator:users:admin
rules:
- apiGroups:
- acid.zalan.do
resources:
- postgresqls
- postgresqls/status
verbs:
- "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
rbac.authorization.k8s.io/aggregate-to-edit: "true"
name: zalando-postgres-operator:users:edit
rules:
- apiGroups:
- acid.zalan.do
resources:
- postgresqls
verbs:
- create
- update
- patch
- delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
name: zalando-postgres-operator:users:view
rules:
- apiGroups:
- acid.zalan.do
resources:
- postgresqls
- postgresqls/status
verbs:
- get
- list
- watch

View File

@ -115,10 +115,14 @@ type Patroni struct {
// CloneDescription describes which cluster the new should clone and up to which point in time // CloneDescription describes which cluster the new should clone and up to which point in time
type CloneDescription struct { type CloneDescription struct {
ClusterName string `json:"cluster,omitempty"` ClusterName string `json:"cluster,omitempty"`
UID string `json:"uid,omitempty"` UID string `json:"uid,omitempty"`
EndTimestamp string `json:"timestamp,omitempty"` EndTimestamp string `json:"timestamp,omitempty"`
S3WalPath string `json:"s3_wal_path,omitempty"` S3WalPath string `json:"s3_wal_path,omitempty"`
S3Endpoint string `json:"s3_endpoint,omitempty"`
S3AccessKeyId string `json:"s3_access_key_id,omitempty"`
S3SecretAccessKey string `json:"s3_secret_access_key,omitempty"`
S3ForcePathStyle *bool `json:"s3_force_path_style,omitempty" defaults:"false"`
} }
// Sidecar defines a container to be run in the same pod as the Postgres container. // Sidecar defines a container to be run in the same pod as the Postgres container.

View File

@ -61,12 +61,12 @@ var cloneClusterDescriptions = []struct {
in *CloneDescription in *CloneDescription
err error err error
}{ }{
{&CloneDescription{"foo+bar", "", "NotEmpty", ""}, nil}, {&CloneDescription{"foo+bar", "", "NotEmpty", "", "", "", "", nil}, nil},
{&CloneDescription{"foo+bar", "", "", ""}, {&CloneDescription{"foo+bar", "", "", "", "", "", "", nil},
errors.New(`clone cluster name must confirm to DNS-1035, regex used for validation is "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)}, errors.New(`clone cluster name must confirm to DNS-1035, regex used for validation is "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)},
{&CloneDescription{"foobar123456789012345678901234567890123456789012345678901234567890", "", "", ""}, {&CloneDescription{"foobar123456789012345678901234567890123456789012345678901234567890", "", "", "", "", "", "", nil},
errors.New("clone cluster name must be no longer than 63 characters")}, errors.New("clone cluster name must be no longer than 63 characters")},
{&CloneDescription{"foobar", "", "", ""}, nil}, {&CloneDescription{"foobar", "", "", "", "", "", "", nil}, nil},
} }
var maintenanceWindows = []struct { var maintenanceWindows = []struct {

View File

@ -50,6 +50,11 @@ func (in *AWSGCPConfiguration) DeepCopy() *AWSGCPConfiguration {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CloneDescription) DeepCopyInto(out *CloneDescription) { func (in *CloneDescription) DeepCopyInto(out *CloneDescription) {
*out = *in *out = *in
if in.S3ForcePathStyle != nil {
in, out := &in.S3ForcePathStyle, &out.S3ForcePathStyle
*out = new(bool)
**out = **in
}
return return
} }
@ -464,7 +469,7 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
} }
out.Clone = in.Clone in.Clone.DeepCopyInto(&out.Clone)
if in.Databases != nil { if in.Databases != nil {
in, out := &in.Databases, &out.Databases in, out := &in.Databases, &out.Databases
*out = make(map[string]string, len(*in)) *out = make(map[string]string, len(*in))

View File

@ -342,7 +342,7 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *v1beta1.StatefulSet) *comp
if c.Statefulset.Spec.Template.Spec.ServiceAccountName != statefulSet.Spec.Template.Spec.ServiceAccountName { if c.Statefulset.Spec.Template.Spec.ServiceAccountName != statefulSet.Spec.Template.Spec.ServiceAccountName {
needsReplace = true needsReplace = true
needsRollUpdate = true needsRollUpdate = true
reasons = append(reasons, "new statefulset's serviceAccountName service asccount name doesn't match the current one") reasons = append(reasons, "new statefulset's serviceAccountName service account name doesn't match the current one")
} }
if *c.Statefulset.Spec.Template.Spec.TerminationGracePeriodSeconds != *statefulSet.Spec.Template.Spec.TerminationGracePeriodSeconds { if *c.Statefulset.Spec.Template.Spec.TerminationGracePeriodSeconds != *statefulSet.Spec.Template.Spec.TerminationGracePeriodSeconds {
needsReplace = true needsReplace = true
@ -462,16 +462,16 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe
func compareResources(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool { func compareResources(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool {
equal := true equal := true
if a != nil { if a != nil {
equal = compareResoucesAssumeFirstNotNil(a, b) equal = compareResourcesAssumeFirstNotNil(a, b)
} }
if equal && (b != nil) { if equal && (b != nil) {
equal = compareResoucesAssumeFirstNotNil(b, a) equal = compareResourcesAssumeFirstNotNil(b, a)
} }
return equal return equal
} }
func compareResoucesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool { func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool {
if b == nil || (len(b.Requests) == 0) { if b == nil || (len(b.Requests) == 0) {
return len(a.Requests) == 0 return len(a.Requests) == 0
} }
@ -884,7 +884,7 @@ func (c *Cluster) initInfrastructureRoles() error {
return nil return nil
} }
// resolves naming conflicts between existing and new roles by chosing either of them. // resolves naming conflicts between existing and new roles by choosing either of them.
func (c *Cluster) resolveNameConflict(currentRole, newRole *spec.PgUser) spec.PgUser { func (c *Cluster) resolveNameConflict(currentRole, newRole *spec.PgUser) spec.PgUser {
var result spec.PgUser var result spec.PgUser
if newRole.Origin >= currentRole.Origin { if newRole.Origin >= currentRole.Origin {
@ -978,7 +978,7 @@ func (c *Cluster) Switchover(curMaster *v1.Pod, candidate spec.NamespacedName) e
// signal the role label waiting goroutine to close the shop and go home // signal the role label waiting goroutine to close the shop and go home
close(stopCh) close(stopCh)
// wait until the goroutine terminates, since unregisterPodSubscriber // wait until the goroutine terminates, since unregisterPodSubscriber
// must be called before the outer return; otherwsise we risk subscribing to the same pod twice. // must be called before the outer return; otherwise we risk subscribing to the same pod twice.
wg.Wait() wg.Wait()
// close the label waiting channel no sooner than the waiting goroutine terminates. // close the label waiting channel no sooner than the waiting goroutine terminates.
close(podLabelErr) close(podLabelErr)

View File

@ -329,7 +329,7 @@ func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]stri
return []v1.Toleration{} return []v1.Toleration{}
} }
// isBootstrapOnlyParameter checks asgainst special Patroni bootstrap parameters. // isBootstrapOnlyParameter checks against special Patroni bootstrap parameters.
// Those parameters must go to the bootstrap/dcs/postgresql/parameters section. // Those parameters must go to the bootstrap/dcs/postgresql/parameters section.
// See http://patroni.readthedocs.io/en/latest/dynamic_configuration.html. // See http://patroni.readthedocs.io/en/latest/dynamic_configuration.html.
func isBootstrapOnlyParameter(param string) bool { func isBootstrapOnlyParameter(param string) bool {
@ -1266,6 +1266,29 @@ func (c *Cluster) generateCloneEnvironment(description *acidv1.CloneDescription)
result = append(result, v1.EnvVar{Name: "CLONE_METHOD", Value: "CLONE_WITH_WALE"}) result = append(result, v1.EnvVar{Name: "CLONE_METHOD", Value: "CLONE_WITH_WALE"})
result = append(result, v1.EnvVar{Name: "CLONE_TARGET_TIME", Value: description.EndTimestamp}) result = append(result, v1.EnvVar{Name: "CLONE_TARGET_TIME", Value: description.EndTimestamp})
result = append(result, v1.EnvVar{Name: "CLONE_WAL_BUCKET_SCOPE_PREFIX", Value: ""}) result = append(result, v1.EnvVar{Name: "CLONE_WAL_BUCKET_SCOPE_PREFIX", Value: ""})
if description.S3Endpoint != "" {
result = append(result, v1.EnvVar{Name: "CLONE_AWS_ENDPOINT", Value: description.S3Endpoint})
result = append(result, v1.EnvVar{Name: "CLONE_WALE_S3_ENDPOINT", Value: description.S3Endpoint})
}
if description.S3AccessKeyId != "" {
result = append(result, v1.EnvVar{Name: "CLONE_AWS_ACCESS_KEY_ID", Value: description.S3AccessKeyId})
}
if description.S3SecretAccessKey != "" {
result = append(result, v1.EnvVar{Name: "CLONE_AWS_SECRET_ACCESS_KEY", Value: description.S3SecretAccessKey})
}
if description.S3ForcePathStyle != nil {
s3ForcePathStyle := "0"
if *description.S3ForcePathStyle {
s3ForcePathStyle = "1"
}
result = append(result, v1.EnvVar{Name: "CLONE_AWS_S3_FORCE_PATH_STYLE", Value: s3ForcePathStyle})
}
} }
return result return result
@ -1372,7 +1395,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) {
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)
} }
// overwrite specifc params of logical backups pods // overwrite specific params of logical backups pods
podTemplate.Spec.Affinity = &podAffinity podTemplate.Spec.Affinity = &podAffinity
podTemplate.Spec.RestartPolicy = "Never" // affects containers within a pod podTemplate.Spec.RestartPolicy = "Never" // affects containers within a pod

View File

@ -361,7 +361,7 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error
// TODO: check if it possible to change the service type with a patch in future versions of Kubernetes // TODO: check if it possible to change the service type with a patch in future versions of Kubernetes
if newService.Spec.Type != c.Services[role].Spec.Type { if newService.Spec.Type != c.Services[role].Spec.Type {
// service type has changed, need to replace the service completely. // service type has changed, need to replace the service completely.
// we cannot use just pach the current service, since it may contain attributes incompatible with the new type. // we cannot use just patch the current service, since it may contain attributes incompatible with the new type.
var ( var (
currentEndpoint *v1.Endpoints currentEndpoint *v1.Endpoints
err error err error
@ -369,7 +369,7 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error
if role == Master { if role == Master {
// for the master service we need to re-create the endpoint as well. Get the up-to-date version of // for the master service we need to re-create the endpoint as well. Get the up-to-date version of
// the addresses stored in it before the service is deleted (deletion of the service removes the endpooint) // the addresses stored in it before the service is deleted (deletion of the service removes the endpoint)
currentEndpoint, err = c.KubeClient.Endpoints(c.Namespace).Get(c.endpointName(role), metav1.GetOptions{}) currentEndpoint, err = c.KubeClient.Endpoints(c.Namespace).Get(c.endpointName(role), metav1.GetOptions{})
if err != nil { if err != nil {
return fmt.Errorf("could not get current cluster %s endpoints: %v", role, err) return fmt.Errorf("could not get current cluster %s endpoints: %v", role, err)

View File

@ -197,25 +197,25 @@ func (c *Controller) initRoleBinding() {
// operator binds it to the cluster role with sufficient privileges // operator binds it to the cluster role with sufficient privileges
// we assume the role is created by the k8s administrator // we assume the role is created by the k8s administrator
if c.opConfig.PodServiceAccountRoleBindingDefinition == "" { if c.opConfig.PodServiceAccountRoleBindingDefinition == "" {
c.opConfig.PodServiceAccountRoleBindingDefinition = ` c.opConfig.PodServiceAccountRoleBindingDefinition = fmt.Sprintf(`
{ {
"apiVersion": "rbac.authorization.k8s.io/v1beta1", "apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "RoleBinding", "kind": "RoleBinding",
"metadata": { "metadata": {
"name": "zalando-postgres-operator" "name": "%s"
}, },
"roleRef": { "roleRef": {
"apiGroup": "rbac.authorization.k8s.io", "apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole", "kind": "ClusterRole",
"name": "zalando-postgres-operator" "name": "%s"
}, },
"subjects": [ "subjects": [
{ {
"kind": "ServiceAccount", "kind": "ServiceAccount",
"name": "operator" "name": "%s"
} }
] ]
}` }`, c.PodServiceAccount.Name, c.PodServiceAccount.Name, c.PodServiceAccount.Name)
} }
c.logger.Info("Parse role bindings") c.logger.Info("Parse role bindings")
// re-uses k8s internal parsing. See k8s client-go issue #193 for explanation // re-uses k8s internal parsing. See k8s client-go issue #193 for explanation
@ -230,9 +230,6 @@ func (c *Controller) initRoleBinding() {
default: default:
c.PodServiceAccountRoleBinding = obj.(*rbacv1beta1.RoleBinding) c.PodServiceAccountRoleBinding = obj.(*rbacv1beta1.RoleBinding)
c.PodServiceAccountRoleBinding.Namespace = "" c.PodServiceAccountRoleBinding.Namespace = ""
c.PodServiceAccountRoleBinding.ObjectMeta.Name = c.PodServiceAccount.Name
c.PodServiceAccountRoleBinding.RoleRef.Name = c.PodServiceAccount.Name
c.PodServiceAccountRoleBinding.Subjects[0].Name = c.PodServiceAccount.Name
c.logger.Info("successfully parsed") c.logger.Info("successfully parsed")
} }

View File

@ -51,17 +51,18 @@ func (c *Controller) clusterWorkerID(clusterName spec.NamespacedName) uint32 {
func (c *Controller) createOperatorCRD(crd *apiextv1beta1.CustomResourceDefinition) error { func (c *Controller) createOperatorCRD(crd *apiextv1beta1.CustomResourceDefinition) error {
if _, err := c.KubeClient.CustomResourceDefinitions().Create(crd); err != nil { if _, err := c.KubeClient.CustomResourceDefinitions().Create(crd); err != nil {
if !k8sutil.ResourceAlreadyExists(err) { if k8sutil.ResourceAlreadyExists(err) {
return fmt.Errorf("could not create customResourceDefinition: %v", err) c.logger.Infof("customResourceDefinition %q is already registered and will only be updated", crd.Name)
}
c.logger.Infof("customResourceDefinition %q is already registered and will only be updated", crd.Name)
patch, err := json.Marshal(crd) patch, err := json.Marshal(crd)
if err != nil { if err != nil {
return fmt.Errorf("could not marshal new customResourceDefintion: %v", err) return fmt.Errorf("could not marshal new customResourceDefintion: %v", err)
} }
if _, err := c.KubeClient.CustomResourceDefinitions().Patch(crd.Name, types.MergePatchType, patch); err != nil { if _, err := c.KubeClient.CustomResourceDefinitions().Patch(crd.Name, types.MergePatchType, patch); err != nil {
return fmt.Errorf("could not update customResourceDefinition: %v", err) return fmt.Errorf("could not update customResourceDefinition: %v", err)
}
} else {
c.logger.Errorf("could not create customResourceDefinition %q: %v", crd.Name, err)
} }
} else { } else {
c.logger.Infof("customResourceDefinition %q has been registered", crd.Name) c.logger.Infof("customResourceDefinition %q has been registered", crd.Name)