postgres-operator/pkg/controller/util.go

443 lines
14 KiB
Go

package controller
import (
"context"
"encoding/json"
"fmt"
"strings"
v1 "k8s.io/api/core/v1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/cluster"
"github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
"gopkg.in/yaml.v2"
)
func (c *Controller) makeClusterConfig() cluster.Config {
infrastructureRoles := make(map[string]spec.PgUser)
for k, v := range c.config.InfrastructureRoles {
infrastructureRoles[k] = v
}
return cluster.Config{
RestConfig: c.config.RestConfig,
OpConfig: config.Copy(c.opConfig),
PgTeamMap: &c.pgTeamMap,
InfrastructureRoles: infrastructureRoles,
PodServiceAccount: c.PodServiceAccount,
}
}
func (c *Controller) clusterWorkerID(clusterName spec.NamespacedName) uint32 {
workerID, ok := c.clusterWorkers[clusterName]
if ok {
return workerID
}
c.clusterWorkers[clusterName] = c.curWorkerID
if c.curWorkerID == c.opConfig.Workers-1 {
c.curWorkerID = 0
} else {
c.curWorkerID++
}
return c.clusterWorkers[clusterName]
}
func (c *Controller) createOperatorCRD(desiredCrd *apiextv1.CustomResourceDefinition) error {
crd, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), desiredCrd.Name, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) {
if _, err := c.KubeClient.CustomResourceDefinitions().Create(context.TODO(), desiredCrd, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("could not create customResourceDefinition %q: %v", desiredCrd.Name, err)
}
}
if err != nil {
c.logger.Errorf("could not get customResourceDefinition %q: %v", desiredCrd.Name, err)
}
if crd != nil {
c.logger.Infof("customResourceDefinition %q is already registered and will only be updated", crd.Name)
// copy annotations and labels from existing CRD since we do not define them
desiredCrd.Annotations = crd.Annotations
desiredCrd.Labels = crd.Labels
patch, err := json.Marshal(desiredCrd)
if err != nil {
return fmt.Errorf("could not marshal new customResourceDefintion %q: %v", desiredCrd.Name, err)
}
if _, err := c.KubeClient.CustomResourceDefinitions().Patch(
context.TODO(), crd.Name, types.MergePatchType, patch, metav1.PatchOptions{}); err != nil {
return fmt.Errorf("could not update customResourceDefinition %q: %v", crd.Name, err)
}
}
c.logger.Infof("customResourceDefinition %q is registered", crd.Name)
return wait.PollUntilContextTimeout(context.TODO(), c.config.CRDReadyWaitInterval, c.config.CRDReadyWaitTimeout, false, func(ctx context.Context) (bool, error) {
c, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), desiredCrd.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
for _, cond := range c.Status.Conditions {
switch cond.Type {
case apiextv1.Established:
if cond.Status == apiextv1.ConditionTrue {
return true, err
}
case apiextv1.NamesAccepted:
if cond.Status == apiextv1.ConditionFalse {
return false, fmt.Errorf("name conflict: %v", cond.Reason)
}
}
}
return false, err
})
}
func (c *Controller) createPostgresCRD() error {
return c.createOperatorCRD(acidv1.PostgresCRD(c.opConfig.CRDCategories))
}
func (c *Controller) createConfigurationCRD() error {
return c.createOperatorCRD(acidv1.ConfigurationCRD(c.opConfig.CRDCategories))
}
func readDecodedRole(s string) (*spec.PgUser, error) {
var result spec.PgUser
if err := yaml.Unmarshal([]byte(s), &result); err != nil {
return nil, fmt.Errorf("could not decode yaml role: %v", err)
}
return &result, nil
}
var emptyName = (spec.NamespacedName{})
// Return information about what secrets we need to use to create
// infrastructure roles and in which format are they. This is done in
// compatible way, so that the previous logic is not changed, and handles both
// configuration in ConfigMap & CRD.
func (c *Controller) getInfrastructureRoleDefinitions() []*config.InfrastructureRole {
var roleDef config.InfrastructureRole
// take from CRD configuration
rolesDefs := c.opConfig.InfrastructureRoles
// check if we can extract something from the configmap config option
if c.opConfig.InfrastructureRolesDefs != "" {
// The configmap option could contain either a role description (in the
// form key1: value1, key2: value2), which has to be used together with
// an old secret name.
var secretName spec.NamespacedName
var err error
propertySep := ","
valueSep := ":"
// The field contains the format in which secret is written, let's
// convert it to a proper definition
properties := strings.Split(c.opConfig.InfrastructureRolesDefs, propertySep)
roleDef = config.InfrastructureRole{Template: false}
for _, property := range properties {
values := strings.Split(property, valueSep)
if len(values) < 2 {
continue
}
name := strings.TrimSpace(values[0])
value := strings.TrimSpace(values[1])
switch name {
case "secretname":
if err = secretName.DecodeWorker(value, "default"); err != nil {
c.logger.Warningf("Could not marshal secret name %s: %v", value, err)
} else {
roleDef.SecretName = secretName
}
case "userkey":
roleDef.UserKey = value
case "passwordkey":
roleDef.PasswordKey = value
case "rolekey":
roleDef.RoleKey = value
case "defaultuservalue":
roleDef.DefaultUserValue = value
case "defaultrolevalue":
roleDef.DefaultRoleValue = value
default:
c.logger.Warningf("Role description is not known: %s", properties)
}
}
if roleDef.SecretName != emptyName &&
(roleDef.UserKey != "" || roleDef.DefaultUserValue != "") &&
roleDef.PasswordKey != "" {
rolesDefs = append(rolesDefs, &roleDef)
}
}
if c.opConfig.InfrastructureRolesSecretName != emptyName {
// At this point we deal with the old format, let's replicate it
// via existing definition structure and remember that it's just a
// template, the real values are in user1,password1,inrole1 etc.
rolesDefs = append(rolesDefs, &config.InfrastructureRole{
SecretName: c.opConfig.InfrastructureRolesSecretName,
UserKey: "user",
PasswordKey: "password",
RoleKey: "inrole",
Template: true,
})
}
return rolesDefs
}
func (c *Controller) getInfrastructureRoles(
rolesSecrets []*config.InfrastructureRole) (
map[string]spec.PgUser, error) {
errors := make([]string, 0)
noRolesProvided := true
roles := []spec.PgUser{}
uniqRoles := make(map[string]spec.PgUser)
// To be compatible with the legacy implementation we need to return nil if
// the provided secret name is empty. The equivalent situation in the
// current implementation is an empty rolesSecrets slice or all its items
// are empty.
for _, role := range rolesSecrets {
if role.SecretName != emptyName {
noRolesProvided = false
}
}
if noRolesProvided {
return uniqRoles, nil
}
for _, secret := range rolesSecrets {
infraRoles, err := c.getInfrastructureRole(secret)
if err != nil || infraRoles == nil {
c.logger.Debugf("cannot get infrastructure role: %+v", *secret)
if err != nil {
errors = append(errors, fmt.Sprintf("%v", err))
}
continue
}
roles = append(roles, infraRoles...)
}
for _, r := range roles {
if _, exists := uniqRoles[r.Name]; exists {
msg := "conflicting infrastructure roles: roles[%s] = (%q, %q)"
c.logger.Debugf(msg, r.Name, uniqRoles[r.Name], r)
}
uniqRoles[r.Name] = r
}
if len(errors) > 0 {
return uniqRoles, fmt.Errorf("%s", strings.Join(errors, `', '`))
}
return uniqRoles, nil
}
// Generate list of users representing one infrastructure role based on its
// description in various K8S objects. An infrastructure role could be
// described by a secret and optionally a config map. The former should contain
// the secret information, i.e. username, password, role. The latter could
// contain an extensive description of the role and even override an
// information obtained from the secret (except a password).
//
// This function returns a list of users to be compatible with the previous
// behaviour, since we don't know how many users are actually encoded in the
// secret if it's a "template" role. If the provided role is not a template
// one, the result would be a list with just one user in it.
//
// FIXME: This dependency on two different objects is rather unnecessary
// complicated, so let's get rid of it via deprecation process.
func (c *Controller) getInfrastructureRole(
infraRole *config.InfrastructureRole) (
[]spec.PgUser, error) {
rolesSecret := infraRole.SecretName
roles := []spec.PgUser{}
if rolesSecret == emptyName {
// we don't have infrastructure roles defined, bail out
return nil, nil
}
infraRolesSecret, err := c.KubeClient.
Secrets(rolesSecret.Namespace).
Get(context.TODO(), rolesSecret.Name, metav1.GetOptions{})
if err != nil {
msg := "could not get infrastructure roles secret %s/%s: %v"
return nil, fmt.Errorf(msg, rolesSecret.Namespace, rolesSecret.Name, err)
}
secretData := infraRolesSecret.Data
if infraRole.Template {
Users:
for i := 1; i <= len(secretData); i++ {
properties := []string{
infraRole.UserKey,
infraRole.PasswordKey,
infraRole.RoleKey,
}
t := spec.PgUser{Origin: spec.RoleOriginInfrastructure}
for _, p := range properties {
key := fmt.Sprintf("%s%d", p, i)
if val, present := secretData[key]; !present {
if p == "user" {
// exit when the user name with the next sequence id is
// absent
break Users
}
} else {
s := string(val)
switch p {
case "user":
t.Name = s
case "password":
t.Password = s
case "inrole":
t.MemberOf = append(t.MemberOf, s)
default:
c.logger.Warningf("unknown key %q", p)
}
}
// XXX: This is a part of the original implementation, which is
// rather obscure. Why do we delete this key? Wouldn't it be
// used later in comparison for configmap?
delete(secretData, key)
}
if t.Valid() {
roles = append(roles, t)
} else {
msg := "infrastructure role %q is not complete and ignored"
c.logger.Warningf(msg, t)
}
}
} else {
roleDescr := &spec.PgUser{Origin: spec.RoleOriginInfrastructure}
if details, exists := secretData[infraRole.Details]; exists {
if err := yaml.Unmarshal(details, &roleDescr); err != nil {
return nil, fmt.Errorf("could not decode yaml role: %v", err)
}
} else {
roleDescr.Name = util.Coalesce(string(secretData[infraRole.UserKey]), infraRole.DefaultUserValue)
roleDescr.Password = string(secretData[infraRole.PasswordKey])
roleDescr.MemberOf = append(roleDescr.MemberOf,
util.Coalesce(string(secretData[infraRole.RoleKey]), infraRole.DefaultRoleValue))
}
if !roleDescr.Valid() {
msg := "infrastructure role %q is not complete and ignored"
c.logger.Warningf(msg, roleDescr)
return nil, nil
}
if roleDescr.Name == "" {
msg := "infrastructure role %q has no name defined and is ignored"
c.logger.Warningf(msg, roleDescr.Name)
return nil, nil
}
if roleDescr.Password == "" {
msg := "infrastructure role %q has no password defined and is ignored"
c.logger.Warningf(msg, roleDescr.Name)
return nil, nil
}
roles = append(roles, *roleDescr)
}
// Now plot twist. We need to check if there is a configmap with the same
// name and extract a role description if it exists.
infraRolesMap, err := c.KubeClient.
ConfigMaps(rolesSecret.Namespace).
Get(context.TODO(), rolesSecret.Name, metav1.GetOptions{})
if err == nil {
// we have a configmap with username - json description, let's read and decode it
for role, s := range infraRolesMap.Data {
roleDescr, err := readDecodedRole(s)
if err != nil {
return nil, fmt.Errorf("could not decode role description: %v", err)
}
// check if we have a a password in a configmap
c.logger.Debugf("found role description for role %q: %+v", role, roleDescr)
if passwd, ok := secretData[role]; ok {
roleDescr.Password = string(passwd)
delete(secretData, role)
} else {
c.logger.Warningf("infrastructure role %q has no password defined and is ignored", role)
continue
}
roleDescr.Name = role
roleDescr.Origin = spec.RoleOriginInfrastructure
roles = append(roles, *roleDescr)
}
}
// TODO: check for role collisions
return roles, nil
}
func (c *Controller) loadPostgresTeams() {
pgTeams, err := c.KubeClient.PostgresTeamsGetter.PostgresTeams(c.opConfig.WatchedNamespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
c.logger.Errorf("could not list postgres team objects: %v", err)
}
c.pgTeamMap.Load(pgTeams)
c.logger.Debugf("Internal Postgres Team Cache: %#v", c.pgTeamMap)
}
func (c *Controller) postgresTeamAdd(obj interface{}) {
pgTeam, ok := obj.(*acidv1.PostgresTeam)
if !ok {
c.logger.Errorf("could not cast to PostgresTeam spec")
return
}
c.logger.Debugf("PostgreTeam %q added. Reloading postgres team CRDs and overwriting cached map", pgTeam.Name)
c.loadPostgresTeams()
}
func (c *Controller) postgresTeamUpdate(prev, obj interface{}) {
pgTeam, ok := obj.(*acidv1.PostgresTeam)
if !ok {
c.logger.Errorf("could not cast to PostgresTeam spec")
return
}
c.logger.Debugf("PostgreTeam %q updated. Reloading postgres team CRDs and overwriting cached map", pgTeam.Name)
c.loadPostgresTeams()
}
func (c *Controller) podClusterName(pod *v1.Pod) spec.NamespacedName {
if name, ok := pod.Labels[c.opConfig.ClusterNameLabel]; ok {
return spec.NamespacedName{
Namespace: pod.Namespace,
Name: name,
}
}
return spec.NamespacedName{}
}