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

     1  package resync
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/kelseyhightower/envconfig"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/oauth"
    13  	"github.com/kyma-incubator/compass/components/director/pkg/tenant"
    14  	"github.com/pkg/errors"
    15  	"github.com/tidwall/gjson"
    16  )
    17  
    18  const (
    19  	jobNamePattern          = "^APP_(.*)_JOB_NAME$"
    20  	regionNamePatternFormat = "^APP_%s_REGIONAL_CONFIG_(.*)_REGION_NAME$"
    21  )
    22  
    23  type job struct {
    24  	ctx             context.Context
    25  	name            string
    26  	environmentVars map[string]string
    27  	jobConfig       *JobConfig
    28  }
    29  
    30  // ResyncConfig holds information about tenant synchronizing intervals
    31  type ResyncConfig struct {
    32  	TenantFetcherJobIntervalMins time.Duration `envconfig:"TENANT_FETCHER_JOB_INTERVAL" default:"5m"`
    33  	FullResyncInterval           time.Duration `envconfig:"FULL_RESYNC_INTERVAL" default:"12h"`
    34  }
    35  
    36  // PagingConfig holds information about Events API pagination
    37  type PagingConfig struct {
    38  	TotalPagesField   string `envconfig:"TENANT_TOTAL_PAGES_FIELD" default:"pages"`
    39  	TotalResultsField string `envconfig:"TENANT_TOTAL_RESULTS_FIELD" default:"totalResults"`
    40  }
    41  
    42  // JobConfig contains tenant fetcher job configuration read from environment
    43  type JobConfig struct {
    44  	EventsConfig
    45  	ResyncConfig
    46  	KubeConfig
    47  
    48  	JobName        string      `envconfig:"JOB_NAME" required:"true"`
    49  	TenantProvider string      `envconfig:"TENANT_PROVIDER" required:"true"`
    50  	TenantType     tenant.Type `envconfig:"TENANT_TYPE" required:"true"`
    51  	RegionPrefix   string      `envconfig:"REGION_PREFIX"`
    52  }
    53  
    54  // EventsConfig contains configuration for Events API requests
    55  type EventsConfig struct {
    56  	QueryConfig
    57  	PagingConfig
    58  
    59  	RegionalAuthConfigSecret AuthProviderConfig          `envconfig:"SECRET"`
    60  	RegionalAPIConfigs       map[string]*EventsAPIConfig `ignored:"true"`
    61  	APIConfig                EventsAPIConfig             `envconfig:"API"`
    62  	PageWorkers              int                         `envconfig:"PAGE_WORKERS" default:"2"`
    63  	TenantOperationChunkSize int                         `envconfig:"TENANT_INSERT_CHUNK_SIZE" default:"500"`
    64  	RetryAttempts            uint                        `envconfig:"RETRY_ATTEMPTS" default:"7"`
    65  }
    66  
    67  // Validate checks if the current configuration contains all required information in order to successfully synchronize tenants of the given type
    68  func (ec EventsConfig) Validate(tenantType tenant.Type) error {
    69  	if err := ec.APIConfig.Validate(tenantType); err != nil {
    70  		return err
    71  	}
    72  	for region, config := range ec.RegionalAPIConfigs {
    73  		if err := config.Validate(tenantType); err != nil {
    74  			return errors.Wrapf(err, "while validating API configuration for region %s", region)
    75  		}
    76  	}
    77  	return nil
    78  }
    79  
    80  // EventsAPIConfig holds information about the Tenant Events API - its endpoints, mappings to Compass tenants, and API client configurations
    81  type EventsAPIConfig struct {
    82  	APIEndpointsConfig
    83  	TenantFieldMapping
    84  	MovedSubaccountsFieldMapping
    85  
    86  	RegionName          string         `envconfig:"REGION_NAME" required:"true"`
    87  	AuthConfigSecretKey string         `envconfig:"AUTH_CONFIG_SECRET_KEY" required:"true"`
    88  	AuthMode            oauth.AuthMode `envconfig:"AUTH_MODE" required:"true"`
    89  	ClientTimeout       time.Duration  `envconfig:"TIMEOUT" default:"1m"`
    90  	OAuthConfig         OAuth2Config   `ignored:"true"`
    91  }
    92  
    93  // Validate checks if the current configuration contains all required information in order to successfully synchronize tenants of the given type
    94  func (c EventsAPIConfig) Validate(tenantType tenant.Type) error {
    95  	missingProperties := make([]string, 0)
    96  	if tenantType == tenant.Subaccount {
    97  		if len(c.APIEndpointsConfig.EndpointSubaccountCreated) == 0 {
    98  			missingProperties = append(missingProperties, "EndpointSubaccountCreated")
    99  		}
   100  		if len(c.APIEndpointsConfig.EndpointSubaccountUpdated) == 0 {
   101  			missingProperties = append(missingProperties, "EndpointSubaccountUpdated")
   102  		}
   103  		if len(c.APIEndpointsConfig.EndpointSubaccountDeleted) == 0 {
   104  			missingProperties = append(missingProperties, "EndpointSubaccountDeleted")
   105  		}
   106  	}
   107  	if tenantType == tenant.Account {
   108  		if len(c.APIEndpointsConfig.EndpointTenantCreated) == 0 {
   109  			missingProperties = append(missingProperties, "EndpointTenantCreated")
   110  		}
   111  		if len(c.APIEndpointsConfig.EndpointTenantUpdated) == 0 {
   112  			missingProperties = append(missingProperties, "EndpointTenantUpdated")
   113  		}
   114  		if len(c.APIEndpointsConfig.EndpointTenantDeleted) == 0 {
   115  			missingProperties = append(missingProperties, "EndpointTenantDeleted")
   116  		}
   117  	}
   118  	if len(missingProperties) > 0 {
   119  		return fmt.Errorf("missing API Client config properties: %s", strings.Join(missingProperties, ","))
   120  	}
   121  
   122  	return c.OAuthConfig.Validate(c.AuthMode)
   123  }
   124  
   125  // NewTenantFetcherJobEnvironment used for job configuration read from environment
   126  func NewTenantFetcherJobEnvironment(ctx context.Context, name string, environmentVars map[string]string) *job {
   127  	return &job{
   128  		ctx:             ctx,
   129  		name:            name,
   130  		environmentVars: environmentVars,
   131  		jobConfig:       nil,
   132  	}
   133  }
   134  
   135  // Validate checks if the current configuration contains all required information in order to successfully synchronize tenants of the configured type
   136  func (jc *JobConfig) Validate() error {
   137  	return jc.EventsConfig.Validate(jc.TenantType)
   138  }
   139  
   140  // ReadJobConfig reads job configuration from environment
   141  func (j *job) ReadJobConfig() (*JobConfig, error) {
   142  	if j.jobConfig != nil {
   143  		return j.jobConfig, nil
   144  	}
   145  
   146  	jobConfigPrefix := fmt.Sprintf("APP_%s", strings.ToUpper(j.name))
   147  	jc := JobConfig{}
   148  	if err := envconfig.Process(jobConfigPrefix, &jc); err != nil {
   149  		return nil, errors.Wrapf(err, "while initializing job config with prefix %s", jobConfigPrefix)
   150  	}
   151  
   152  	regionalCfg, err := j.readRegionalEventsConfig()
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	authConfigs, err := j.mapClientsAuthConfigs(jc)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	clientCfg, ok := authConfigs[jc.APIConfig.AuthConfigSecretKey]
   163  	if !ok {
   164  		return nil, fmt.Errorf("auth config not found for Events API: secret file does not contain key %s", jc.APIConfig.AuthConfigSecretKey)
   165  	}
   166  
   167  	jc.APIConfig.OAuthConfig = clientCfg
   168  
   169  	for region, regionalCfg := range regionalCfg {
   170  		authCfg, ok := authConfigs[regionalCfg.AuthConfigSecretKey]
   171  		if !ok {
   172  			return nil, fmt.Errorf("auth config not found for Events API for region %s: secret file does not contain key %s", region, regionalCfg.AuthConfigSecretKey)
   173  		}
   174  		regionalCfg.OAuthConfig = authCfg
   175  	}
   176  
   177  	jc.RegionalAPIConfigs = regionalCfg
   178  	if err := jc.Validate(); err != nil {
   179  		return nil, errors.Wrapf(err, "while reading job configuration for job %s", j.name)
   180  	}
   181  
   182  	j.jobConfig = &jc
   183  	return j.jobConfig, nil
   184  }
   185  
   186  func (j *job) readRegionalEventsConfig() (map[string]*EventsAPIConfig, error) {
   187  	regEventsConfig := map[string]*EventsAPIConfig{}
   188  	for _, region := range j.jobRegions() {
   189  		regionalConfigEnvPrefix := fmt.Sprintf("APP_%s_REGIONAL_CONFIG_%s", strings.ToUpper(j.name), strings.ToUpper(region))
   190  		newCfg := &EventsAPIConfig{}
   191  		if err := envconfig.Process(regionalConfigEnvPrefix, newCfg); err != nil {
   192  			return nil, errors.Wrapf(err, "while reading config for region %s", region)
   193  		}
   194  
   195  		regEventsConfig[strings.ToLower(region)] = newCfg
   196  	}
   197  
   198  	return regEventsConfig, nil
   199  }
   200  
   201  // mapClientsAuthConfigs Parses the InstanceConfigs json string to map with key: region name and value: InstanceConfig for the instance in the region
   202  func (j *job) mapClientsAuthConfigs(jc JobConfig) (map[string]OAuth2Config, error) {
   203  	secretData, err := j.getJobSecret(jc)
   204  	if err != nil {
   205  		return nil, errors.Wrapf(err, "while getting job secret")
   206  	}
   207  
   208  	if ok := gjson.Valid(secretData); !ok {
   209  		return nil, errors.New("failed to validate job auth configs")
   210  	}
   211  
   212  	authConfig := jc.EventsConfig.RegionalAuthConfigSecret
   213  	bindingsResult := gjson.Parse(secretData)
   214  	bindingsMap := bindingsResult.Map()
   215  	clientsAuthConfig := make(map[string]OAuth2Config)
   216  	for secretKey, config := range bindingsMap {
   217  		i := OAuth2Config{
   218  			ClientID:           gjson.Get(config.String(), authConfig.ClientIDPath).String(),
   219  			ClientSecret:       gjson.Get(config.String(), authConfig.ClientSecretPath).String(),
   220  			OAuthTokenEndpoint: gjson.Get(config.String(), authConfig.TokenEndpointPath).String(),
   221  			TokenPath:          authConfig.TokenPath,
   222  			X509Config: X509Config{
   223  				Cert: gjson.Get(config.String(), authConfig.CertPath).String(),
   224  				Key:  gjson.Get(config.String(), authConfig.KeyPath).String(),
   225  			},
   226  			SkipSSLValidation: authConfig.SkipSSLValidation,
   227  		}
   228  
   229  		clientsAuthConfig[secretKey] = i
   230  	}
   231  
   232  	return clientsAuthConfig, nil
   233  }
   234  
   235  func (j *job) getJobSecret(jc JobConfig) (string, error) {
   236  	path := jc.RegionalAuthConfigSecret.SecretFilePath
   237  	if path == "" {
   238  		return "", errors.New("job secret path cannot be empty")
   239  	}
   240  	secret, err := os.ReadFile(path)
   241  	if err != nil {
   242  		return "", errors.Wrapf(err, "unable to read job secret file")
   243  	}
   244  
   245  	return string(secret), nil
   246  }
   247  
   248  // ReadFromEnvironment returns a key-value map of environment variables
   249  func ReadFromEnvironment(environ []string) map[string]string {
   250  	vars := make(map[string]string)
   251  	for _, env := range environ {
   252  		pair := strings.SplitN(env, "=", 2)
   253  		key := pair[0]
   254  		value := pair[1]
   255  		vars[key] = value
   256  	}
   257  	return vars
   258  }
   259  
   260  // GetJobNames retrieves the names of tenant fetchers jobs
   261  func GetJobNames(envVars map[string]string) []string {
   262  	searchPattern := regexp.MustCompile(jobNamePattern)
   263  	var jobNames []string
   264  
   265  	for key := range envVars {
   266  		matches := searchPattern.FindStringSubmatch(key)
   267  		if len(matches) > 0 {
   268  			jobName := matches[1]
   269  			jobNames = append(jobNames, jobName)
   270  		}
   271  	}
   272  
   273  	return jobNames
   274  }
   275  
   276  // jobRegions retrieves the names of the tenant fetchers' job regions
   277  func (j *job) jobRegions() []string {
   278  	searchPattern := regexp.MustCompile(fmt.Sprintf(regionNamePatternFormat, strings.ToUpper(j.name)))
   279  
   280  	var regionNames []string
   281  	for key := range j.environmentVars {
   282  		matches := searchPattern.FindStringSubmatch(key)
   283  		if len(matches) > 0 {
   284  			regionName := matches[1]
   285  			regionNames = append(regionNames, regionName)
   286  		}
   287  	}
   288  
   289  	return regionNames
   290  }