mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-09-30 23:01:14 +02:00
Implement replicaLoadBalancer boolean flag. (#38)
The flag adds a replica service with the name cluster_name-repl and
a DNS name that defaults to {cluster}-repl.{team}.{hostedzone}.
The implementation converted Service field of the cluster into a map
with one or two elements and deals with the cases when the new flag
is changed on a running cluster
(the update and the sync should create or delete the replica service).
In order to pick up master and replica service and master endpoint
when listing cluster resources.
* Update the spec when updating the cluster.
This commit is contained in:
+14
-13
@@ -43,19 +43,20 @@ type Config struct {
|
||||
TPR
|
||||
Resources
|
||||
Auth
|
||||
Namespace string `name:"namespace"`
|
||||
EtcdHost string `name:"etcd_host" default:"etcd-client.default.svc.cluster.local:2379"`
|
||||
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spiloprivate-9.6:1.2-p4"`
|
||||
ServiceAccountName string `name:"service_account_name" default:"operator"`
|
||||
DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"`
|
||||
EtcdScope string `name:"etcd_scope" default:"service"`
|
||||
WALES3Bucket string `name:"wal_s3_bucket"`
|
||||
KubeIAMRole string `name:"kube_iam_role"`
|
||||
DebugLogging bool `name:"debug_logging" default:"true"`
|
||||
EnableDBAccess bool `name:"enable_database_access" default:"true"`
|
||||
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`
|
||||
DNSNameFormat stringTemplate `name:"dns_name_format" default:"{cluster}.{team}.{hostedzone}"`
|
||||
Workers uint32 `name:"workers" default:"4"`
|
||||
Namespace string `name:"namespace"`
|
||||
EtcdHost string `name:"etcd_host" default:"etcd-client.default.svc.cluster.local:2379"`
|
||||
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spiloprivate-9.6:1.2-p4"`
|
||||
ServiceAccountName string `name:"service_account_name" default:"operator"`
|
||||
DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"`
|
||||
EtcdScope string `name:"etcd_scope" default:"service"`
|
||||
WALES3Bucket string `name:"wal_s3_bucket"`
|
||||
KubeIAMRole string `name:"kube_iam_role"`
|
||||
DebugLogging bool `name:"debug_logging" default:"true"`
|
||||
EnableDBAccess bool `name:"enable_database_access" default:"true"`
|
||||
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`
|
||||
MasterDNSNameFormat stringTemplate `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"`
|
||||
ReplicaDNSNameFormat stringTemplate `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"`
|
||||
Workers uint32 `name:"workers" default:"4"`
|
||||
}
|
||||
|
||||
func (c Config) MustMarshal() string {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Decoder interface {
|
||||
type decoder interface {
|
||||
Decode(value string) error
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ type fieldInfo struct {
|
||||
|
||||
type stringTemplate string
|
||||
|
||||
func decoderFrom(field reflect.Value) (d Decoder) {
|
||||
func decoderFrom(field reflect.Value) (d decoder) {
|
||||
// it may be impossible for a struct field to fail this check
|
||||
if !field.CanInterface() {
|
||||
return
|
||||
}
|
||||
|
||||
d, ok := field.Interface().(Decoder)
|
||||
d, ok := field.Interface().(decoder)
|
||||
if !ok && field.CanAddr() {
|
||||
d, _ = field.Addr().Interface().(Decoder)
|
||||
d, _ = field.Addr().Interface().(decoder)
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package constants
|
||||
|
||||
// Names and values in Kubernetes annotation for services, statefulsets and volumes
|
||||
const (
|
||||
ZalandoDNSNameAnnotation = "external-dns.alpha.kubernetes.io/hostname"
|
||||
ElbTimeoutAnnotationName = "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout"
|
||||
|
||||
@@ -2,8 +2,11 @@ package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// AWS specific constants used by other modules
|
||||
const (
|
||||
AWS_REGION = "eu-central-1"
|
||||
// default region for AWS. TODO: move it to the operator configuration
|
||||
AWS_REGION = "eu-central-1"
|
||||
// EBS related constants
|
||||
EBSVolumeIDStart = "/vol-"
|
||||
EBSProvisioner = "kubernetes.io/aws-ebs"
|
||||
//https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_VolumeModification.html
|
||||
|
||||
@@ -2,6 +2,7 @@ package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// General kubernetes-related constants
|
||||
const (
|
||||
ListClustersURITemplate = "/apis/" + TPRVendor + "/" + TPRApiVersion + "/namespaces/%s/" + ResourceName // Namespace
|
||||
WatchClustersURITemplate = "/apis/" + TPRVendor + "/" + TPRApiVersion + "/watch/namespaces/%s/" + ResourceName // Namespace
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package constants
|
||||
|
||||
// Different properties of the PostgreSQL Third Party Resources
|
||||
const (
|
||||
TPRName = "postgresql"
|
||||
TPRVendor = "acid.zalan.do"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package constants
|
||||
|
||||
// Measurement-unit definitions
|
||||
const (
|
||||
Gigabyte = 1073741824
|
||||
)
|
||||
|
||||
@@ -11,19 +11,22 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
EXT2 = "ext2"
|
||||
EXT3 = "ext3"
|
||||
EXT4 = "ext4"
|
||||
ext2 = "ext2"
|
||||
ext3 = "ext3"
|
||||
ext4 = "ext4"
|
||||
resize2fs = "resize2fs"
|
||||
)
|
||||
|
||||
// Ext234Resize implements the FilesystemResizer interface for the ext4/3/2fs.
|
||||
type Ext234Resize struct {
|
||||
}
|
||||
|
||||
// CanResizeFilesystem checks whether Ext234Resize can resize this filesystem.
|
||||
func (c *Ext234Resize) CanResizeFilesystem(fstype string) bool {
|
||||
return fstype == EXT2 || fstype == EXT3 || fstype == EXT4
|
||||
return fstype == ext2 || fstype == ext3 || fstype == ext4
|
||||
}
|
||||
|
||||
// ResizeFilesystem calls resize2fs to resize the filesystem if necessary.
|
||||
func (c *Ext234Resize) ResizeFilesystem(deviceName string, commandExecutor func(cmd string) (out string, err error)) error {
|
||||
command := fmt.Sprintf("%s %s 2>&1", resize2fs, deviceName)
|
||||
out, err := commandExecutor(command)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type ConditionFunc func() (bool, error)
|
||||
|
||||
// Retry calls ConditionFunc until it returns boolean true, a timeout expires or an error occurs.
|
||||
func Retry(interval time.Duration, timeout time.Duration, f ConditionFunc) error {
|
||||
//TODO: make the retry exponential
|
||||
if timeout < interval {
|
||||
|
||||
+10
-5
@@ -9,7 +9,8 @@ import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type InfrastructureAccount struct {
|
||||
// InfrastructureAccount defines an account of the team on some infrastructure (i.e AWS, Google) platform.
|
||||
type infrastructureAccount struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
@@ -20,7 +21,8 @@ type InfrastructureAccount struct {
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type Team struct {
|
||||
// Team defines informaiton for a single team, including the list of members and infrastructure accounts.
|
||||
type team struct {
|
||||
Dn string `json:"dn"`
|
||||
ID string `json:"id"`
|
||||
TeamName string `json:"id_name"`
|
||||
@@ -34,15 +36,17 @@ type Team struct {
|
||||
DeliveryLead string `json:"delivery_lead"`
|
||||
ParentTeamID string `json:"parent_team_id"`
|
||||
|
||||
InfrastructureAccounts []InfrastructureAccount `json:"infrastructure-accounts"`
|
||||
InfrastructureAccounts []infrastructureAccount `json:"infrastructure-accounts"`
|
||||
}
|
||||
|
||||
//
|
||||
type API struct {
|
||||
url string
|
||||
httpClient *http.Client
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// NewTeamsAPI creates an object to query the team API.
|
||||
func NewTeamsAPI(url string, log *logrus.Logger) *API {
|
||||
t := API{
|
||||
url: strings.TrimRight(url, "/"),
|
||||
@@ -53,7 +57,8 @@ func NewTeamsAPI(url string, log *logrus.Logger) *API {
|
||||
return &t
|
||||
}
|
||||
|
||||
func (t *API) TeamInfo(teamID, token string) (*Team, error) {
|
||||
// TeamInfo returns information about a given team using its ID and a token to authenticate to the API service.
|
||||
func (t *API) TeamInfo(teamID, token string) (*team, error) {
|
||||
url := fmt.Sprintf("%s/teams/%s", t.url, teamID)
|
||||
t.logger.Debugf("Request url: %s", url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
@@ -81,7 +86,7 @@ func (t *API) TeamInfo(teamID, token string) (*Team, error) {
|
||||
|
||||
return nil, fmt.Errorf("team API query failed with status code %d", resp.StatusCode)
|
||||
}
|
||||
teamInfo := &Team{}
|
||||
teamInfo := &team{}
|
||||
d := json.NewDecoder(resp.Body)
|
||||
err = d.Decode(teamInfo)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ var (
|
||||
var teamsAPItc = []struct {
|
||||
in string
|
||||
inCode int
|
||||
out *Team
|
||||
out *team
|
||||
err error
|
||||
}{
|
||||
{`{
|
||||
@@ -66,7 +66,7 @@ var teamsAPItc = []struct {
|
||||
"parent_team_id": "111221"
|
||||
}`,
|
||||
200,
|
||||
&Team{
|
||||
&team{
|
||||
Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
ID: "acid",
|
||||
TeamName: "ACID",
|
||||
@@ -79,7 +79,7 @@ var teamsAPItc = []struct {
|
||||
CostCenter: "00099999",
|
||||
DeliveryLead: "member4",
|
||||
ParentTeamID: "111221",
|
||||
InfrastructureAccounts: []InfrastructureAccount{
|
||||
InfrastructureAccounts: []infrastructureAccount{
|
||||
{
|
||||
ID: "1234512345",
|
||||
Name: "acid",
|
||||
|
||||
@@ -18,9 +18,14 @@ const (
|
||||
inRoleTemplate = `IN ROLE %s`
|
||||
)
|
||||
|
||||
// DefaultUserSyncStrategy implements a user sync strategy that merges already existing database users
|
||||
// with those defined in the manifest, altering existing users when necessary. It will never strips
|
||||
// an existing roles of another role membership, nor it removes the already assigned flag
|
||||
// (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
|
||||
type DefaultUserSyncStrategy struct {
|
||||
}
|
||||
|
||||
// ProduceSyncRequests figures out the types of changes that need to happen with the given users.
|
||||
func (s DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserMap,
|
||||
newUsers spec.PgUserMap) (reqs []spec.PgSyncUserRequest) {
|
||||
|
||||
@@ -55,6 +60,7 @@ func (s DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserMap,
|
||||
return
|
||||
}
|
||||
|
||||
// ExecuteSyncRequests makes actual database changes from the requests passed in its arguments.
|
||||
func (s DefaultUserSyncStrategy) ExecuteSyncRequests(reqs []spec.PgSyncUserRequest, db *sql.DB) error {
|
||||
for _, r := range reqs {
|
||||
switch r.Kind {
|
||||
|
||||
+5
-6
@@ -3,7 +3,6 @@ package util
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -24,6 +23,7 @@ func init() {
|
||||
rand.Seed(int64(time.Now().Unix()))
|
||||
}
|
||||
|
||||
// RandomPassword generates random alphanumeric password of a given length.
|
||||
func RandomPassword(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
@@ -33,6 +33,7 @@ func RandomPassword(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// NameFromMeta converts a metadata object to the NamespacedName name representation.
|
||||
func NameFromMeta(meta v1.ObjectMeta) spec.NamespacedName {
|
||||
return spec.NamespacedName{
|
||||
Namespace: meta.Namespace,
|
||||
@@ -40,6 +41,7 @@ func NameFromMeta(meta v1.ObjectMeta) spec.NamespacedName {
|
||||
}
|
||||
}
|
||||
|
||||
// PGUserPassword is used to generate md5 password hash for a given user. It does nothing for already hashed passwords.
|
||||
func PGUserPassword(user spec.PgUser) string {
|
||||
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) || user.Password == "" {
|
||||
// Avoid processing already encrypted or empty passwords
|
||||
@@ -49,17 +51,14 @@ func PGUserPassword(user spec.PgUser) string {
|
||||
return md5prefix + hex.EncodeToString(s[:])
|
||||
}
|
||||
|
||||
func Pretty(x interface{}) (f fmt.Formatter) {
|
||||
return pretty.Formatter(x)
|
||||
}
|
||||
|
||||
// PrettyDiff shows the diff between 2 objects in an easy to understand format. It is mainly used for debugging output.
|
||||
func PrettyDiff(a, b interface{}) (result string) {
|
||||
diff := pretty.Diff(a, b)
|
||||
return strings.Join(diff, "\n")
|
||||
}
|
||||
|
||||
// SubstractStringSlices finds elements in a that are not in b and return them as a result slice.
|
||||
func SubstractStringSlices(a []string, b []string) (result []string, equal bool) {
|
||||
// Find elements in a that are not in b and return them as a result slice
|
||||
// Slices are assumed to contain unique elements only
|
||||
OUTER:
|
||||
for _, vala := range a {
|
||||
|
||||
@@ -13,10 +13,12 @@ import (
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
)
|
||||
|
||||
// EBSVolumeResizer implements volume resizing interface for AWS EBS volumes.
|
||||
type EBSVolumeResizer struct {
|
||||
connection *ec2.EC2
|
||||
}
|
||||
|
||||
// ConnectToProvider connects to AWS.
|
||||
func (c *EBSVolumeResizer) ConnectToProvider() error {
|
||||
sess, err := session.NewSession(&aws.Config{Region: aws.String(constants.AWS_REGION)})
|
||||
if err != nil {
|
||||
@@ -26,10 +28,12 @@ func (c *EBSVolumeResizer) ConnectToProvider() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnectedToProvider checks if AWS connection is established.
|
||||
func (c *EBSVolumeResizer) IsConnectedToProvider() bool {
|
||||
return c.connection != nil
|
||||
}
|
||||
|
||||
// VolumeBelongsToProvider checks if the given persistent volume is backed by EBS.
|
||||
func (c *EBSVolumeResizer) VolumeBelongsToProvider(pv *v1.PersistentVolume) bool {
|
||||
return pv.Spec.AWSElasticBlockStore != nil && pv.Annotations[constants.VolumeStorateProvisionerAnnotation] == constants.EBSProvisioner
|
||||
}
|
||||
@@ -47,6 +51,7 @@ func (c *EBSVolumeResizer) GetProviderVolumeID(pv *v1.PersistentVolume) (string,
|
||||
return volumeID[idx:], nil
|
||||
}
|
||||
|
||||
// ResizeVolume actually calls AWS API to resize the EBS volume if necessary.
|
||||
func (c *EBSVolumeResizer) ResizeVolume(volumeId string, newSize int64) error {
|
||||
/* first check if the volume is already of a requested size */
|
||||
volumeOutput, err := c.connection.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIds: []*string{&volumeId}})
|
||||
@@ -89,7 +94,8 @@ func (c *EBSVolumeResizer) ResizeVolume(volumeId string, newSize int64) error {
|
||||
return false, fmt.Errorf("describe volume modification didn't return one record for volume \"%s\"", volumeId)
|
||||
}
|
||||
if *out.VolumesModifications[0].VolumeId != volumeId {
|
||||
return false, fmt.Errorf("non-matching volume id when describing modifications: \"%s\" is different from \"%s\"")
|
||||
return false, fmt.Errorf("non-matching volume id when describing modifications: \"%s\" is different from \"%s\"",
|
||||
*out.VolumesModifications[0].VolumeId, volumeId)
|
||||
}
|
||||
return *out.VolumesModifications[0].ModificationState != constants.EBSVolumeStateModifying, nil
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
)
|
||||
|
||||
// VolumeResizer defines the set of methods used to implememnt provider-specific resizing of persistent volumes.
|
||||
type VolumeResizer interface {
|
||||
ConnectToProvider() error
|
||||
IsConnectedToProvider() bool
|
||||
|
||||
Reference in New Issue
Block a user