github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/cmd/tenantfetcher-svc/main.go (about)

     1  /*
     2   * Copyright 2020 The Compass Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"net/http"
    24  	"os"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/kyma-incubator/compass/components/director/internal/authenticator/claims"
    29  	auth "github.com/kyma-incubator/compass/components/director/pkg/auth-middleware"
    30  
    31  	"github.com/google/uuid"
    32  	"github.com/gorilla/mux"
    33  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    34  	"github.com/kyma-incubator/compass/components/director/internal/features"
    35  	"github.com/kyma-incubator/compass/components/director/internal/metrics"
    36  	tenantfetcher "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc"
    37  	"github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc/resync"
    38  	configprovider "github.com/kyma-incubator/compass/components/director/pkg/config"
    39  	"github.com/kyma-incubator/compass/components/director/pkg/correlation"
    40  	"github.com/kyma-incubator/compass/components/director/pkg/executor"
    41  	graphqlclient "github.com/kyma-incubator/compass/components/director/pkg/graphql_client"
    42  	timeouthandler "github.com/kyma-incubator/compass/components/director/pkg/handler"
    43  	httputil "github.com/kyma-incubator/compass/components/director/pkg/http"
    44  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    45  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    46  	"github.com/kyma-incubator/compass/components/director/pkg/signal"
    47  	tenant2 "github.com/kyma-incubator/compass/components/director/pkg/tenant"
    48  	gcli "github.com/machinebox/graphql"
    49  	"github.com/pkg/errors"
    50  	"github.com/tidwall/gjson"
    51  	"github.com/vrischmann/envconfig"
    52  )
    53  
    54  const envPrefix = "APP"
    55  
    56  type config struct {
    57  	Address string `envconfig:"default=127.0.0.1:8080"`
    58  
    59  	ServerTimeout   time.Duration `envconfig:"default=110s"`
    60  	ShutdownTimeout time.Duration `envconfig:"default=10s"`
    61  
    62  	Log log.Config
    63  
    64  	TenantsRootAPI string `envconfig:"APP_ROOT_API,default=/tenants"`
    65  
    66  	Handler tenantfetcher.HandlerConfig
    67  
    68  	Features features.Config
    69  
    70  	SecurityConfig securityConfig
    71  }
    72  
    73  type securityConfig struct {
    74  	JWKSSyncPeriod            time.Duration `envconfig:"default=5m"`
    75  	AllowJWTSigningNone       bool          `envconfig:"APP_ALLOW_JWT_SIGNING_NONE,default=false"`
    76  	JwksEndpoint              string        `envconfig:"default=file://hack/default-jwks.json,APP_JWKS_ENDPOINT"`
    77  	SubscriptionCallbackScope string        `envconfig:"APP_SUBSCRIPTION_CALLBACK_SCOPE"`
    78  	FetchTenantOnDemandScope  string        `envconfig:"APP_FETCH_TENANT_ON_DEMAND_SCOPE"`
    79  }
    80  
    81  func main() {
    82  	ctx, cancel := context.WithCancel(context.Background())
    83  
    84  	defer cancel()
    85  
    86  	term := make(chan os.Signal)
    87  	signal.HandleInterrupts(ctx, cancel, term)
    88  
    89  	cfg := config{}
    90  	err := envconfig.InitWithPrefix(&cfg, envPrefix)
    91  	exitOnError(err, "Error while loading app config")
    92  
    93  	ctx, err = log.Configure(ctx, &cfg.Log)
    94  	exitOnError(err, "Failed to configure Logger")
    95  
    96  	tenantSynchronizers, dbCloseFuncs := tenantSynchronizers(ctx, cfg.Handler, cfg.Features)
    97  	defer func() {
    98  		for _, fn := range dbCloseFuncs {
    99  			if err := fn(); err != nil {
   100  				log.D().WithError(err).Error("Error while closing the connection to the database")
   101  			}
   102  		}
   103  	}()
   104  
   105  	for _, sync := range tenantSynchronizers {
   106  		log.C(ctx).Infof("Starting tenant synchronizer %s...", sync.Name())
   107  		go func(synchronizer *resync.TenantsSynchronizer) {
   108  			synchronizeTenants(synchronizer, ctx)
   109  		}(sync)
   110  	}
   111  
   112  	httpClient := &http.Client{
   113  		Transport: httputil.NewCorrelationIDTransport(httputil.NewHTTPTransportWrapper(http.DefaultTransport.(*http.Transport))),
   114  		CheckRedirect: func(req *http.Request, via []*http.Request) error {
   115  			return http.ErrUseLastResponse
   116  		},
   117  	}
   118  
   119  	handler := initAPIHandler(ctx, httpClient, cfg, tenantSynchronizers)
   120  	runMainSrv, shutdownMainSrv := createServer(ctx, cfg, handler, "main")
   121  
   122  	go func() {
   123  		<-ctx.Done()
   124  		// Interrupt signal received - shut down the servers
   125  		shutdownMainSrv()
   126  	}()
   127  
   128  	runMainSrv()
   129  }
   130  
   131  func stopTenantFetcherJobTicker(ctx context.Context, tenantFetcherJobTicker *time.Ticker, jobName string) {
   132  	tenantFetcherJobTicker.Stop()
   133  	log.C(ctx).Infof("Ticker for tenant fetcher job %s is stopped", jobName)
   134  }
   135  
   136  func initAPIHandler(ctx context.Context, httpClient *http.Client, cfg config, synchronizers []*resync.TenantsSynchronizer) http.Handler {
   137  	const (
   138  		healthzEndpoint = "/healthz"
   139  		readyzEndpoint  = "/readyz"
   140  	)
   141  	logger := log.C(ctx)
   142  	mainRouter := mux.NewRouter()
   143  	mainRouter.Use(correlation.AttachCorrelationIDToContext(), log.RequestLogger(
   144  		cfg.TenantsRootAPI+healthzEndpoint, cfg.TenantsRootAPI+readyzEndpoint))
   145  
   146  	tenantsAPIRouter := mainRouter.PathPrefix(cfg.TenantsRootAPI).Subrouter()
   147  	configureAuthMiddleware(ctx, httpClient, tenantsAPIRouter, cfg.SecurityConfig, cfg.SecurityConfig.SubscriptionCallbackScope)
   148  	registerTenantsHandler(ctx, tenantsAPIRouter, cfg.Handler)
   149  
   150  	tenantsOnDemandAPIRouter := mainRouter.PathPrefix(cfg.TenantsRootAPI).Subrouter()
   151  	configureAuthMiddleware(ctx, httpClient, tenantsOnDemandAPIRouter, cfg.SecurityConfig, cfg.SecurityConfig.FetchTenantOnDemandScope)
   152  	registerTenantsOnDemandHandler(ctx, tenantsOnDemandAPIRouter, cfg.Handler, synchronizers)
   153  
   154  	healthCheckRouter := mainRouter.PathPrefix(cfg.TenantsRootAPI).Subrouter()
   155  	logger.Infof("Registering readiness endpoint...")
   156  	healthCheckRouter.HandleFunc(readyzEndpoint, newReadinessHandler())
   157  	logger.Infof("Registering liveness endpoint...")
   158  	healthCheckRouter.HandleFunc(healthzEndpoint, newReadinessHandler())
   159  
   160  	return mainRouter
   161  }
   162  
   163  func exitOnError(err error, context string) {
   164  	if err != nil {
   165  		wrappedError := errors.Wrap(err, context)
   166  		log.D().Fatal(wrappedError)
   167  	}
   168  }
   169  
   170  func createServer(ctx context.Context, cfg config, handler http.Handler, name string) (func(), func()) {
   171  	logger := log.C(ctx)
   172  
   173  	handlerWithTimeout, err := timeouthandler.WithTimeout(handler, cfg.ServerTimeout)
   174  	exitOnError(err, "Error while configuring tenant mapping handler")
   175  
   176  	srv := &http.Server{
   177  		Addr:              cfg.Address,
   178  		Handler:           handlerWithTimeout,
   179  		ReadHeaderTimeout: cfg.ServerTimeout,
   180  	}
   181  
   182  	runFn := func() {
   183  		logger.Infof("Running %s server on %s...", name, cfg.Address)
   184  		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
   185  			logger.Errorf("%s HTTP server ListenAndServe: %v", name, err)
   186  		}
   187  	}
   188  
   189  	shutdownFn := func() {
   190  		ctx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
   191  		defer cancel()
   192  
   193  		logger.Infof("Shutting down %s server...", name)
   194  		if err := srv.Shutdown(ctx); err != nil {
   195  			logger.Errorf("%s HTTP server Shutdown: %v", name, err)
   196  		}
   197  	}
   198  
   199  	return runFn, shutdownFn
   200  }
   201  
   202  func configureAuthMiddleware(ctx context.Context, httpClient *http.Client, router *mux.Router, cfg securityConfig, requiredScopes ...string) {
   203  	scopeValidator := claims.NewScopesValidator(requiredScopes)
   204  	middleware := auth.New(httpClient, cfg.JwksEndpoint, cfg.AllowJWTSigningNone, "", scopeValidator)
   205  	router.Use(middleware.Handler())
   206  
   207  	log.C(ctx).Infof("JWKS synchronization enabled. Sync period: %v", cfg.JWKSSyncPeriod)
   208  	periodicExecutor := executor.NewPeriodic(cfg.JWKSSyncPeriod, func(ctx context.Context) {
   209  		if err := middleware.SynchronizeJWKS(ctx); err != nil {
   210  			log.C(ctx).WithError(err).Errorf("An error has occurred while synchronizing JWKS: %v", err)
   211  		}
   212  	})
   213  	go periodicExecutor.Run(ctx)
   214  }
   215  
   216  func registerTenantsHandler(ctx context.Context, router *mux.Router, cfg tenantfetcher.HandlerConfig) {
   217  	gqlClient := newInternalGraphQLClient(cfg.DirectorGraphQLEndpoint, cfg.ClientTimeout, cfg.HTTPClientSkipSslValidation)
   218  	directorClient := graphqlclient.NewDirector(gqlClient)
   219  
   220  	tenantConverter := tenant.NewConverter()
   221  
   222  	provisioner := tenantfetcher.NewTenantProvisioner(directorClient, tenantConverter, cfg.TenantProvider)
   223  	subscriber := tenantfetcher.NewSubscriber(directorClient, provisioner)
   224  
   225  	dependencies, err := dependenciesConfigToMap(cfg)
   226  	exitOnError(err, "failed to read service dependencies")
   227  	cfg.RegionToDependenciesConfig = dependencies
   228  
   229  	tenantHandler := tenantfetcher.NewTenantsHTTPHandler(subscriber, cfg)
   230  
   231  	log.C(ctx).Infof("Registering Regional Tenant Onboarding endpoint on %s...", cfg.RegionalHandlerEndpoint)
   232  	router.HandleFunc(cfg.RegionalHandlerEndpoint, tenantHandler.SubscribeTenant).Methods(http.MethodPut)
   233  
   234  	log.C(ctx).Infof("Registering Regional Tenant Decommissioning endpoint on %s...", cfg.RegionalHandlerEndpoint)
   235  	router.HandleFunc(cfg.RegionalHandlerEndpoint, tenantHandler.UnSubscribeTenant).Methods(http.MethodDelete)
   236  
   237  	log.C(ctx).Infof("Registering service dependencies endpoint on %s...", cfg.DependenciesEndpoint)
   238  	router.HandleFunc(cfg.DependenciesEndpoint, tenantHandler.Dependencies).Methods(http.MethodGet)
   239  }
   240  
   241  func tenantSynchronizers(ctx context.Context, taConfig tenantfetcher.HandlerConfig, featuresConfig features.Config) ([]*resync.TenantsSynchronizer, []func() error) {
   242  	envVars := resync.ReadFromEnvironment(os.Environ())
   243  	jobNames := resync.GetJobNames(envVars)
   244  	log.C(ctx).Infof("Tenant fetcher jobs are: %s", strings.Join(jobNames, ","))
   245  
   246  	jobConfigs := make([]resync.JobConfig, 0)
   247  	for _, job := range jobNames {
   248  		jobConfig, err := resync.NewTenantFetcherJobEnvironment(ctx, job, envVars).ReadJobConfig()
   249  		exitOnError(err, fmt.Sprintf("Error while reading job config for job %s", job))
   250  		jobConfigs = append(jobConfigs, *jobConfig)
   251  	}
   252  
   253  	gqlClient := newInternalGraphQLClient(taConfig.DirectorGraphQLEndpoint, taConfig.ClientTimeout, taConfig.HTTPClientSkipSslValidation)
   254  	directorClient := graphqlclient.NewDirector(gqlClient)
   255  
   256  	dbCloseFunctions := make([]func() error, 0)
   257  	synchronizers := make([]*resync.TenantsSynchronizer, 0)
   258  	for _, jobConfig := range jobConfigs {
   259  		transact, closeFunc, err := persistence.Configure(ctx, taConfig.Database)
   260  		exitOnError(err, "Error while establishing the connection to the database")
   261  
   262  		metricsCfg := metrics.PusherConfig{
   263  			Enabled:    len(taConfig.MetricsPushEndpoint) > 0,
   264  			Endpoint:   taConfig.MetricsPushEndpoint,
   265  			MetricName: strings.ReplaceAll(strings.ToLower(jobConfig.JobName), "-", "_") + "_job_sync_failure_number",
   266  			Timeout:    taConfig.ClientTimeout,
   267  			Subsystem:  metrics.TenantFetcherSubsystem,
   268  			Labels:     []string{metrics.ErrorMetricLabel},
   269  		}
   270  		metricsPusher := metrics.NewAggregationFailurePusher(metricsCfg)
   271  		builder := resync.NewSynchronizerBuilder(jobConfig, featuresConfig, transact, directorClient, metricsPusher)
   272  		log.C(ctx).Infof("Creating tenant synchronizer %s for tenants of type %s", jobConfig.JobName, jobConfig.TenantType)
   273  		synchronizer, err := builder.Build(ctx)
   274  		exitOnError(err, fmt.Sprintf("Error while creating tenant synchronizer %s for tenants of type %s", jobConfig.JobName, jobConfig.TenantType))
   275  
   276  		synchronizers = append(synchronizers, synchronizer)
   277  		dbCloseFunctions = append(dbCloseFunctions, closeFunc)
   278  	}
   279  
   280  	return synchronizers, dbCloseFunctions
   281  }
   282  
   283  func synchronizeTenants(synchronizer *resync.TenantsSynchronizer, ctx context.Context) {
   284  	ticker := time.NewTicker(synchronizer.ResyncInterval())
   285  	for {
   286  		select {
   287  		case <-ticker.C:
   288  			logger := log.C(ctx)
   289  			logger = logger.WithField(log.FieldRequestID, uuid.New().String()).WithField("tenant-synchronizer-name", synchronizer.Name())
   290  			resyncCtx := log.ContextWithLogger(ctx, logger)
   291  			log.C(resyncCtx).Infof("Scheduled tenant resync job %s will be executed, job interval is %s", synchronizer.Name(), synchronizer.ResyncInterval())
   292  			if err := synchronizer.Synchronize(resyncCtx); err != nil {
   293  				log.C(resyncCtx).WithError(err).Errorf("Tenant fetcher resync %s failed with error: %v", synchronizer.Name(), err)
   294  			}
   295  		case <-ctx.Done():
   296  			log.C(ctx).Errorf("Context is canceled and scheduled tenant fetcher job %s will be stopped", synchronizer.Name())
   297  			stopTenantFetcherJobTicker(ctx, ticker, synchronizer.Name())
   298  			return
   299  		}
   300  	}
   301  }
   302  
   303  func registerTenantsOnDemandHandler(ctx context.Context, router *mux.Router, handlerCfg tenantfetcher.HandlerConfig, synchronizers []*resync.TenantsSynchronizer) {
   304  	var subaccountSynchronizer *resync.TenantsSynchronizer
   305  	for _, tenantSynchronizer := range synchronizers {
   306  		if tenantSynchronizer.TenantType() == tenant2.Subaccount {
   307  			subaccountSynchronizer = tenantSynchronizer
   308  			break
   309  		}
   310  	}
   311  
   312  	if subaccountSynchronizer == nil {
   313  		log.C(ctx).Infof("Subaccount synchronizer not found, tenant on-demand API won't be enabled")
   314  		return
   315  	}
   316  
   317  	log.C(ctx).Infof("Registering fetch tenant on-demand endpoint on %s...", handlerCfg.TenantOnDemandHandlerEndpoint)
   318  	tenantHandler := tenantfetcher.NewTenantFetcherHTTPHandler(subaccountSynchronizer, handlerCfg)
   319  	router.HandleFunc(handlerCfg.TenantOnDemandHandlerEndpoint, tenantHandler.FetchTenantOnDemand).Methods(http.MethodPost)
   320  }
   321  
   322  func newReadinessHandler() func(writer http.ResponseWriter, request *http.Request) {
   323  	return func(writer http.ResponseWriter, request *http.Request) {
   324  		writer.WriteHeader(http.StatusOK)
   325  	}
   326  }
   327  
   328  func newInternalGraphQLClient(url string, timeout time.Duration, skipSSLValidation bool) *gcli.Client {
   329  	tr := &http.Transport{
   330  		TLSClientConfig: &tls.Config{
   331  			InsecureSkipVerify: skipSSLValidation,
   332  		},
   333  	}
   334  	client := &http.Client{
   335  		Transport: httputil.NewCorrelationIDTransport(httputil.NewServiceAccountTokenTransportWithHeader(httputil.NewHTTPTransportWrapper(tr), "Authorization")),
   336  		Timeout:   timeout,
   337  	}
   338  
   339  	gqlClient := gcli.NewClient(url, gcli.WithHTTPClient(client))
   340  
   341  	gqlClient.Log = func(s string) {
   342  		log.D().Debug(s)
   343  	}
   344  
   345  	return gqlClient
   346  }
   347  
   348  func dependenciesConfigToMap(cfg tenantfetcher.HandlerConfig) (map[string][]tenantfetcher.Dependency, error) {
   349  	secretData, err := configprovider.ReadConfigFile(cfg.TenantDependenciesConfigPath)
   350  	if err != nil {
   351  		return nil, errors.Wrapf(err, "while reading tenant service dependencies config file")
   352  	}
   353  
   354  	dependenciesConfig := make(map[string][]tenantfetcher.Dependency)
   355  	config, err := configprovider.ParseConfigToJSONMap(secretData)
   356  	if err != nil {
   357  		return nil, errors.Wrapf(err, "while parsing tenant service dependencies config file")
   358  	}
   359  
   360  	for region, dependencies := range config {
   361  		for _, dependency := range dependencies.Array() {
   362  			xsappName := gjson.Get(dependency.String(), cfg.XsAppNamePathParam)
   363  			dependenciesConfig[region] = append(dependenciesConfig[region], tenantfetcher.Dependency{Xsappname: xsappName.String()})
   364  		}
   365  	}
   366  
   367  	return dependenciesConfig, nil
   368  }