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 }