Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions router/core/init_config_poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"

rjwt "github.com/wundergraph/cosmo/router/internal/jwt"
"github.com/wundergraph/cosmo/router/pkg/controlplane/configpoller"
"github.com/wundergraph/cosmo/router/pkg/execution_config"
"github.com/wundergraph/cosmo/router/pkg/routerconfig"
Expand Down Expand Up @@ -125,6 +126,22 @@ func InitializeConfigPoller(r *Router, registry *ProviderRegistry) (*configpolle
return nil, nil
}

// Check whether the router JWT requests the split-config-loading strategy.
// Split config is only supported with the default Cosmo CDN (no custom storage provider).
hasSplitCfgFeature, err := hasSplitConfigFeature(r)
if err != nil {
return nil, err
}

if hasSplitCfgFeature {
providerID := r.routerConfigPollerConfig.Storage.ProviderID
if providerID == "" {
return newSplitConfigPoller(r)
}
r.logger.Info("split-config-loading feature is enabled but a custom storage provider is configured; falling back to regular config polling",
zap.String("provider_id", providerID))
}

primaryClient, err := getConfigClient(r, registry, r.routerConfigPollerConfig.Storage.ProviderID, false)
if err != nil {
return nil, err
Expand Down Expand Up @@ -159,3 +176,37 @@ func InitializeConfigPoller(r *Router, registry *ProviderRegistry) (*configpolle

return &configPoller, nil
}

func hasSplitConfigFeature(r *Router) (bool, error) {
if r.graphApiToken == "" {
return false, nil
}

claims, err := rjwt.ExtractFederatedGraphTokenClaims(r.graphApiToken)
if err != nil {
return false, fmt.Errorf("failed to parse graph API token: %w", err)
}

return claims.HasFeature(rjwt.FeatureSplitConfigLoading), nil
}

func newSplitConfigPoller(r *Router) (*configpoller.ConfigPoller, error) {
fetcher, err := configCDNProvider.NewSplitFetcher(
r.cdnConfig.URL,
r.graphApiToken,
&configCDNProvider.Options{
Logger: r.logger,
SignatureKey: r.routerConfigPollerConfig.GraphSignKey,
},
)
if err != nil {
return nil, fmt.Errorf("failed to create split config fetcher: %w", err)
}

splitPoller := configpoller.NewSplitConfigPoller(
fetcher,
configpoller.WithSplitLogger(r.logger),
configpoller.WithSplitPolling(r.routerConfigPollerConfig.PollInterval, r.routerConfigPollerConfig.PollJitter),
)
return &splitPoller, nil
}
30 changes: 28 additions & 2 deletions router/internal/jwt/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@ package jwt

import (
"fmt"
"slices"

"github.com/golang-jwt/jwt/v5"
)

const (
FederatedGraphIDClaim = "federated_graph_id"
OrganizationIDClaim = "organization_id"
FederatedGraphIDClaim = "federated_graph_id"
OrganizationIDClaim = "organization_id"
FeaturesClaim = "features"
FeatureSplitConfigLoading = "split-config-loading"
)

type FederatedGraphTokenClaims struct {
FederatedGraphID string
OrganizationID string
Features []string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You could also use a hash set here to not use slices.Contains() but have O(1) lookups but I don't think it really matters in this case. Up to you

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we will ever run into the amount of features where we can feel this

}

// HasFeature reports whether the given feature flag is present in the token claims.
func (c *FederatedGraphTokenClaims) HasFeature(feature string) bool {
return slices.Contains(c.Features, feature)
}

func ExtractFederatedGraphTokenClaims(token string) (*FederatedGraphTokenClaims, error) {
Expand Down Expand Up @@ -44,8 +54,24 @@ func ExtractFederatedGraphTokenClaims(token string) (*FederatedGraphTokenClaims,
return nil, fmt.Errorf("invalid token claims, %q is not a string, it's %T", OrganizationIDClaim, organizationIDValue)
}

var features []string
if featuresValue := claims[FeaturesClaim]; featuresValue != nil {
featuresSlice, ok := featuresValue.([]any)
if !ok {
return nil, fmt.Errorf("invalid token claims, %q is not an array, it's %T", FeaturesClaim, featuresValue)
}
for _, v := range featuresSlice {
s, ok := v.(string)
if !ok {
return nil, fmt.Errorf("invalid token claims, %q contains a non-string element %T", FeaturesClaim, v)
}
features = append(features, s)
}
}

return &FederatedGraphTokenClaims{
FederatedGraphID: federatedGraphID,
OrganizationID: organizationID,
Features: features,
}, nil
}
Loading
Loading