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 }