diff --git a/CODEOWNERS b/CODEOWNERS index 6d1a2ab6aefe8db175c8925dc784c3a9d12eace6..512c4b2534366e22a527e60c0a39d6d1db6e2938 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # global owners -* @sdudoladov @Jan-M @CyberDem0n @FxKu @jopadi +* @sdudoladov @Jan-M @CyberDem0n @FxKu @jopadi @idanovinda diff --git a/MAINTAINERS b/MAINTAINERS index b2abdf85857b4569f6e7f4009c0fc1332230b68d..2af9b9e17668d1da9f8d2e7e7e5d3bea47658ebe 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,4 +1,5 @@ Sergey Dudoladov <sergey.dudoladov@zalando.de> Felix Kunde <felix.kunde@zalando.de> Jan Mussler <jan.mussler@zalando.de> -Jociele Padilha <jociele.padilha@zalando.de> \ No newline at end of file +Jociele Padilha <jociele.padilha@zalando.de> +Ida Novindasari <ida.novindasari@zalando.de> \ No newline at end of file diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 85f60b601dd63257eada195111d32b99a7c310a0..a51c9871e748000daf60cd2ba993e41194f40f64 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -1032,12 +1032,20 @@ func (c *Cluster) processPodEvent(obj interface{}) error { return fmt.Errorf("could not cast to PodEvent") } + // can only take lock when (un)registerPodSubscriber is finshed c.podSubscribersMu.RLock() subscriber, ok := c.podSubscribers[spec.NamespacedName(event.PodName)] - c.podSubscribersMu.RUnlock() if ok { - subscriber <- event + select { + case subscriber <- event: + default: + // ending up here when there is no receiver on the channel (i.e. waitForPodLabel finished) + // avoids blocking channel: https://gobyexample.com/non-blocking-channel-operations + } } + // hold lock for the time of processing the event to avoid race condition + // with unregisterPodSubscriber closing the channel (see #1876) + c.podSubscribersMu.RUnlock() return nil } @@ -1501,34 +1509,16 @@ func (c *Cluster) Switchover(curMaster *v1.Pod, candidate spec.NamespacedName) e var err error c.logger.Debugf("switching over from %q to %q", curMaster.Name, candidate) c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeNormal, "Switchover", "Switching over from %q to %q", curMaster.Name, candidate) - - var wg sync.WaitGroup - - podLabelErr := make(chan error) stopCh := make(chan struct{}) - - wg.Add(1) - - go func() { - defer wg.Done() - ch := c.registerPodSubscriber(candidate) - defer c.unregisterPodSubscriber(candidate) - - role := Master - - select { - case <-stopCh: - case podLabelErr <- func() (err2 error) { - _, err2 = c.waitForPodLabel(ch, stopCh, &role) - return - }(): - } - }() + ch := c.registerPodSubscriber(candidate) + defer c.unregisterPodSubscriber(candidate) + defer close(stopCh) if err = c.patroni.Switchover(curMaster, candidate.Name); err == nil { c.logger.Debugf("successfully switched over from %q to %q", curMaster.Name, candidate) c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeNormal, "Switchover", "Successfully switched over from %q to %q", curMaster.Name, candidate) - if err = <-podLabelErr; err != nil { + _, err = c.waitForPodLabel(ch, stopCh, nil) + if err != nil { err = fmt.Errorf("could not get master pod label: %v", err) } } else { @@ -1536,14 +1526,6 @@ func (c *Cluster) Switchover(curMaster *v1.Pod, candidate spec.NamespacedName) e c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeNormal, "Switchover", "Switchover from %q to %q FAILED: %v", curMaster.Name, candidate, err) } - // signal the role label waiting goroutine to close the shop and go home - close(stopCh) - // wait until the goroutine terminates, since unregisterPodSubscriber - // must be called before the outer return; otherwise we risk subscribing to the same pod twice. - wg.Wait() - // close the label waiting channel no sooner than the waiting goroutine terminates. - close(podLabelErr) - return err } diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 5b2f4c745c7c3e1a1fb4f5dea10a631458accba8..8d0d16df29ac9b70f82ec6eac48cc3135aa326c2 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -940,7 +940,6 @@ func (c *Cluster) generateSpiloPodEnvVars( func appendEnvVars(envs []v1.EnvVar, appEnv ...v1.EnvVar) []v1.EnvVar { collectedEnvs := envs for _, env := range appEnv { - env.Name = strings.ToUpper(env.Name) if !isEnvVarPresent(collectedEnvs, env.Name) { collectedEnvs = append(collectedEnvs, env) } @@ -950,7 +949,7 @@ func appendEnvVars(envs []v1.EnvVar, appEnv ...v1.EnvVar) []v1.EnvVar { func isEnvVarPresent(envs []v1.EnvVar, key string) bool { for _, env := range envs { - if env.Name == key { + if strings.EqualFold(env.Name, key) { return true } } diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 7a1bdeaf9de24c06c133fcfd645501dade5f2957..d966344e4e22b62d3c73c25372b0bf4f6ee2eb6d 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -504,7 +504,7 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { expectedS3BucketConfigMap := []ExpectedValue{ { envIndex: 17, - envVarConstant: "WAL_S3_BUCKET", + envVarConstant: "wal_s3_bucket", envVarValue: "global-s3-bucket-configmap", }, } @@ -518,7 +518,7 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { expectedCustomVariableSecret := []ExpectedValue{ { envIndex: 16, - envVarConstant: "CUSTOM_VARIABLE", + envVarConstant: "custom_variable", envVarValueRef: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ @@ -532,7 +532,7 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { expectedCustomVariableConfigMap := []ExpectedValue{ { envIndex: 16, - envVarConstant: "CUSTOM_VARIABLE", + envVarConstant: "custom_variable", envVarValue: "configmap-test", }, } @@ -573,14 +573,14 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { }, { envIndex: 20, - envVarConstant: "CLONE_AWS_ENDPOINT", + envVarConstant: "clone_aws_endpoint", envVarValue: "s3.eu-west-1.amazonaws.com", }, } expectedCloneEnvSecret := []ExpectedValue{ { envIndex: 20, - envVarConstant: "CLONE_AWS_ACCESS_KEY_ID", + envVarConstant: "clone_aws_access_key_id", envVarValueRef: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ @@ -599,7 +599,7 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { }, { envIndex: 20, - envVarConstant: "STANDBY_GOOGLE_APPLICATION_CREDENTIALS", + envVarConstant: "standby_google_application_credentials", envVarValueRef: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ diff --git a/pkg/cluster/pod.go b/pkg/cluster/pod.go index 26c4c332d05290f6319b0c0e667a15f89e10d8b5..74ee5998771903fb879f26beca66518d29e20ac1 100644 --- a/pkg/cluster/pod.go +++ b/pkg/cluster/pod.go @@ -67,7 +67,7 @@ func (c *Cluster) markRollingUpdateFlagForPod(pod *v1.Pod, msg string) error { return fmt.Errorf("could not form patch for pod's rolling update flag: %v", err) } - err = retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout, + err = retryutil.Retry(1*time.Second, 5*time.Second, func() (bool, error) { _, err2 := c.KubeClient.Pods(pod.Namespace).Patch( context.TODO(), @@ -151,12 +151,13 @@ func (c *Cluster) unregisterPodSubscriber(podName spec.NamespacedName) { c.podSubscribersMu.Lock() defer c.podSubscribersMu.Unlock() - if _, ok := c.podSubscribers[podName]; !ok { + ch, ok := c.podSubscribers[podName] + if !ok { panic("subscriber for pod '" + podName.String() + "' is not found") } - close(c.podSubscribers[podName]) delete(c.podSubscribers, podName) + close(ch) } func (c *Cluster) registerPodSubscriber(podName spec.NamespacedName) chan PodEvent { @@ -399,11 +400,12 @@ func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error) } func (c *Cluster) recreatePod(podName spec.NamespacedName) (*v1.Pod, error) { + stopCh := make(chan struct{}) ch := c.registerPodSubscriber(podName) defer c.unregisterPodSubscriber(podName) - stopChan := make(chan struct{}) + defer close(stopCh) - err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout, + err := retryutil.Retry(1*time.Second, 5*time.Second, func() (bool, error) { err2 := c.KubeClient.Pods(podName.Namespace).Delete( context.TODO(), @@ -421,7 +423,7 @@ func (c *Cluster) recreatePod(podName spec.NamespacedName) (*v1.Pod, error) { if err := c.waitForPodDeletion(ch); err != nil { return nil, err } - pod, err := c.waitForPodLabel(ch, stopChan, nil) + pod, err := c.waitForPodLabel(ch, stopCh, nil) if err != nil { return nil, err } @@ -446,7 +448,7 @@ func (c *Cluster) recreatePods(pods []v1.Pod, switchoverCandidates []spec.Namesp continue } - podName := util.NameFromMeta(pod.ObjectMeta) + podName := util.NameFromMeta(pods[i].ObjectMeta) newPod, err := c.recreatePod(podName) if err != nil { return fmt.Errorf("could not recreate replica pod %q: %v", util.NameFromMeta(pod.ObjectMeta), err) @@ -520,13 +522,13 @@ func (c *Cluster) getSwitchoverCandidate(master *v1.Pod) (spec.NamespacedName, e // if sync_standby replicas were found assume synchronous_mode is enabled and ignore other candidates list if len(syncCandidates) > 0 { sort.Slice(syncCandidates, func(i, j int) bool { - return util.IntFromIntStr(syncCandidates[i].Lag) < util.IntFromIntStr(syncCandidates[j].Lag) + return syncCandidates[i].Lag < syncCandidates[j].Lag }) return spec.NamespacedName{Namespace: master.Namespace, Name: syncCandidates[0].Name}, nil } if len(candidates) > 0 { sort.Slice(candidates, func(i, j int) bool { - return util.IntFromIntStr(candidates[i].Lag) < util.IntFromIntStr(candidates[j].Lag) + return candidates[i].Lag < candidates[j].Lag }) return spec.NamespacedName{Namespace: master.Namespace, Name: candidates[0].Name}, nil } diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 0f71d2d642ea3825a92c3af1827655877818e649..0bfda78bfb05d2da96dd4fef012f7c9ba237606a 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -316,7 +316,7 @@ func (c *Cluster) annotationsSet(annotations map[string]string) map[string]strin return nil } -func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopChan chan struct{}, role *PostgresRole) (*v1.Pod, error) { +func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopCh chan struct{}, role *PostgresRole) (*v1.Pod, error) { timeout := time.After(c.OpConfig.PodLabelWaitTimeout) for { select { @@ -332,7 +332,7 @@ func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopChan chan struct{ } case <-timeout: return nil, fmt.Errorf("pod label wait timeout") - case <-stopChan: + case <-stopCh: return nil, fmt.Errorf("pod label wait cancelled") } } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index de0dec69f39f317c5118c05aad912a845e0d3dad..e46b9ee4468af8661e8715b136faf8fbe14135dd 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -451,7 +451,7 @@ func (c *Controller) Run(stopCh <-chan struct{}, wg *sync.WaitGroup) { panic("could not acquire initial list of clusters") } - wg.Add(5) + wg.Add(5 + util.Bool2Int(c.opConfig.EnablePostgresTeamCRD)) go c.runPodInformer(stopCh, wg) go c.runPostgresqlInformer(stopCh, wg) go c.clusterResync(stopCh, wg) diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index aac078933aba2ef16dca2f2ffd63a81f7551b709..4e07b4e0d6c638b943f60752c99d6593bc0a1344 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -225,7 +225,7 @@ func (c *Controller) processEvent(event ClusterEvent) { switch event.EventType { case EventAdd: if clusterFound { - lg.Infof("recieved add event for already existing Postgres cluster") + lg.Infof("received add event for already existing Postgres cluster") return } diff --git a/pkg/util/patroni/patroni.go b/pkg/util/patroni/patroni.go index 8126eddc7c28feb5a454c196fd238054ab2c14ce..7f8f633746548e31de9d39a86f30556527e7c306 100644 --- a/pkg/util/patroni/patroni.go +++ b/pkg/util/patroni/patroni.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math" "net" "net/http" "strconv" @@ -16,7 +17,6 @@ import ( "github.com/sirupsen/logrus" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/intstr" ) const ( @@ -185,11 +185,27 @@ type ClusterMembers struct { // ClusterMember cluster member data from Patroni API type ClusterMember struct { - Name string `json:"name"` - Role string `json:"role"` - State string `json:"state"` - Timeline int `json:"timeline"` - Lag intstr.IntOrString `json:"lag,omitempty"` + Name string `json:"name"` + Role string `json:"role"` + State string `json:"state"` + Timeline int `json:"timeline"` + Lag ReplicationLag `json:"lag,omitempty"` +} + +type ReplicationLag uint64 + +// UnmarshalJSON converts member lag (can be int or string) into uint64 +func (rl *ReplicationLag) UnmarshalJSON(data []byte) error { + var lagUInt64 uint64 + if data[0] == '"' { + *rl = math.MaxUint64 + return nil + } + if err := json.Unmarshal(data, &lagUInt64); err != nil { + return err + } + *rl = ReplicationLag(lagUInt64) + return nil } // MemberDataPatroni child element diff --git a/pkg/util/patroni/patroni_test.go b/pkg/util/patroni/patroni_test.go index aa6ad920605d531f95a6feeb0c580d1ebe798b40..216a46b864e15619376891049d0d786e41349e1e 100644 --- a/pkg/util/patroni/patroni_test.go +++ b/pkg/util/patroni/patroni_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "math" "net/http" "reflect" "testing" @@ -15,7 +16,6 @@ import ( acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/intstr" ) var logger = logrus.New().WithField("test", "patroni") @@ -101,16 +101,27 @@ func TestGetClusterMembers(t *testing.T) { Role: "sync_standby", State: "running", Timeline: 1, - Lag: intstr.IntOrString{IntVal: 0}, + Lag: 0, }, { Name: "acid-test-cluster-2", Role: "replica", State: "running", Timeline: 1, - Lag: intstr.IntOrString{Type: 1, StrVal: "unknown"}, + Lag: math.MaxUint64, + }, { + Name: "acid-test-cluster-3", + Role: "replica", + State: "running", + Timeline: 1, + Lag: 3000000000, }} - json := `{"members": [{"name": "acid-test-cluster-0", "role": "leader", "state": "running", "api_url": "http://192.168.100.1:8008/patroni", "host": "192.168.100.1", "port": 5432, "timeline": 1}, {"name": "acid-test-cluster-1", "role": "sync_standby", "state": "running", "api_url": "http://192.168.100.2:8008/patroni", "host": "192.168.100.2", "port": 5432, "timeline": 1, "lag": 0}, {"name": "acid-test-cluster-2", "role": "replica", "state": "running", "api_url": "http://192.168.100.3:8008/patroni", "host": "192.168.100.3", "port": 5432, "timeline": 1, "lag": "unknown"}]}` + json := `{"members": [ + {"name": "acid-test-cluster-0", "role": "leader", "state": "running", "api_url": "http://192.168.100.1:8008/patroni", "host": "192.168.100.1", "port": 5432, "timeline": 1}, + {"name": "acid-test-cluster-1", "role": "sync_standby", "state": "running", "api_url": "http://192.168.100.2:8008/patroni", "host": "192.168.100.2", "port": 5432, "timeline": 1, "lag": 0}, + {"name": "acid-test-cluster-2", "role": "replica", "state": "running", "api_url": "http://192.168.100.3:8008/patroni", "host": "192.168.100.3", "port": 5432, "timeline": 1, "lag": "unknown"}, + {"name": "acid-test-cluster-3", "role": "replica", "state": "running", "api_url": "http://192.168.100.3:8008/patroni", "host": "192.168.100.3", "port": 5432, "timeline": 1, "lag": 3000000000} + ]}` r := ioutil.NopCloser(bytes.NewReader([]byte(json))) response := http.Response{ diff --git a/pkg/util/util.go b/pkg/util/util.go index 688153b89158bf5a7cca2efd3da846e88cf19215..3eb9c5301dfd7a542096fb196c38f6b403e6d299 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/hex" "fmt" - "math" "math/big" "math/rand" "reflect" @@ -324,18 +323,18 @@ func testNil(values ...*int32) bool { return false } -// Convert int to IntOrString type +// ToIntStr converts int to IntOrString type func ToIntStr(val int) *intstr.IntOrString { b := intstr.FromInt(val) return &b } -// Get int from IntOrString and return max int if string -func IntFromIntStr(intOrStr intstr.IntOrString) int { - if intOrStr.Type == 1 { - return math.MaxInt +// Bool2Int converts bool to int +func Bool2Int(flag bool) int { + if flag { + return 1 } - return intOrStr.IntValue() + return 0 } // MaxInt32 : Return maximum of two integers provided via pointers. If one value diff --git a/ui/app/src/postgresqls.tag.pug b/ui/app/src/postgresqls.tag.pug index fabf769a038081bea8c9a2cf5b3579220bffcb5a..742bb2968c1ec97a037230bdfe981485a2a6741a 100644 --- a/ui/app/src/postgresqls.tag.pug +++ b/ui/app/src/postgresqls.tag.pug @@ -51,7 +51,23 @@ postgresqls th(style='width: 140px') CPU th(style='width: 130px') Memory th(style='width: 100px') Size - th(style='width: 120px') Cost/Month + th(style='width: 100px') IOPS + th(style='width: 100px') Throughput + th(style='width: 120px') + .tooltip(style='width: 120px') + | Cost/Month + .tooltiptext + strong Cost = MAX(CPU, Memory) + rest + br + | 1 CPU core : 42.09$ + br + | 1GB memory: 10.5225$ + br + | 1GB volume: 0.0952$ + br + | IOPS (-3000 baseline): 0.006$ + br + | Throughput (-125 baseline): 0.0476$ th(stlye='width: 120px') tbody @@ -69,6 +85,8 @@ postgresqls td { cpu } / { cpu_limit } td { memory } / { memory_limit } td { volume_size } + td { iops } + td { throughput } td { calcCosts(nodes, cpu, memory, volume_size, iops, throughput) }$ td @@ -132,7 +150,23 @@ postgresqls th(style='width: 140px') CPU th(style='width: 130px') Memory th(style='width: 100px') Size - th(style='width: 120px') Cost/Month + th(style='width: 100px') IOPS + th(style='width: 100px') Throughput + th(style='width: 120px') + .tooltip(style='width: 120px') + | Cost/Month + .tooltiptext + strong Cost = MAX(CPU, Memory) + rest + br + | 1 CPU core : 42.09$ + br + | 1GB memory: 10.5225$ + br + | 1GB volume: 0.0952$ + br + | IOPS (-3000 baseline): 0.006$ + br + | Throughput (-125 baseline): 0.0476$ th(stlye='width: 120px') tbody @@ -152,6 +186,8 @@ postgresqls td { cpu } / { cpu_limit } td { memory } / { memory_limit } td { volume_size } + td { iops } + td { throughput } td { calcCosts(nodes, cpu, memory, volume_size, iops, throughput) }$ td @@ -229,28 +265,44 @@ postgresqls const calcCosts = this.calcCosts = (nodes, cpu, memory, disk, iops, throughput) => { podcount = Math.max(nodes, opts.config.min_pods) - corecost = toCores(cpu) * opts.config.cost_core - memorycost = toMemory(memory) * opts.config.cost_memory + corecost = toCores(cpu) * opts.config.cost_core * 30.5 * 24 + memorycost = toMemory(memory) * opts.config.cost_memory * 30.5 * 24 diskcost = toDisk(disk) * opts.config.cost_ebs iopscost = 0 - if (iops !== undefined && iops > 3000) { - iopscost = (iops - 3000) * opts.config.cost_iops + if (iops !== undefined && iops > opts.config.free_iops) { + if (iops > opts.config.limit_iops) { + iops = opts.config.limit_iops + } + iopscost = (iops - opts.config.free_iops) * opts.config.cost_iops } throughputcost = 0 - if (throughput !== undefined && throughput > 125) { - throughputcost = (throughput - 125) * opts.config.cost_throughput + if (throughput !== undefined && throughput > opts.config.free_throughput) { + if (throughput > opts.config.limit_throughput) { + throughput = opts.config.limit_throughput + } + throughputcost = (throughput - opts.config.free_throughput) * opts.config.cost_throughput } - costs = podcount * (corecost + memorycost + diskcost + iopscost + throughputcost) + costs = podcount * (Math.max(corecost, memorycost) + diskcost + iopscost + throughputcost) return costs.toFixed(2) } const toDisk = this.toDisk = value => { - if(value.endsWith("Gi")) { + if(value.endsWith("Mi")) { + value = value.substring(0, value.length-2) + value = Number(value) / 1000. + return value + } + else if(value.endsWith("Gi")) { value = value.substring(0, value.length-2) value = Number(value) return value } + else if(value.endsWith("Ti")) { + value = value.substring(0, value.length-2) + value = Number(value) * 1000 + return value + } return value } diff --git a/ui/manifests/deployment.yaml b/ui/manifests/deployment.yaml index 4edd999e424e6e660974bba8bbe778afe7614779..60722fd92a7636a9a05567d9215450fac6bb706c 100644 --- a/ui/manifests/deployment.yaml +++ b/ui/manifests/deployment.yaml @@ -67,6 +67,10 @@ spec: "cost_throughput": 0.0476, "cost_core": 0.0575, "cost_memory": 0.014375, + "free_iops": 3000, + "free_throughput": 125, + "limit_iops": 16000, + "limit_throughput": 1000, "postgresql_versions": [ "14", "13", diff --git a/ui/operator_ui/main.py b/ui/operator_ui/main.py index 579f0e7d8b8ac55c6bd0a713f9c1c9a991e5cefc..b671c4a01d5546f6453a4a14eec03f24c48ed7a0 100644 --- a/ui/operator_ui/main.py +++ b/ui/operator_ui/main.py @@ -82,12 +82,16 @@ OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-nam OPERATOR_UI_CONFIG = getenv('OPERATOR_UI_CONFIG', '{}') OPERATOR_UI_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}') READ_ONLY_MODE = getenv('READ_ONLY_MODE', False) in [True, 'true'] -RESOURCES_VISIBLE = getenv('RESOURCES_VISIBLE', True) SPILO_S3_BACKUP_PREFIX = getenv('SPILO_S3_BACKUP_PREFIX', 'spilo/') SUPERUSER_TEAM = getenv('SUPERUSER_TEAM', 'acid') TARGET_NAMESPACE = getenv('TARGET_NAMESPACE') GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False) MIN_PODS= getenv('MIN_PODS', 2) +RESOURCES_VISIBLE = getenv('RESOURCES_VISIBLE', True) +CUSTOM_MESSAGE_RED = getenv('CUSTOM_MESSAGE_RED', '') + +APPLICATION_DEPLOYMENT_DOCS = getenv('APPLICATION_DEPLOYMENT_DOCS', '') +CONNECTION_DOCS = getenv('CONNECTION_DOCS', '') # storage pricing, i.e. https://aws.amazon.com/ebs/pricing/ (e.g. Europe - Franfurt) COST_EBS = float(getenv('COST_EBS', 0.0952)) # GB per month @@ -95,8 +99,19 @@ COST_IOPS = float(getenv('COST_IOPS', 0.006)) # IOPS per month above 3000 basel COST_THROUGHPUT = float(getenv('COST_THROUGHPUT', 0.0476)) # MB/s per month above 125 MB/s baseline # compute costs, i.e. https://www.ec2instances.info/?region=eu-central-1&selected=m5.2xlarge -COST_CORE = 30.5 * 24 * float(getenv('COST_CORE', 0.0575)) # Core per hour m5.2xlarge / 8. -COST_MEMORY = 30.5 * 24 * float(getenv('COST_MEMORY', 0.014375)) # Memory GB m5.2xlarge / 32. +COST_CORE = float(getenv('COST_CORE', 0.0575)) # Core per hour m5.2xlarge / 8. +COST_MEMORY = float(getenv('COST_MEMORY', 0.014375)) # Memory GB m5.2xlarge / 32. + +# maximum and limitation of IOPS and throughput +FREE_IOPS = float(getenv('FREE_IOPS', 3000)) +LIMIT_IOPS = float(getenv('LIMIT_IOPS', 16000)) +FREE_THROUGHPUT = float(getenv('FREE_THROUGHPUT', 125)) +LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 1000)) +# get the default value of core and memory +DEFAULT_MEMORY = getenv('DEFAULT_MEMORY', '300Mi') +DEFAULT_MEMORY_LIMIT = getenv('DEFAULT_MEMORY_LIMIT', '300Mi') +DEFAULT_CPU = getenv('DEFAULT_CPU', '10m') +DEFAULT_CPU_LIMIT = getenv('DEFAULT_CPU_LIMIT', '300m') WALE_S3_ENDPOINT = getenv( 'WALE_S3_ENDPOINT', @@ -304,29 +319,34 @@ DEFAULT_UI_CONFIG = { 'nat_gateways_visible': True, 'users_visible': True, 'databases_visible': True, - 'resources_visible': True, - 'postgresql_versions': ['11','12','13'], + 'resources_visible': RESOURCES_VISIBLE, + 'postgresql_versions': ['11','12','13','14'], 'dns_format_string': '{0}.{1}.{2}', 'pgui_link': '', 'static_network_whitelist': {}, + 'read_only_mode': READ_ONLY_MODE, + 'superuser_team': SUPERUSER_TEAM, + 'target_namespace': TARGET_NAMESPACE, + 'connection_docs': CONNECTION_DOCS, + 'application_deployment_docs': APPLICATION_DEPLOYMENT_DOCS, 'cost_ebs': COST_EBS, 'cost_iops': COST_IOPS, 'cost_throughput': COST_THROUGHPUT, 'cost_core': COST_CORE, 'cost_memory': COST_MEMORY, - 'min_pods': MIN_PODS + 'min_pods': MIN_PODS, + 'free_iops': FREE_IOPS, + 'free_throughput': FREE_THROUGHPUT, + 'limit_iops': LIMIT_IOPS, + 'limit_throughput': LIMIT_THROUGHPUT } @app.route('/config') @authorize def get_config(): - config = loads(OPERATOR_UI_CONFIG) or DEFAULT_UI_CONFIG - config['read_only_mode'] = READ_ONLY_MODE - config['resources_visible'] = RESOURCES_VISIBLE - config['superuser_team'] = SUPERUSER_TEAM - config['target_namespace'] = TARGET_NAMESPACE - config['min_pods'] = MIN_PODS + config = DEFAULT_UI_CONFIG.copy() + config.update(loads(OPERATOR_UI_CONFIG)) config['namespaces'] = ( [TARGET_NAMESPACE] @@ -961,11 +981,13 @@ def get_operator_get_logs(worker: int): @app.route('/operator/clusters/<namespace>/<cluster>/logs') @authorize def get_operator_get_logs_per_cluster(namespace: str, cluster: str): + team, cluster_name = cluster.split('-', 1) + # team id might contain hyphens, try to find correct team name user_teams = get_teams_for_user(session.get('user_name', '')) for user_team in user_teams: - if cluster.find(user_team) == 0: + if cluster.find(user_team + '-') == 0: team = cluster[:len(user_team)] - cluster_name = cluster[len(user_team)+1:] + cluster_name = cluster[len(user_team + '-'):] break return proxy_operator(f'/clusters/{team}/{namespace}/{cluster_name}/logs/') diff --git a/ui/operator_ui/static/styles.css b/ui/operator_ui/static/styles.css index 3f05cb29023a39a13a4d6037ff6cac8784ad6f63..42182cc7657e01f1e6c5f7449ea8ee5b79c8a6d4 100644 --- a/ui/operator_ui/static/styles.css +++ b/ui/operator_ui/static/styles.css @@ -64,3 +64,56 @@ label { td { vertical-align: middle !important; } + +.tooltip { + position: relative; + display: inline-block; + opacity: 1; + font-size: 14px; + font-weight: bold; +} +.tooltip:after { + content: '?'; + display: inline-block; + font-family: sans-serif; + font-weight: bold; + text-align: center; + width: 16px; + height: 16px; + font-size: 12px; + line-height: 16px; + border-radius: 12px; + padding: 0px; + color: white; + background: black; + border: 1px solid black; +} +.tooltip .tooltiptext { + visibility: hidden; + width: 250px; + background-color: white; + color: #000; + text-align: justify; + border-radius: 6px; + padding: 10px 10px; + position: absolute; + z-index: 1; + bottom: 150%; + left: 50%; + margin-left: -120px; + border: 1px solid black; + font-weight: normal; +} +.tooltip .tooltiptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: black transparent transparent transparent; +} +.tooltip:hover .tooltiptext { + visibility: visible; +}