github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/cmd/destinationfetcher-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  	"net/http"
    22  	"os"
    23  	"time"
    24  
    25  	"github.com/kyma-incubator/compass/components/director/internal/authenticator/claims"
    26  	auth_middleware "github.com/kyma-incubator/compass/components/director/pkg/auth-middleware"
    27  
    28  	"github.com/gorilla/mux"
    29  	destinationfetcher "github.com/kyma-incubator/compass/components/director/internal/destinationfetchersvc"
    30  	"github.com/kyma-incubator/compass/components/director/internal/domain/api"
    31  	"github.com/kyma-incubator/compass/components/director/internal/domain/auth"
    32  	"github.com/kyma-incubator/compass/components/director/internal/domain/bundle"
    33  	"github.com/kyma-incubator/compass/components/director/internal/domain/destination"
    34  	"github.com/kyma-incubator/compass/components/director/internal/domain/document"
    35  	"github.com/kyma-incubator/compass/components/director/internal/domain/eventdef"
    36  	"github.com/kyma-incubator/compass/components/director/internal/domain/fetchrequest"
    37  	"github.com/kyma-incubator/compass/components/director/internal/domain/label"
    38  	"github.com/kyma-incubator/compass/components/director/internal/domain/spec"
    39  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    40  	"github.com/kyma-incubator/compass/components/director/internal/domain/version"
    41  	configprovider "github.com/kyma-incubator/compass/components/director/pkg/config"
    42  	"github.com/kyma-incubator/compass/components/director/pkg/correlation"
    43  	"github.com/kyma-incubator/compass/components/director/pkg/cronjob"
    44  	"github.com/kyma-incubator/compass/components/director/pkg/executor"
    45  	timeouthandler "github.com/kyma-incubator/compass/components/director/pkg/handler"
    46  	httputil "github.com/kyma-incubator/compass/components/director/pkg/http"
    47  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    48  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    49  	"github.com/kyma-incubator/compass/components/director/pkg/signal"
    50  	"github.com/kyma-incubator/compass/components/system-broker/pkg/uuid"
    51  	"github.com/pkg/errors"
    52  	"github.com/vrischmann/envconfig"
    53  )
    54  
    55  const envPrefix = "APP"
    56  
    57  type config struct {
    58  	Address string `envconfig:"default=127.0.0.1:8080"`
    59  
    60  	ServerTimeout              time.Duration `envconfig:"default=110s"`
    61  	ShutdownTimeout            time.Duration `envconfig:"default=10s"`
    62  	DestinationFetcherSchedule time.Duration `envconfig:"APP_DESTINATION_FETCHER_SCHEDULE,default=10m"`
    63  	TenantSyncTimeout          time.Duration `envconfig:"APP_DESTINATION_FETCHER_TENANT_SYNC_TIMEOUT,default=5m"`
    64  	ParallelTenantSyncs        int           `envconfig:"APP_DESTINATION_FETCHER_PARALLEL_TENANTS,default=10"`
    65  	DestinationsRootAPI        string        `envconfig:"APP_ROOT_API,default=/destinations"`
    66  
    67  	HandlerConfig               destinationfetcher.HandlerConfig
    68  	DestinationServiceAPIConfig destinationfetcher.DestinationServiceAPIConfig
    69  	DestinationsConfig          configprovider.DestinationsConfig
    70  	Database                    persistence.DatabaseConfig
    71  	Log                         log.Config
    72  	SecurityConfig              securityConfig
    73  	ElectionConfig              cronjob.ElectionConfig
    74  }
    75  
    76  type securityConfig struct {
    77  	JWKSSyncPeriod                 time.Duration `envconfig:"default=5m"`
    78  	AllowJWTSigningNone            bool          `envconfig:"APP_ALLOW_JWT_SIGNING_NONE,default=false"`
    79  	JwksEndpoint                   string        `envconfig:"APP_JWKS_ENDPOINT,default=file://hack/default-jwks.json"`
    80  	DestinationsOnDemandScope      string        `envconfig:"APP_DESTINATIONS_SYNC_SCOPE,default=destinations:sync"`
    81  	DestinationsSensitiveDataScope string        `envconfig:"APP_DESTINATIONS_SENSITIVE_DATA_SCOPE,default=destinations_sensitive_data:read"`
    82  }
    83  
    84  func main() {
    85  	ctx, cancel := context.WithCancel(context.Background())
    86  
    87  	defer cancel()
    88  
    89  	term := make(chan os.Signal)
    90  	signal.HandleInterrupts(ctx, cancel, term)
    91  
    92  	cfg := config{}
    93  	err := envconfig.InitWithPrefix(&cfg, envPrefix)
    94  	exitOnError(err, "Error while loading app config")
    95  
    96  	ctx, err = log.Configure(ctx, &cfg.Log)
    97  	exitOnError(err, "Failed to configure Logger")
    98  
    99  	transactioner, closeFunc, err := persistence.Configure(ctx, cfg.Database)
   100  	exitOnError(err, "Error while establishing the connection to the database")
   101  
   102  	defer func() {
   103  		err := closeFunc()
   104  		exitOnError(err, "error while closing the connection to the database")
   105  	}()
   106  
   107  	httpClient := &http.Client{
   108  		Transport: httputil.NewCorrelationIDTransport(httputil.NewHTTPTransportWrapper(http.DefaultTransport.(*http.Transport))),
   109  		CheckRedirect: func(req *http.Request, via []*http.Request) error {
   110  			return http.ErrUseLastResponse
   111  		},
   112  	}
   113  
   114  	destinationService := getDestinationService(cfg, transactioner)
   115  	handler := initAPIHandler(ctx, httpClient, cfg, destinationService)
   116  	runMainSrv, shutdownMainSrv := createServer(ctx, cfg, handler, "main")
   117  
   118  	go func() {
   119  		<-ctx.Done()
   120  		// Interrupt signal received - shut down the servers
   121  		shutdownMainSrv()
   122  	}()
   123  
   124  	syncJobConfig := destinationfetcher.SyncJobConfig{
   125  		ElectionCfg:       cfg.ElectionConfig,
   126  		JobSchedulePeriod: cfg.DestinationFetcherSchedule,
   127  		ParallelTenants:   cfg.ParallelTenantSyncs,
   128  		TenantSyncTimeout: cfg.TenantSyncTimeout,
   129  	}
   130  	go func() {
   131  		err := destinationfetcher.StartDestinationFetcherSyncJob(ctx, syncJobConfig, destinationService)
   132  		if err != nil {
   133  			log.C(ctx).WithError(err).Error("Failed to start destination fetcher cronjob. Stopping app...")
   134  		}
   135  		cancel()
   136  	}()
   137  
   138  	runMainSrv()
   139  }
   140  
   141  func configureAuthMiddleware(ctx context.Context, httpClient *http.Client, router *mux.Router, cfg securityConfig, requiredScopes ...string) {
   142  	scopeValidator := claims.NewScopesValidator(requiredScopes)
   143  	middleware := auth_middleware.New(httpClient, cfg.JwksEndpoint, cfg.AllowJWTSigningNone, "", scopeValidator)
   144  	router.Use(middleware.Handler())
   145  
   146  	log.C(ctx).Infof("JWKS synchronization enabled. Sync period: %v", cfg.JWKSSyncPeriod)
   147  	periodicExecutor := executor.NewPeriodic(cfg.JWKSSyncPeriod, func(ctx context.Context) {
   148  		if err := middleware.SynchronizeJWKS(ctx); err != nil {
   149  			log.C(ctx).WithError(err).Errorf("An error has occurred while synchronizing JWKS: %v", err)
   150  		}
   151  	})
   152  	go periodicExecutor.Run(ctx)
   153  }
   154  
   155  func exitOnError(err error, context string) {
   156  	if err != nil {
   157  		wrappedError := errors.Wrap(err, context)
   158  		log.D().Fatal(wrappedError)
   159  	}
   160  }
   161  
   162  func getDestinationService(cfg config, transact persistence.Transactioner) *destinationfetcher.DestinationService {
   163  	uuidSvc := uuid.NewService()
   164  	destConv := destination.NewConverter()
   165  	destRepo := destination.NewRepository(destConv)
   166  	bundleRepo := bundleRepo()
   167  
   168  	labelConverter := label.NewConverter()
   169  	labelRepo := label.NewRepository(labelConverter)
   170  
   171  	tenantConverter := tenant.NewConverter()
   172  	tenantRepo := tenant.NewRepository(tenantConverter)
   173  
   174  	err := cfg.DestinationsConfig.MapInstanceConfigs()
   175  	exitOnError(err, "error while loading destination instances config")
   176  
   177  	return &destinationfetcher.DestinationService{
   178  		Transactioner:      transact,
   179  		UUIDSvc:            uuidSvc,
   180  		DestinationRepo:    destRepo,
   181  		BundleRepo:         bundleRepo,
   182  		LabelRepo:          labelRepo,
   183  		DestinationsConfig: cfg.DestinationsConfig,
   184  		APIConfig:          cfg.DestinationServiceAPIConfig,
   185  		TenantRepo:         tenantRepo,
   186  	}
   187  }
   188  
   189  func initAPIHandler(ctx context.Context, httpClient *http.Client,
   190  	cfg config, destService *destinationfetcher.DestinationService) http.Handler {
   191  	const (
   192  		healthzEndpoint = "/healthz"
   193  		readyzEndpoint  = "/readyz"
   194  	)
   195  	logger := log.C(ctx)
   196  	mainRouter := mux.NewRouter()
   197  	mainRouter.Use(correlation.AttachCorrelationIDToContext(), log.RequestLogger(
   198  		cfg.DestinationsRootAPI+healthzEndpoint, cfg.DestinationsRootAPI+readyzEndpoint))
   199  
   200  	syncDestinationsAPIRouter := mainRouter.PathPrefix(cfg.DestinationsRootAPI).Subrouter()
   201  	destinationHandler := destinationfetcher.NewDestinationsHTTPHandler(destService, cfg.HandlerConfig)
   202  	sensitiveDataAPIRouter := syncDestinationsAPIRouter
   203  
   204  	log.C(ctx).Infof("Registering service destinations endpoint on %s...", cfg.HandlerConfig.SyncDestinationsEndpoint)
   205  	configureAuthMiddleware(ctx, httpClient, syncDestinationsAPIRouter,
   206  		cfg.SecurityConfig, cfg.SecurityConfig.DestinationsOnDemandScope)
   207  
   208  	syncDestinationsAPIRouter.HandleFunc(cfg.HandlerConfig.SyncDestinationsEndpoint, destinationHandler.SyncTenantDestinations).
   209  		Methods(http.MethodPut)
   210  
   211  	log.C(ctx).Infof("Registering service destinations endpoint on %s...", cfg.HandlerConfig.DestinationsSensitiveEndpoint)
   212  	configureAuthMiddleware(ctx, httpClient, sensitiveDataAPIRouter,
   213  		cfg.SecurityConfig, cfg.SecurityConfig.DestinationsSensitiveDataScope)
   214  	sensitiveDataAPIRouter.HandleFunc(cfg.HandlerConfig.DestinationsSensitiveEndpoint, destinationHandler.FetchDestinationsSensitiveData).
   215  		Methods(http.MethodGet)
   216  
   217  	healthCheckRouter := mainRouter.PathPrefix(cfg.DestinationsRootAPI).Subrouter()
   218  	logger.Infof("Registering readiness endpoint...")
   219  	healthCheckRouter.HandleFunc(readyzEndpoint, newReadinessHandler())
   220  	logger.Infof("Registering liveness endpoint...")
   221  	healthCheckRouter.HandleFunc(healthzEndpoint, newReadinessHandler())
   222  
   223  	return mainRouter
   224  }
   225  
   226  func bundleRepo() destinationfetcher.BundleRepo {
   227  	authConverter := auth.NewConverter()
   228  	frConverter := fetchrequest.NewConverter(authConverter)
   229  	versionConverter := version.NewConverter()
   230  	specConverter := spec.NewConverter(frConverter)
   231  	eventAPIConverter := eventdef.NewConverter(versionConverter, specConverter)
   232  	docConverter := document.NewConverter(frConverter)
   233  	apiConverter := api.NewConverter(versionConverter, specConverter)
   234  
   235  	return bundle.NewRepository(bundle.NewConverter(authConverter, apiConverter, eventAPIConverter, docConverter))
   236  }
   237  
   238  func newReadinessHandler() func(writer http.ResponseWriter, request *http.Request) {
   239  	return func(writer http.ResponseWriter, request *http.Request) {
   240  		writer.WriteHeader(http.StatusOK)
   241  	}
   242  }
   243  
   244  func createServer(ctx context.Context, cfg config, handler http.Handler, name string) (func(), func()) {
   245  	logger := log.C(ctx)
   246  
   247  	handlerWithTimeout, err := timeouthandler.WithTimeout(handler, cfg.ServerTimeout)
   248  	exitOnError(err, "Error while configuring tenant mapping handler")
   249  
   250  	srv := &http.Server{
   251  		Addr:              cfg.Address,
   252  		Handler:           handlerWithTimeout,
   253  		ReadHeaderTimeout: cfg.ServerTimeout,
   254  	}
   255  
   256  	runFn := func() {
   257  		logger.Infof("Running %s server on %s...", name, cfg.Address)
   258  		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
   259  			logger.Errorf("%s HTTP server ListenAndServe: %v", name, err)
   260  		}
   261  	}
   262  
   263  	shutdownFn := func() {
   264  		ctx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
   265  		defer cancel()
   266  
   267  		logger.Infof("Shutting down %s server...", name)
   268  		if err := srv.Shutdown(ctx); err != nil {
   269  			logger.Errorf("%s HTTP server Shutdown: %v", name, err)
   270  		}
   271  	}
   272  
   273  	return runFn, shutdownFn
   274  }