github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/tenantfetchersvc/handler.go (about)

     1  package tenantfetchersvc
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"time"
    10  
    11  	"github.com/gorilla/mux"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    13  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    14  	"github.com/tidwall/gjson"
    15  )
    16  
    17  const (
    18  	// InternalServerError message
    19  	InternalServerError = "Internal Server Error"
    20  	compassURL          = "https://github.com/kyma-incubator/compass"
    21  )
    22  
    23  // TenantFetcher is used to fectch tenants for creation;
    24  //
    25  //go:generate mockery --name=TenantFetcher --output=automock --outpkg=automock --case=underscore --disable-version-string
    26  type TenantFetcher interface {
    27  	SynchronizeTenant(ctx context.Context, parentTenantID, tenantID string) error
    28  }
    29  
    30  // TenantSubscriber is used to apply subscription changes for tenants;
    31  //
    32  //go:generate mockery --name=TenantSubscriber --output=automock --outpkg=automock --case=underscore --disable-version-string
    33  type TenantSubscriber interface {
    34  	Subscribe(ctx context.Context, tenantSubscriptionRequest *TenantSubscriptionRequest) error
    35  	Unsubscribe(ctx context.Context, tenantSubscriptionRequest *TenantSubscriptionRequest) error
    36  }
    37  
    38  // HandlerConfig is the configuration required by the tenant handler.
    39  // It includes configurable parameters for incoming requests, including different tenant IDs json properties, and path parameters.
    40  type HandlerConfig struct {
    41  	TenantOnDemandHandlerEndpoint      string `envconfig:"APP_TENANT_ON_DEMAND_HANDLER_ENDPOINT,default=/v1/fetch/{parentTenantId}/{tenantId}"`
    42  	RegionalHandlerEndpoint            string `envconfig:"APP_REGIONAL_HANDLER_ENDPOINT,default=/v1/regional/{region}/callback/{tenantId}"`
    43  	DependenciesEndpoint               string `envconfig:"APP_REGIONAL_DEPENDENCIES_ENDPOINT,default=/v1/regional/{region}/dependencies"`
    44  	TenantPathParam                    string `envconfig:"APP_TENANT_PATH_PARAM,default=tenantId"`
    45  	ParentTenantPathParam              string `envconfig:"APP_PARENT_TENANT_PATH_PARAM,default=parentTenantId"`
    46  	RegionPathParam                    string `envconfig:"APP_REGION_PATH_PARAM,default=region"`
    47  	XsAppNamePathParam                 string `envconfig:"APP_TENANT_FETCHER_XSAPPNAME_PATH,default=xsappname"`
    48  	OmitDependenciesCallbackParam      string `envconfig:"APP_TENANT_FETCHER_OMIT_PARAM_NAME"`
    49  	OmitDependenciesCallbackParamValue string `envconfig:"APP_TENANT_FETCHER_OMIT_PARAM_VALUE"`
    50  
    51  	Database persistence.DatabaseConfig
    52  
    53  	DirectorGraphQLEndpoint     string        `envconfig:"APP_DIRECTOR_GRAPHQL_ENDPOINT"`
    54  	ClientTimeout               time.Duration `envconfig:"default=60s"`
    55  	HTTPClientSkipSslValidation bool          `envconfig:"APP_HTTP_CLIENT_SKIP_SSL_VALIDATION,default=false"`
    56  
    57  	TenantProviderConfig
    58  
    59  	MetricsPushEndpoint          string                  `envconfig:"optional,APP_METRICS_PUSH_ENDPOINT"`
    60  	TenantDependenciesConfigPath string                  `envconfig:"APP_TENANT_REGION_DEPENDENCIES_CONFIG_PATH"`
    61  	RegionToDependenciesConfig   map[string][]Dependency `envconfig:"-"`
    62  }
    63  
    64  // TenantProviderConfig includes the configuration for tenant providers - the tenant ID json property names, the subdomain property name, and the tenant provider name.
    65  type TenantProviderConfig struct {
    66  	TenantIDProperty                    string `envconfig:"APP_TENANT_PROVIDER_TENANT_ID_PROPERTY,default=tenantId"`
    67  	SubaccountTenantIDProperty          string `envconfig:"APP_TENANT_PROVIDER_SUBACCOUNT_TENANT_ID_PROPERTY,default=subaccountTenantId"`
    68  	CustomerIDProperty                  string `envconfig:"APP_TENANT_PROVIDER_CUSTOMER_ID_PROPERTY,default=customerId"`
    69  	SubdomainProperty                   string `envconfig:"APP_TENANT_PROVIDER_SUBDOMAIN_PROPERTY,default=subdomain"`
    70  	LicenseTypeProperty                 string `envconfig:"APP_TENANT_PROVIDER_LICENSE_TYPE_PROPERTY,default=licenseType"`
    71  	TenantProvider                      string `envconfig:"APP_TENANT_PROVIDER,default=external-provider"`
    72  	SubscriptionProviderIDProperty      string `envconfig:"APP_TENANT_PROVIDER_SUBSCRIPTION_PROVIDER_ID_PROPERTY,default=subscriptionProviderIdProperty"`
    73  	ProviderSubaccountIDProperty        string `envconfig:"APP_TENANT_PROVIDER_PROVIDER_SUBACCOUNT_ID_PROPERTY,default=providerSubaccountIdProperty"`
    74  	ConsumerTenantIDProperty            string `envconfig:"APP_TENANT_PROVIDER_CONSUMER_TENANT_ID_PROPERTY,default=consumerTenantIdProperty"`
    75  	SubscriptionProviderAppNameProperty string `envconfig:"APP_TENANT_PROVIDER_SUBSCRIPTION_PROVIDER_APP_NAME_PROPERTY,default=subscriptionProviderAppNameProperty"`
    76  }
    77  
    78  // Dependency contains the xsappname to be used in the dependencies callback
    79  type Dependency struct {
    80  	Xsappname string `json:"xsappname"`
    81  }
    82  
    83  type handler struct {
    84  	fetcher    TenantFetcher
    85  	subscriber TenantSubscriber
    86  	config     HandlerConfig
    87  }
    88  
    89  // NewTenantsHTTPHandler returns a new HTTP handler, responsible for creation and deletion of regional and non-regional tenants.
    90  func NewTenantsHTTPHandler(subscriber TenantSubscriber, config HandlerConfig) *handler {
    91  	return &handler{
    92  		subscriber: subscriber,
    93  		config:     config,
    94  	}
    95  }
    96  
    97  // NewTenantFetcherHTTPHandler returns a new HTTP handler, responsible for creation of on-demand tenants.
    98  func NewTenantFetcherHTTPHandler(fetcher TenantFetcher, config HandlerConfig) *handler {
    99  	return &handler{
   100  		fetcher: fetcher,
   101  		config:  config,
   102  	}
   103  }
   104  
   105  // FetchTenantOnDemand fetches External tenants registry events for a provided subaccount and creates a subaccount tenant
   106  func (h *handler) FetchTenantOnDemand(writer http.ResponseWriter, request *http.Request) {
   107  	ctx := request.Context()
   108  
   109  	vars := mux.Vars(request)
   110  	tenantID, ok := vars[h.config.TenantPathParam]
   111  	if !ok || len(tenantID) == 0 {
   112  		log.C(ctx).Error("Tenant path parameter is missing from request")
   113  		http.Error(writer, "Tenant path parameter is missing from request", http.StatusBadRequest)
   114  		return
   115  	}
   116  
   117  	parentTenantID, ok := vars[h.config.ParentTenantPathParam]
   118  	if !ok || len(parentTenantID) == 0 {
   119  		log.C(ctx).Error("Parent tenant path parameter is missing from request")
   120  		http.Error(writer, "Parent tenant ID path parameter is missing from request", http.StatusBadRequest)
   121  		return
   122  	}
   123  
   124  	log.C(ctx).Infof("Fetching create event for tenant with ID %s", tenantID)
   125  
   126  	if err := h.fetcher.SynchronizeTenant(ctx, parentTenantID, tenantID); err != nil {
   127  		log.C(ctx).WithError(err).Errorf("Error while processing request for creation of tenant %s: %v", tenantID, err)
   128  		http.Error(writer, InternalServerError, http.StatusInternalServerError)
   129  		return
   130  	}
   131  	writeCreatedResponse(writer, ctx, tenantID)
   132  }
   133  
   134  func writeCreatedResponse(writer http.ResponseWriter, ctx context.Context, tenantID string) {
   135  	writer.Header().Set("Content-Type", "text/plain")
   136  	writer.WriteHeader(http.StatusOK)
   137  	if _, err := writer.Write([]byte(compassURL)); err != nil {
   138  		log.C(ctx).WithError(err).Errorf("Failed to write response body for request for creation of tenant %s: %v", tenantID, err)
   139  	}
   140  }
   141  
   142  // SubscribeTenant handles subscription for tenant. If tenant does not exist, will create it first.
   143  func (h *handler) SubscribeTenant(writer http.ResponseWriter, request *http.Request) {
   144  	h.applySubscriptionChange(writer, request, h.subscriber.Subscribe)
   145  }
   146  
   147  // UnSubscribeTenant handles unsubscription for tenant which will remove the tenant id label from the runtime
   148  func (h *handler) UnSubscribeTenant(writer http.ResponseWriter, request *http.Request) {
   149  	h.applySubscriptionChange(writer, request, h.subscriber.Unsubscribe)
   150  }
   151  
   152  // Dependencies handler returns all external services where once created in Compass, the tenant should be created as well.
   153  func (h *handler) Dependencies(writer http.ResponseWriter, request *http.Request) {
   154  	ctx := request.Context()
   155  
   156  	vars := mux.Vars(request)
   157  	region, ok := vars[h.config.RegionPathParam]
   158  	if !ok {
   159  		log.C(ctx).Error("Region path parameter is missing from request")
   160  		http.Error(writer, "Region path parameter is missing from request", http.StatusBadRequest)
   161  		return
   162  	}
   163  
   164  	var bytes []byte
   165  	var err error
   166  
   167  	if len(h.config.OmitDependenciesCallbackParam) > 0 && len(h.config.OmitDependenciesCallbackParamValue) > 0 {
   168  		queryType, ok := request.URL.Query()[h.config.OmitDependenciesCallbackParam]
   169  		if ok && queryType[0] == h.config.OmitDependenciesCallbackParamValue {
   170  			bytes = []byte("[]")
   171  		}
   172  	}
   173  	if bytes == nil {
   174  		dependencies, ok := h.config.RegionToDependenciesConfig[region]
   175  		if !ok {
   176  			log.C(ctx).Errorf("Invalid region provided: %s", region)
   177  			http.Error(writer, fmt.Sprintf("Invalid region provided: %s", region), http.StatusBadRequest)
   178  			return
   179  		}
   180  
   181  		bytes, err = json.Marshal(dependencies)
   182  		if err != nil {
   183  			log.C(ctx).WithError(err).Error("Failed to marshal response body for dependencies request")
   184  			http.Error(writer, InternalServerError, http.StatusInternalServerError)
   185  			return
   186  		}
   187  	}
   188  
   189  	writer.Header().Set("Content-Type", "application/json")
   190  	if _, err = writer.Write(bytes); err != nil {
   191  		log.C(ctx).WithError(err).Errorf("Failed to write response body for dependencies request")
   192  		http.Error(writer, InternalServerError, http.StatusInternalServerError)
   193  		return
   194  	}
   195  }
   196  
   197  func (h *handler) applySubscriptionChange(writer http.ResponseWriter, request *http.Request, subscriptionFunc subscriptionFunc) {
   198  	ctx := request.Context()
   199  
   200  	vars := mux.Vars(request)
   201  	region, ok := vars[h.config.RegionPathParam]
   202  	if !ok {
   203  		log.C(ctx).Error("Region path parameter is missing from request")
   204  		http.Error(writer, "Region path parameter is missing from request", http.StatusBadRequest)
   205  		return
   206  	}
   207  
   208  	body, err := io.ReadAll(request.Body)
   209  	if err != nil {
   210  		log.C(ctx).WithError(err).Errorf("Failed to read tenant information from request body: %v", err)
   211  		http.Error(writer, InternalServerError, http.StatusInternalServerError)
   212  		return
   213  	}
   214  
   215  	subscriptionRequest, err := h.getSubscriptionRequest(body, region)
   216  	if err != nil {
   217  		log.C(ctx).WithError(err).Errorf("Failed to extract tenant information from request body: %v", err)
   218  		http.Error(writer, fmt.Sprintf("Failed to extract tenant information from request body: %v", err), http.StatusBadRequest)
   219  		return
   220  	}
   221  
   222  	mainTenantID := subscriptionRequest.MainTenantID()
   223  	if err := subscriptionFunc(ctx, subscriptionRequest); err != nil {
   224  		log.C(ctx).WithError(err).Errorf("Failed to apply subscription change for tenant %s: %v", mainTenantID, err)
   225  		http.Error(writer, InternalServerError, http.StatusInternalServerError)
   226  		return
   227  	}
   228  
   229  	respondSuccess(ctx, writer, mainTenantID)
   230  }
   231  
   232  func (h *handler) getSubscriptionRequest(body []byte, region string) (*TenantSubscriptionRequest, error) {
   233  	properties, err := getProperties(body, map[string]bool{
   234  		h.config.TenantIDProperty:                    true,
   235  		h.config.SubaccountTenantIDProperty:          false,
   236  		h.config.SubdomainProperty:                   true,
   237  		h.config.LicenseTypeProperty:                 false,
   238  		h.config.CustomerIDProperty:                  false,
   239  		h.config.SubscriptionProviderIDProperty:      true,
   240  		h.config.ProviderSubaccountIDProperty:        true,
   241  		h.config.ConsumerTenantIDProperty:            true,
   242  		h.config.SubscriptionProviderAppNameProperty: true,
   243  	})
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	req := &TenantSubscriptionRequest{
   249  		AccountTenantID:             properties[h.config.TenantIDProperty],
   250  		SubaccountTenantID:          properties[h.config.SubaccountTenantIDProperty],
   251  		CustomerTenantID:            properties[h.config.CustomerIDProperty],
   252  		SubscriptionLcenseType:      properties[h.config.LicenseTypeProperty],
   253  		Subdomain:                   properties[h.config.SubdomainProperty],
   254  		SubscriptionProviderID:      properties[h.config.SubscriptionProviderIDProperty],
   255  		ProviderSubaccountID:        properties[h.config.ProviderSubaccountIDProperty],
   256  		ConsumerTenantID:            properties[h.config.ConsumerTenantIDProperty],
   257  		SubscriptionProviderAppName: properties[h.config.SubscriptionProviderAppNameProperty],
   258  		Region:                      region,
   259  		SubscriptionPayload:         string(body),
   260  	}
   261  
   262  	if req.AccountTenantID == req.SubaccountTenantID {
   263  		req.SubaccountTenantID = ""
   264  	}
   265  
   266  	if req.AccountTenantID == req.CustomerTenantID {
   267  		req.CustomerTenantID = ""
   268  	}
   269  
   270  	return req, nil
   271  }
   272  
   273  func getProperties(body []byte, props map[string]bool) (map[string]string, error) {
   274  	resultProps := map[string]string{}
   275  	for propName, mandatory := range props {
   276  		result := gjson.GetBytes(body, propName).String()
   277  		if mandatory && len(result) == 0 {
   278  			return nil, fmt.Errorf("mandatory property %q is missing from request body", propName)
   279  		}
   280  		resultProps[propName] = result
   281  	}
   282  
   283  	return resultProps, nil
   284  }
   285  
   286  func respondSuccess(ctx context.Context, writer http.ResponseWriter, mainTenantID string) {
   287  	writer.Header().Set("Content-Type", "text/plain")
   288  	writer.WriteHeader(http.StatusOK)
   289  	if _, err := writer.Write([]byte(compassURL)); err != nil {
   290  		log.C(ctx).WithError(err).Errorf("Failed to write response body for tenant request creation for tenant %s: %v", mainTenantID, err)
   291  	}
   292  }