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:
Oleksii Kliukin
2017-06-07 13:54:17 +02:00
committed by GitHub
parent 7b0ca31bfb
commit dc36c4ca12
26 changed files with 329 additions and 166 deletions
+14 -13
View File
@@ -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 {
+4 -4
View File
@@ -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
View File
@@ -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"
+4 -1
View File
@@ -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
+1
View File
@@ -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
View File
@@ -1,5 +1,6 @@
package constants
// Different properties of the PostgreSQL Third Party Resources
const (
TPRName = "postgresql"
TPRVendor = "acid.zalan.do"
+1
View File
@@ -1,5 +1,6 @@
package constants
// Measurement-unit definitions
const (
Gigabyte = 1073741824
)
+7 -4
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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 {
+3 -3
View File
@@ -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",
+6
View File
@@ -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
View File
@@ -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 {
+7 -1
View File
@@ -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
})
+1
View File
@@ -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