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 }