-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1625 from keboola/jt-PSGO-386-sandboxes-service
feat: Implement appconfig package
- Loading branch information
Showing
5 changed files
with
549 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package appconfig | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/keboola/go-client/pkg/request" | ||
"github.com/pquerna/cachecontrol/cacheobject" | ||
) | ||
|
||
type AppProxyConfig struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
UpstreamAppHost string `json:"upstreamAppHost"` | ||
AuthProviders []AuthProvider `json:"authProviders"` | ||
AuthRules []AuthRule `json:"authRules"` | ||
eTag string | ||
maxAge time.Duration | ||
} | ||
|
||
type AuthProvider struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
ClientID string `json:"clientId"` | ||
ClientSecret string `json:"clientSecret"` | ||
IssuerURL string `json:"issuerUrl"` | ||
AllowedRoles []string `json:"allowedRoles"` | ||
} | ||
|
||
type AuthRule struct { | ||
Type string `json:"type"` | ||
Value string `json:"value"` | ||
Auth []string `json:"auth"` | ||
} | ||
|
||
func GetAppProxyConfig(sender request.Sender, appID string, eTag string) request.APIRequest[*AppProxyConfig] { | ||
result := &AppProxyConfig{} | ||
req := request.NewHTTPRequest(sender). | ||
WithError(&SandboxesError{}). | ||
WithResult(result). | ||
WithGet("apps/{appId}/proxy-config"). | ||
AndPathParam("appId", appID). | ||
AndHeader("If-None-Match", eTag). | ||
WithOnSuccess(func(ctx context.Context, response request.HTTPResponse) error { | ||
// Use id as fallback until name is added to Sandboxes API | ||
if result.Name == "" { | ||
result.Name = result.ID | ||
} | ||
for i, provider := range result.AuthProviders { | ||
if provider.Name == "" { | ||
result.AuthProviders[i].Name = provider.ID | ||
} | ||
} | ||
|
||
// Add ETag to result | ||
result.eTag = response.ResponseHeader().Get("ETag") | ||
|
||
// Process Cache-Control header | ||
cacheControl := response.ResponseHeader().Get("Cache-Control") | ||
if cacheControl == "" { | ||
return nil | ||
} | ||
|
||
cacheDirectives, err := cacheobject.ParseResponseCacheControl(cacheControl) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if cacheDirectives.NoStore || cacheDirectives.NoCache != nil { | ||
return nil | ||
} | ||
|
||
result.maxAge = time.Second * time.Duration(cacheDirectives.MaxAge) | ||
return nil | ||
}) | ||
return request.NewAPIRequest(result, req) | ||
} | ||
|
||
// SandboxesError represents the structure of API error. | ||
type SandboxesError struct { | ||
Message string `json:"error"` | ||
ExceptionID string `json:"exceptionId"` | ||
request *http.Request | ||
response *http.Response | ||
} | ||
|
||
func (e *SandboxesError) Error() string { | ||
return fmt.Sprintf("sandboxes api error[%d]: %s", e.StatusCode(), e.Message) | ||
} | ||
|
||
// ErrorName returns a human-readable name of the error. | ||
func (e *SandboxesError) ErrorName() string { | ||
return http.StatusText(e.StatusCode()) | ||
} | ||
|
||
// ErrorUserMessage returns error message for end user. | ||
func (e *SandboxesError) ErrorUserMessage() string { | ||
return e.Message | ||
} | ||
|
||
// ErrorExceptionID returns exception ID to find details in logs. | ||
func (e *SandboxesError) ErrorExceptionID() string { | ||
return e.ExceptionID | ||
} | ||
|
||
// StatusCode returns HTTP status code. | ||
func (e *SandboxesError) StatusCode() int { | ||
return e.response.StatusCode | ||
} | ||
|
||
// SetRequest method allows injection of HTTP request to the error, it implements client.errorWithRequest. | ||
func (e *SandboxesError) SetRequest(request *http.Request) { | ||
e.request = request | ||
} | ||
|
||
// SetResponse method allows injection of HTTP response to the error, it implements client.errorWithResponse. | ||
func (e *SandboxesError) SetResponse(response *http.Response) { | ||
e.response = response | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package appconfig | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/benbjohnson/clock" | ||
"github.com/keboola/go-client/pkg/client" | ||
"github.com/keboola/go-client/pkg/request" | ||
"go.opentelemetry.io/otel/attribute" | ||
|
||
"github.com/keboola/keboola-as-code/internal/pkg/log" | ||
"github.com/keboola/keboola-as-code/internal/pkg/utils/errors" | ||
) | ||
|
||
type Loader struct { | ||
logger log.Logger | ||
clock clock.Clock | ||
sender request.Sender | ||
cache map[string]cacheItem | ||
} | ||
|
||
type cacheItem struct { | ||
config AppProxyConfig | ||
eTag string | ||
expiresAt time.Time | ||
} | ||
|
||
func NewLoader(logger log.Logger, clock clock.Clock, baseURL string) *Loader { | ||
return &Loader{ | ||
logger: logger, | ||
clock: clock, | ||
sender: client.New().WithBaseURL(baseURL), | ||
cache: make(map[string]cacheItem), | ||
} | ||
} | ||
|
||
const staleCacheFallbackDuration = time.Hour | ||
|
||
func (l *Loader) LoadConfig(ctx context.Context, appID string) (AppProxyConfig, error) { | ||
var config *AppProxyConfig | ||
var err error | ||
now := l.clock.Now() | ||
|
||
if item, ok := l.cache[appID]; ok { | ||
// Return config from cache if still valid | ||
if now.Before(item.expiresAt) { | ||
return item.config, nil | ||
} | ||
|
||
// API request with cached ETag | ||
config, err = GetAppProxyConfig(l.sender, appID, item.eTag).Send(ctx) | ||
if err != nil { | ||
return l.handleError(ctx, appID, now, err, &item) | ||
} | ||
|
||
// Update expiration and use the cached config if ETag is still the same | ||
if config.eTag == item.eTag { | ||
l.cache[appID] = cacheItem{ | ||
config: item.config, | ||
eTag: item.eTag, | ||
expiresAt: now.Add(config.maxAge), | ||
} | ||
return item.config, nil | ||
} | ||
} else { | ||
// API request without ETag because cache is empty | ||
config, err = GetAppProxyConfig(l.sender, appID, "").Send(ctx) | ||
if err != nil { | ||
return l.handleError(ctx, appID, now, err, nil) | ||
} | ||
} | ||
|
||
// Save result to cache | ||
l.cache[appID] = cacheItem{ | ||
config: *config, | ||
eTag: config.eTag, | ||
expiresAt: now.Add(config.maxAge), | ||
} | ||
return *config, nil | ||
} | ||
|
||
func (l *Loader) handleError(ctx context.Context, appID string, now time.Time, err error, fallbackItem *cacheItem) (AppProxyConfig, error) { | ||
var sandboxesError *SandboxesError | ||
errors.As(err, &sandboxesError) | ||
if sandboxesError != nil && sandboxesError.StatusCode() == http.StatusNotFound { | ||
return AppProxyConfig{}, err | ||
} | ||
|
||
logger := l.logger | ||
if sandboxesError != nil { | ||
logger = l.logger.With(attribute.String("exceptionId", sandboxesError.ExceptionID)) | ||
} | ||
|
||
// An error other than 404 is considered a temporary failure. Keep using the stale cache for staleCacheFallbackDuration as fallback. | ||
if fallbackItem != nil && now.Before(fallbackItem.expiresAt.Add(staleCacheFallbackDuration)) { | ||
logger.Warnf(ctx, `Using stale cache for app "%s": %s`, appID, err.Error()) | ||
|
||
return fallbackItem.config, nil | ||
} | ||
|
||
logger.Errorf(ctx, `Failed loading config for app "%s": %s`, appID, err.Error()) | ||
|
||
return AppProxyConfig{}, err | ||
} |
Oops, something went wrong.