github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/btpmanager/credentials/manager.go (about)

     1  package btpmgrcreds
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/kyma-project/control-plane/components/provisioner/pkg/gqlschema"
    11  	"github.com/kyma-project/kyma-environment-broker/internal"
    12  	"github.com/kyma-project/kyma-environment-broker/internal/provisioner"
    13  	"github.com/kyma-project/kyma-environment-broker/internal/storage"
    14  	"github.com/kyma-project/kyma-environment-broker/internal/storage/dbmodel"
    15  	"github.com/sirupsen/logrus"
    16  	apicorev1 "k8s.io/api/core/v1"
    17  	v1 "k8s.io/api/core/v1"
    18  	"k8s.io/apimachinery/pkg/api/errors"
    19  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/client-go/tools/clientcmd"
    24  	"sigs.k8s.io/controller-runtime/pkg/client"
    25  )
    26  
    27  const (
    28  	BtpManagerSecretName      = "sap-btp-manager"
    29  	BtpManagerSecretNamespace = "kyma-system"
    30  )
    31  
    32  var (
    33  	BtpManagerLabels      = map[string]string{"app.kubernetes.io/managed-by": keb, "app.kubernetes.io/watched-by": keb}
    34  	BtpManagerAnnotations = map[string]string{"Warning": "This secret is generated. Do not edit!"}
    35  	KymaGvk               = schema.GroupVersionKind{Group: "operator.kyma-project.io", Version: "v1beta2", Kind: "Kyma"}
    36  )
    37  
    38  const (
    39  	keb             = "kcp-kyma-environment-broker"
    40  	kcpNamespace    = "kcp-system"
    41  	instanceIdLabel = "kyma-project.io/instance-id"
    42  )
    43  
    44  const (
    45  	secretClientId     = "clientid"
    46  	secretClientSecret = "clientsecret"
    47  	secretSmUrl        = "sm_url"
    48  	secretTokenUrl     = "tokenurl"
    49  	secretClusterId    = "cluster_id"
    50  )
    51  
    52  type Manager struct {
    53  	ctx          context.Context
    54  	instances    storage.Instances
    55  	kcpK8sClient client.Client
    56  	dryRun       bool
    57  	provisioner  provisioner.Client
    58  	logger       *logrus.Logger
    59  }
    60  
    61  func NewManager(ctx context.Context, kcpK8sClient client.Client, instanceDb storage.Instances, logs *logrus.Logger, dryRun bool, provisioner provisioner.Client) *Manager {
    62  	return &Manager{
    63  		ctx:          ctx,
    64  		instances:    instanceDb,
    65  		kcpK8sClient: kcpK8sClient,
    66  		dryRun:       dryRun,
    67  		provisioner:  provisioner,
    68  		logger:       logs,
    69  	}
    70  }
    71  
    72  func (s *Manager) MatchInstance(kymaName string) (*internal.Instance, error) {
    73  	kyma := &unstructured.Unstructured{}
    74  	kyma.SetGroupVersionKind(KymaGvk)
    75  	err := s.kcpK8sClient.Get(s.ctx, client.ObjectKey{
    76  		Namespace: kcpNamespace,
    77  		Name:      kymaName,
    78  	}, kyma)
    79  	if err != nil && errors.IsNotFound(err) {
    80  		s.logger.Errorf("not found secret with name %s on cluster : %s", kymaName, err)
    81  		return nil, err
    82  	} else if err != nil {
    83  		s.logger.Errorf("unexpected error while getting secret %s from cluster : %s", kymaName, err)
    84  		return nil, err
    85  	}
    86  	s.logger.Infof("found kyma CR on kcp for kyma name: %s", kymaName)
    87  	labels := kyma.GetLabels()
    88  	instanceId, ok := labels[instanceIdLabel]
    89  	if !ok {
    90  		s.logger.Errorf("not found instance for kyma name %s : %s", kymaName, err)
    91  		return nil, err
    92  	}
    93  	s.logger.Infof("found instance id %s for kyma name %s", instanceId, kymaName)
    94  	instance, err := s.instances.GetByID(instanceId)
    95  	if err != nil {
    96  		s.logger.Errorf("while getting instance %s from db %s", instanceId, err)
    97  		return nil, err
    98  	}
    99  	s.logger.Infof("instance %s found in db", instance.InstanceID)
   100  	return instance, err
   101  }
   102  
   103  func (s *Manager) ReconcileAll(jobReconciliationDelay time.Duration) (int, int, int, int, error) {
   104  	instances, err := s.GetReconcileCandidates()
   105  	if err != nil {
   106  		return 0, 0, 0, 0, err
   107  	}
   108  	s.logger.Infof("processing %d instances as candidates", len(instances))
   109  
   110  	updateDone, updateNotDoneDueError, updateNotDoneDueOkState := 0, 0, 0
   111  	for _, instance := range instances {
   112  		time.Sleep(jobReconciliationDelay)
   113  		updated, err := s.ReconcileSecretForInstance(&instance)
   114  		if err != nil {
   115  			s.logger.Errorf("while doing update, for instance: %s, %s", instance.InstanceID, err)
   116  			updateNotDoneDueError++
   117  			continue
   118  		}
   119  		if updated {
   120  			s.logger.Infof("update done for instance %s", instance.InstanceID)
   121  			updateDone++
   122  		} else {
   123  			s.logger.Infof("no need to update instance %s", instance.InstanceID)
   124  			updateNotDoneDueOkState++
   125  		}
   126  	}
   127  	s.logger.Infof("(runtime-reconciler summary) from total %d instances: %d are OK, update was needed (and done with success) for %d instances, errors occur for %d instances",
   128  		len(instances), updateNotDoneDueOkState, updateDone, updateNotDoneDueError)
   129  	return len(instances), updateDone, updateNotDoneDueError, updateNotDoneDueOkState, nil
   130  }
   131  
   132  func (s *Manager) GetReconcileCandidates() ([]internal.Instance, error) {
   133  	allInstances, _, _, err := s.instances.List(dbmodel.InstanceFilter{})
   134  	if err != nil {
   135  		return nil, fmt.Errorf("while getting all instances %s", err)
   136  	}
   137  	s.logger.Infof("total number of instances in db: %d", len(allInstances))
   138  
   139  	var instancesWithinRuntime []internal.Instance
   140  	for _, instance := range allInstances {
   141  		if !instance.Reconcilable {
   142  			s.logger.Infof("skipping instance %s because it is not reconilable (no runtimeId,last op was deprovisoning or op is in progress)", instance.InstanceID)
   143  			continue
   144  		}
   145  
   146  		if instance.Parameters.ErsContext.SMOperatorCredentials == nil || instance.InstanceDetails.ServiceManagerClusterID == "" {
   147  			s.logger.Warnf("skipping instance %s because there are no needed data attached to instance", instance.InstanceID)
   148  			continue
   149  		}
   150  
   151  		instancesWithinRuntime = append(instancesWithinRuntime, instance)
   152  		s.logger.Infof("adding instance %s as candidate for reconcilation", instance.InstanceID)
   153  	}
   154  
   155  	s.logger.Infof("from total number of instances (%d) took %d as candidates", len(allInstances), len(instancesWithinRuntime))
   156  	return instancesWithinRuntime, nil
   157  }
   158  
   159  func (s *Manager) ReconcileSecretForInstance(instance *internal.Instance) (bool, error) {
   160  	s.logger.Infof("reconcilation of btp-manager secret started for %s", instance.InstanceID)
   161  
   162  	futureSecret, err := PrepareSecret(instance.Parameters.ErsContext.SMOperatorCredentials, instance.InstanceDetails.ServiceManagerClusterID)
   163  	if err != nil {
   164  		return false, err
   165  	}
   166  
   167  	k8sClient, err := s.getSkrK8sClient(instance)
   168  	if err != nil {
   169  		return false, fmt.Errorf("while getting k8sClient for %s : %w", instance.InstanceID, err)
   170  	}
   171  	s.logger.Infof("connected to skr with success for instance %s", instance.InstanceID)
   172  
   173  	currentSecret := &v1.Secret{}
   174  	err = k8sClient.Get(context.Background(), client.ObjectKey{Name: BtpManagerSecretName, Namespace: BtpManagerSecretNamespace}, currentSecret)
   175  	if err != nil && errors.IsNotFound(err) {
   176  		s.logger.Infof("not found btp-manager secret on cluster for instance: %s", instance.InstanceID)
   177  		if s.dryRun {
   178  			s.logger.Infof("[dry-run] secret for instance %s would be created", instance.InstanceID)
   179  		} else {
   180  			if err := CreateOrUpdateSecret(k8sClient, futureSecret, s.logger); err != nil {
   181  				s.logger.Errorf("while creating secret in cluster for %s", instance.InstanceID)
   182  				return false, err
   183  			}
   184  			s.logger.Infof("created btp-manager secret on cluster for instance %s successfully", instance.InstanceID)
   185  		}
   186  		return true, nil
   187  	} else if err != nil {
   188  		return false, fmt.Errorf("while getting secret from cluster for instance %s : %s", instance.InstanceID, err)
   189  	}
   190  
   191  	notMatchingKeys, err := s.compareSecrets(currentSecret, futureSecret)
   192  	if err != nil {
   193  		return false, fmt.Errorf("validation of secrets failed with unexpected reason for instance: %s : %s", instance.InstanceID, err)
   194  	} else if len(notMatchingKeys) > 0 {
   195  		s.logger.Infof("btp-manager secret on cluster does not match for instance credentials in db : %s, incorrect values for keys: %s ", instance.InstanceID, strings.Join(notMatchingKeys, ","))
   196  		if s.dryRun {
   197  			s.logger.Infof("[dry-run] secret for instance %s would be updated", instance.InstanceID)
   198  		} else {
   199  			if err := CreateOrUpdateSecret(k8sClient, futureSecret, s.logger); err != nil {
   200  				s.logger.Errorf("while updating secret in cluster for %s %s", instance.InstanceID, err)
   201  				return false, err
   202  			}
   203  			s.logger.Infof("btp-manager secret on cluster updated for %s to match state from instances db", instance.InstanceID)
   204  		}
   205  		return true, nil
   206  	} else {
   207  		s.logger.Infof("instance %s OK: btp-manager secret on cluster match within expected data", instance.InstanceID)
   208  	}
   209  
   210  	return false, nil
   211  }
   212  
   213  func (s *Manager) getSkrK8sClient(instance *internal.Instance) (client.Client, error) {
   214  	secretName := getKubeConfigSecretName(instance.RuntimeID)
   215  	kubeConfigSecret := &v1.Secret{}
   216  	err := s.kcpK8sClient.Get(s.ctx, client.ObjectKey{Name: secretName, Namespace: kcpNamespace}, kubeConfigSecret)
   217  	if err != nil && !errors.IsNotFound(err) {
   218  		return nil, fmt.Errorf("while getting secret from kcp for %s : %w", instance.InstanceID, err)
   219  	}
   220  
   221  	var kubeConfig []byte
   222  	if errors.IsNotFound(err) {
   223  		s.logger.Infof("not found secret for %s, now it will be executed try to get kubeConfig from provisioner.", instance.InstanceID)
   224  		status, err := CallWithRetry(func() (gqlschema.RuntimeStatus, error) {
   225  			return s.provisioner.RuntimeStatus(instance.Parameters.ErsContext.GlobalAccountID, instance.RuntimeID)
   226  		}, 5, time.Second*5)
   227  		if err != nil {
   228  			return nil, fmt.Errorf("while getting runtime status from provisioner for %s : %s", instance.InstanceID, err)
   229  		}
   230  
   231  		if status.RuntimeConfiguration.Kubeconfig == nil {
   232  			return nil, fmt.Errorf("kubeconfig empty in provisioner response for %s", instance.InstanceID)
   233  		}
   234  
   235  		s.logger.Infof("found kubeconfig in provisioner for %s", instance.InstanceID)
   236  		kubeConfig = []byte(*status.RuntimeConfiguration.Kubeconfig)
   237  	} else {
   238  		s.logger.Infof("found secret %s on kcp cluster for %s", secretName, instance.InstanceID)
   239  
   240  		config, ok := kubeConfigSecret.Data["config"]
   241  		if !ok {
   242  			return nil, fmt.Errorf("while getting 'config' from secret from %s for %s", secretName, instance.InstanceID)
   243  		}
   244  
   245  		s.logger.Infof("found kubeconfig in secret %s for %s", secretName, instance.InstanceID)
   246  		kubeConfig = config
   247  	}
   248  
   249  	if kubeConfig == nil || len(kubeConfig) == 0 {
   250  		return nil, fmt.Errorf("not found kubeConfig as secret nor in provisioner or is empty for %s", instance.InstanceID)
   251  	}
   252  
   253  	k8sClient, err := CallWithRetry(func() (client.Client, error) {
   254  		restCfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfig)
   255  		if err != nil {
   256  			return nil, fmt.Errorf("while making REST cfg from kube config string for %s : %s", instance.InstanceID, err)
   257  		}
   258  		k8sClient, err := client.New(restCfg, client.Options{})
   259  		if err != nil {
   260  			return nil, fmt.Errorf("while creating k8sClient from REST config for %s : %s", instance.InstanceID, err)
   261  		}
   262  		return k8sClient, nil
   263  	}, 5, time.Second*5)
   264  
   265  	if err != nil {
   266  		return nil, fmt.Errorf("while creating k8sClient from REST config for %s : %s", instance.InstanceID, err)
   267  	}
   268  
   269  	return k8sClient, nil
   270  }
   271  
   272  func (s *Manager) compareSecrets(s1, s2 *v1.Secret) ([]string, error) {
   273  	areSecretEqualByKey := func(key string) (bool, error) {
   274  		currentValue, ok := s1.Data[key]
   275  		if !ok {
   276  			return false, fmt.Errorf("while getting the value for the  key %s in the first secret", key)
   277  		}
   278  		expectedValue, ok := s2.Data[key]
   279  		if !ok {
   280  			return false, fmt.Errorf("while getting the value for the key %s in the second secret", key)
   281  		}
   282  		return reflect.DeepEqual(currentValue, expectedValue), nil
   283  	}
   284  
   285  	notEqual := make([]string, 0)
   286  	for _, key := range []string{secretClientSecret, secretClientId, secretSmUrl, secretTokenUrl, secretClusterId} {
   287  		equal, err := areSecretEqualByKey(key)
   288  		if err != nil {
   289  			s.logger.Errorf("getting value for key %s", key)
   290  			return nil, err
   291  		}
   292  		if !equal {
   293  			notEqual = append(notEqual, key)
   294  		}
   295  	}
   296  
   297  	return notEqual, nil
   298  }
   299  
   300  func getKubeConfigSecretName(runtimeId string) string {
   301  	return fmt.Sprintf("kubeconfig-%s", runtimeId)
   302  }
   303  
   304  func PrepareSecret(credentials *internal.ServiceManagerOperatorCredentials, clusterID string) (*apicorev1.Secret, error) {
   305  	if credentials == nil || clusterID == "" {
   306  		return nil, fmt.Errorf("empty params given")
   307  	}
   308  	if credentials.ClientID == "" {
   309  		return nil, fmt.Errorf("client Id not set")
   310  	}
   311  	if credentials.ClientSecret == "" {
   312  		return nil, fmt.Errorf("clients ecret not set")
   313  	}
   314  	if credentials.ServiceManagerURL == "" {
   315  		return nil, fmt.Errorf("service manager url not set")
   316  	}
   317  	if credentials.URL == "" {
   318  		return nil, fmt.Errorf("url not set")
   319  	}
   320  
   321  	return &v1.Secret{
   322  		TypeMeta: metav1.TypeMeta{Kind: "Secret"},
   323  		ObjectMeta: metav1.ObjectMeta{
   324  			Name:        BtpManagerSecretName,
   325  			Namespace:   BtpManagerSecretNamespace,
   326  			Labels:      BtpManagerLabels,
   327  			Annotations: BtpManagerAnnotations,
   328  		},
   329  		Data: map[string][]byte{
   330  			secretClientId:     []byte(credentials.ClientID),
   331  			secretClientSecret: []byte(credentials.ClientSecret),
   332  			secretSmUrl:        []byte(credentials.ServiceManagerURL),
   333  			secretTokenUrl:     []byte(credentials.URL),
   334  			secretClusterId:    []byte(clusterID),
   335  		},
   336  		Type: apicorev1.SecretTypeOpaque,
   337  	}, nil
   338  }
   339  
   340  func CreateOrUpdateSecret(k8sClient client.Client, futureSecret *apicorev1.Secret, log logrus.FieldLogger) error {
   341  	if futureSecret == nil {
   342  		return fmt.Errorf("empty secret data given")
   343  	}
   344  	currentSecret := apicorev1.Secret{}
   345  	getErr := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: BtpManagerSecretNamespace, Name: BtpManagerSecretName}, &currentSecret)
   346  	switch {
   347  	case getErr != nil && !apierrors.IsNotFound(getErr):
   348  		return fmt.Errorf("failed to get the secret for BTP Manager: %s", getErr)
   349  	case getErr != nil && apierrors.IsNotFound(getErr):
   350  		namespace := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: BtpManagerSecretNamespace}}
   351  		createErr := k8sClient.Create(context.Background(), namespace)
   352  		if createErr != nil && !apierrors.IsAlreadyExists(createErr) {
   353  			return fmt.Errorf("could not create %s namespace: %s", BtpManagerSecretNamespace, createErr)
   354  		}
   355  
   356  		createErr = k8sClient.Create(context.Background(), futureSecret)
   357  		if createErr != nil {
   358  			return fmt.Errorf("failed to create the secret for BTP Manager: %s", createErr)
   359  		}
   360  
   361  		log.Info("the secret for BTP Manager created")
   362  		return nil
   363  	default:
   364  		if !reflect.DeepEqual(currentSecret.Labels, BtpManagerLabels) {
   365  			log.Warnf("the secret %s was not created by KEB and its data will be overwritten", BtpManagerSecretName)
   366  		}
   367  
   368  		currentSecret.Data = futureSecret.Data
   369  		currentSecret.ObjectMeta.Labels = futureSecret.ObjectMeta.Labels
   370  		currentSecret.ObjectMeta.Annotations = futureSecret.ObjectMeta.Annotations
   371  		updateErr := k8sClient.Update(context.Background(), &currentSecret)
   372  		if updateErr != nil {
   373  			return fmt.Errorf("failed to update the secret for BTP Manager: %s", updateErr)
   374  		}
   375  
   376  		log.Info("the secret for BTP Manager updated")
   377  		return nil
   378  	}
   379  }