github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/open_resource_discovery/client.go (about) 1 package ord 2 3 import ( 4 "context" 5 "encoding/json" 6 "io" 7 "net/http" 8 "net/url" 9 "strings" 10 "sync" 11 12 directorresource "github.com/kyma-incubator/compass/components/director/pkg/resource" 13 14 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 15 16 "github.com/kyma-incubator/compass/components/director/pkg/accessstrategy" 17 18 "github.com/kyma-incubator/compass/components/director/internal/model" 19 20 httputil "github.com/kyma-incubator/compass/components/director/pkg/http" 21 "github.com/kyma-incubator/compass/components/director/pkg/log" 22 23 "github.com/pkg/errors" 24 ) 25 26 // ClientConfig contains configuration for the ORD aggregator client 27 type ClientConfig struct { 28 maxParallelDocumentsPerApplication int 29 } 30 31 // Resource represents a resource that is being aggregated. This would be an Application or Application Template 32 type Resource struct { 33 Type directorresource.Type 34 ID string 35 ParentID *string 36 Name string 37 LocalTenantID *string 38 } 39 40 // NewClientConfig creates new ClientConfig from the supplied parameters 41 func NewClientConfig(maxParallelDocumentsPerApplication int) ClientConfig { 42 return ClientConfig{ 43 maxParallelDocumentsPerApplication: maxParallelDocumentsPerApplication, 44 } 45 } 46 47 // Client represents ORD documents client 48 // 49 //go:generate mockery --name=Client --output=automock --outpkg=automock --case=underscore --disable-version-string 50 type Client interface { 51 FetchOpenResourceDiscoveryDocuments(ctx context.Context, resource Resource, webhook *model.Webhook) (Documents, string, error) 52 } 53 54 type client struct { 55 config ClientConfig 56 *http.Client 57 accessStrategyExecutorProvider accessstrategy.ExecutorProvider 58 } 59 60 // NewClient creates new ORD Client via a provided http.Client 61 func NewClient(config ClientConfig, httpClient *http.Client, accessStrategyExecutorProvider accessstrategy.ExecutorProvider) *client { 62 return &client{ 63 config: config, 64 Client: httpClient, 65 accessStrategyExecutorProvider: accessStrategyExecutorProvider, 66 } 67 } 68 69 // FetchOpenResourceDiscoveryDocuments fetches all the documents for a single ORD .well-known endpoint 70 func (c *client) FetchOpenResourceDiscoveryDocuments(ctx context.Context, resource Resource, webhook *model.Webhook) (Documents, string, error) { 71 var tenantValue string 72 73 if needsTenantHeader := webhook.ObjectType == model.ApplicationTemplateWebhookReference && resource.Type != directorresource.ApplicationTemplate; needsTenantHeader { 74 tntFromCtx, err := tenant.LoadTenantPairFromContext(ctx) 75 if err != nil { 76 return nil, "", errors.Wrapf(err, "while loading tenant from context for application template webhook flow") 77 } 78 79 tenantValue = tntFromCtx.ExternalID 80 } 81 82 config, err := c.fetchConfig(ctx, resource, webhook, tenantValue) 83 if err != nil { 84 return nil, "", err 85 } 86 87 baseURL, err := calculateBaseURL(*webhook.URL, *config) 88 if err != nil { 89 return nil, "", errors.Wrap(err, "while calculating baseURL") 90 } 91 92 err = config.Validate(baseURL) 93 if err != nil { 94 return nil, "", errors.Wrap(err, "while validating ORD config") 95 } 96 97 docs := make([]*Document, 0) 98 docMutex := sync.Mutex{} 99 wg := sync.WaitGroup{} 100 workers := make(chan struct{}, c.config.maxParallelDocumentsPerApplication) 101 fetchDocErrors := make([]error, 0) 102 errMutex := sync.Mutex{} 103 104 for _, docDetails := range config.OpenResourceDiscoveryV1.Documents { 105 wg.Add(1) 106 workers <- struct{}{} 107 go func(docDetails DocumentDetails) { 108 defer func() { 109 wg.Done() 110 <-workers 111 }() 112 113 documentURL, err := buildDocumentURL(docDetails.URL, baseURL) 114 if err != nil { 115 log.C(ctx).Warn(errors.Wrap(err, "error building document URL").Error()) 116 addError(&fetchDocErrors, err, &errMutex) 117 return 118 } 119 strategy, ok := docDetails.AccessStrategies.GetSupported() 120 if !ok { 121 log.C(ctx).Warnf("Unsupported access strategies for ORD Document %q", documentURL) 122 } 123 doc, err := c.fetchOpenDiscoveryDocumentWithAccessStrategy(ctx, documentURL, strategy, tenantValue) 124 if err != nil { 125 log.C(ctx).Warn(errors.Wrapf(err, "error fetching ORD document from: %s", documentURL).Error()) 126 addError(&fetchDocErrors, err, &errMutex) 127 return 128 } 129 130 if docDetails.Perspective == SystemVersionPerspective { 131 doc.Perspective = SystemVersionPerspective 132 } else { 133 doc.Perspective = SystemInstancePerspective 134 } 135 addDocument(&docs, doc, &docMutex) 136 }(docDetails) 137 } 138 139 wg.Wait() 140 141 var fetchDocErr error = nil 142 if len(fetchDocErrors) > 0 { 143 stringErrors := convertErrorsToStrings(fetchDocErrors) 144 fetchDocErr = errors.Errorf(strings.Join(stringErrors, "\n")) 145 } 146 return docs, baseURL, fetchDocErr 147 } 148 149 func convertErrorsToStrings(errors []error) (result []string) { 150 for _, err := range errors { 151 result = append(result, err.Error()) 152 } 153 return result 154 } 155 156 func (c *client) fetchOpenDiscoveryDocumentWithAccessStrategy(ctx context.Context, documentURL string, accessStrategy accessstrategy.Type, tenantValue string) (*Document, error) { 157 log.C(ctx).Infof("Fetching ORD Document %q with Access Strategy %q", documentURL, accessStrategy) 158 executor, err := c.accessStrategyExecutorProvider.Provide(accessStrategy) 159 if err != nil { 160 return nil, err 161 } 162 163 resp, err := executor.Execute(ctx, c.Client, documentURL, tenantValue) 164 if err != nil { 165 return nil, err 166 } 167 168 defer closeBody(ctx, resp.Body) 169 170 if resp.StatusCode != http.StatusOK { 171 return nil, errors.Errorf("error while fetching open resource discovery document %q: status code %d", documentURL, resp.StatusCode) 172 } 173 174 resp.Body = http.MaxBytesReader(nil, resp.Body, 2097152) 175 bodyBytes, err := io.ReadAll(resp.Body) 176 if err != nil { 177 return nil, errors.Wrap(err, "error reading document body") 178 } 179 result := &Document{} 180 if err := json.Unmarshal(bodyBytes, &result); err != nil { 181 return nil, errors.Wrap(err, "error unmarshaling document") 182 } 183 return result, nil 184 } 185 186 func closeBody(ctx context.Context, body io.ReadCloser) { 187 if err := body.Close(); err != nil { 188 log.C(ctx).WithError(err).Warnf("Got error on closing response body") 189 } 190 } 191 192 func addDocument(docs *[]*Document, doc *Document, mutex *sync.Mutex) { 193 mutex.Lock() 194 defer mutex.Unlock() 195 *docs = append(*docs, doc) 196 } 197 198 func addError(fetchDocErrors *[]error, err error, mutex *sync.Mutex) { 199 mutex.Lock() 200 defer mutex.Unlock() 201 *fetchDocErrors = append(*fetchDocErrors, err) 202 } 203 204 func (c *client) fetchConfig(ctx context.Context, resource Resource, webhook *model.Webhook, tenantValue string) (*WellKnownConfig, error) { 205 var resp *http.Response 206 var err error 207 if webhook.Auth != nil && webhook.Auth.AccessStrategy != nil && len(*webhook.Auth.AccessStrategy) > 0 { 208 log.C(ctx).Infof("%s %q (id = %q) ORD webhook is configured with %q access strategy.", resource.Type, resource.Name, resource.ID, *webhook.Auth.AccessStrategy) 209 executor, err := c.accessStrategyExecutorProvider.Provide(accessstrategy.Type(*webhook.Auth.AccessStrategy)) 210 if err != nil { 211 return nil, errors.Wrapf(err, "cannot find executor for access strategy %q as part of webhook processing", *webhook.Auth.AccessStrategy) 212 } 213 resp, err = executor.Execute(ctx, c.Client, *webhook.URL, tenantValue) 214 if err != nil { 215 return nil, errors.Wrapf(err, "error while fetching open resource discovery well-known configuration with access strategy %q", *webhook.Auth.AccessStrategy) 216 } 217 } else if webhook.Auth != nil { 218 log.C(ctx).Infof("%s %q (id = %q) configuration endpoint is secured and webhook credentials will be used", resource.Type, resource.Name, resource.ID) 219 resp, err = httputil.GetRequestWithCredentials(ctx, c.Client, *webhook.URL, tenantValue, webhook.Auth) 220 if err != nil { 221 return nil, errors.Wrap(err, "error while fetching open resource discovery well-known configuration with webhook credentials") 222 } 223 } else { 224 log.C(ctx).Infof("%s %q (id = %q) configuration endpoint is not secured", resource.Type, resource.Name, resource.ID) 225 resp, err = httputil.GetRequestWithoutCredentials(c.Client, *webhook.URL, tenantValue) 226 if err != nil { 227 return nil, errors.Wrap(err, "error while fetching open resource discovery well-known configuration") 228 } 229 } 230 231 defer closeBody(ctx, resp.Body) 232 233 bodyBytes, err := io.ReadAll(resp.Body) 234 if err != nil { 235 return nil, errors.Wrap(err, "error reading response body") 236 } 237 238 if resp.StatusCode != http.StatusOK { 239 return nil, errors.Errorf("error while fetching open resource discovery well-known configuration: status code %d Body: %s", resp.StatusCode, string(bodyBytes)) 240 } 241 242 config := WellKnownConfig{} 243 if err := json.Unmarshal(bodyBytes, &config); err != nil { 244 return nil, errors.Wrap(err, "error unmarshaling json body") 245 } 246 247 return &config, nil 248 } 249 250 func buildDocumentURL(docURL, baseURL string) (string, error) { 251 docURLParsed, err := url.Parse(docURL) 252 if err != nil { 253 return "", err 254 } 255 if docURLParsed.IsAbs() { 256 return docURL, nil 257 } 258 return baseURL + docURL, nil 259 } 260 261 // if webhookURL is not /well-known, but there is a valid baseURL provided in the config - use it 262 // if webhookURL is /well-known, strip the suffix and use it as baseURL. In case both are provided - the config baseURL is used. 263 func calculateBaseURL(webhookURL string, config WellKnownConfig) (string, error) { 264 if config.BaseURL != "" { 265 return config.BaseURL, nil 266 } 267 268 parsedWebhookURL, err := url.ParseRequestURI(webhookURL) 269 if err != nil { 270 return "", errors.New("error while parsing input webhook url") 271 } 272 273 if strings.HasSuffix(parsedWebhookURL.Path, WellKnownEndpoint) { 274 strippedPath := strings.ReplaceAll(parsedWebhookURL.Path, WellKnownEndpoint, "") 275 parsedWebhookURL.Path = strippedPath 276 return parsedWebhookURL.String(), nil 277 } 278 return "", nil 279 }