Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UPSTREAM: <carry>: with shard prefix support #96

Conversation

p0lyn0mial
Copy link

@p0lyn0mial p0lyn0mial commented Aug 26, 2022

What type of PR is this?

What this PR does / why we need it:

This PR adds support for setting a shard prefix to the storage layer.
The prefix is used by the cache server.
The storage layer has to understand both the cluster and the shard prefixes
because the database can be shared between the kcp and the cache server.

Once a shard name is set, the storage layer will change the key under which a resource is stored, for example:

amber/root:org/default/secret
amber/root:org/secret

Where:

  • amber: is a shard name
  • root:org: is a cluster name
  • default: is a namespace name
  • secret: is a name of a resource

Which issue(s) this PR fixes:

Fixes #

Special notes for your reviewer:

Does this PR introduce a user-facing change?


Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:


@@ -238,15 +238,23 @@ const (
// to resource directories enforcing namespace rules.
func NoNamespaceKeyRootFunc(ctx context.Context, prefix string) string {
key := prefix
shard, err := genericapirequest.ValidShardFrom(ctx)
Copy link
Author

Choose a reason for hiding this comment

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

we need a better way of plugging a shard name into the storage layer.
something that the kcp and the cache server could customize upon initialization.

Copy link
Author

Choose a reason for hiding this comment

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

initially, I wanted to plug in different implementations but I realised the kcp and the cache server might share the same database instance.


type Shard struct {
// Name is the name of the cluster.
Name string

Choose a reason for hiding this comment

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

Given that we (likely) have shard name requirements, and * will not be a valid shard name, is there any reason not just have type Shard string? Otherwise, what is the meaning of:

Shard{
  Name: "*",
  Wildcard: false,
}

or

Shard{
  Name: "sapphire",
  Wildcard: true,
}

?

Copy link
Author

Choose a reason for hiding this comment

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

a request like 'https://localhost:6443/shards/*/clusters/*/apis/apis.kcp.dev/v1alpha1/apiexports' will set

Shard{ 
  Name: "", 
  Wildcard: true
 }

a request like https://localhost:6443/shards/sapphire/clusters/system:sapphire/apis/apis.kcp.dev/v1alpha1/apiexports' will set

Shard{ 
  Name: "sapphire", 
  Wildcard: false
 }

Choose a reason for hiding this comment

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

Right, those are the happy paths. My question was about the others, which are (I think) not possible. In e.g. kcp-dev/kcp#1841 you have a New('*") value for wildcard... seems like this could also ?

Copy link
Author

Choose a reason for hiding this comment

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

I do use New(*) on the client side just to set the right URL path, then on the server side, it gets mapped to Wildcard: true, Name: "".

I think we need both fields. An empty Name:"", Wildcard: false indicates no shard was specified.

Choose a reason for hiding this comment

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

How is Name:"", Wildcard: false different from New("")?

Choose a reason for hiding this comment

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

My argument: the Wildcard field as you have written it is logically equivalent in every case to obj.Name == "*"

Copy link
Author

Choose a reason for hiding this comment

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

@stevekuznetsov I updated the type to type Shard string, now Shard("*") indicates wildcard and Shard("") indicates a request without a shard name specified.

Copy link
Author

Choose a reason for hiding this comment

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

I hope it is less confusing now.

@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch 3 times, most recently from e120a07 to 4147b2a Compare August 30, 2022 11:46
@p0lyn0mial p0lyn0mial changed the title WIP: shard prefix UPSTREAM: <carry>: with shard prefix support Aug 30, 2022
@p0lyn0mial
Copy link
Author

/assign @stevekuznetsov @sttts

ready for review, PTAL.

@p0lyn0mial
Copy link
Author

proof PR kcp-dev/kcp#1851

Name string

// If true the query applies to all shards
Wildcard bool
Copy link
Member

@sttts sttts Aug 31, 2022

Choose a reason for hiding this comment

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

why this complex struct again? Why not "empty shard string = wildcard" or "* shard string = wildcard" ?

Note: we should do the same for the cluster.

Copy link
Author

Choose a reason for hiding this comment

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

we need both, I can replace with a string but then we need to compare to *

@@ -133,8 +134,12 @@ func (w *watcher) Watch(ctx context.Context, key string, rev int64, recursive bo
if err != nil {
return nil, err
}
shard, err := genericapirequest.ValidShardFrom(ctx)
Copy link
Member

Choose a reason for hiding this comment

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

what does this return if there is no shard in the context?

Copy link
Author

Choose a reason for hiding this comment

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

// ValidShardFrom returns the value of the shard key from the context.
// An empty value is returned if there's no shard key,
// or if the shard name is empty, and it wasn't a wildcard request.

// ValidShardFrom returns the value of the shard key from the context.
// An empty value is returned if there's no shard key,
// or if the shard name is empty, and it wasn't a wildcard request.
func ValidShardFrom(ctx context.Context) (*Shard, error) {
Copy link
Member

Choose a reason for hiding this comment

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

why "Valid"? ShardFromOrEmpty would be more idiomatic

Copy link
Author

Choose a reason for hiding this comment

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

true

@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch from 4147b2a to 9333285 Compare August 31, 2022 09:47
@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch 2 times, most recently from ba40ff6 to 97e00f1 Compare August 31, 2022 10:41
@@ -45,26 +51,31 @@ var (
// adjustClusterNameIfWildcard determines the logical cluster name. If this is not a wildcard list/watch request,
// the cluster name is returned unmodified. Otherwise, the cluster name is extracted from the key based on whether
// it is a CR partial metadata request (prefix/identity/clusterName/remainder) or not (prefix/clusterName/remainder).
func adjustClusterNameIfWildcard(cluster *genericapirequest.Cluster, crdRequest bool, keyPrefix, key string) logicalcluster.Name {
func adjustClusterNameIfWildcard(shard genericapirequest.Shard, cluster *genericapirequest.Cluster, crdRequest bool, keyPrefix, key string) logicalcluster.Name {
if cluster.Name != logicalcluster.Wildcard {
return cluster.Name
}

keyWithoutPrefix := strings.TrimPrefix(key, keyPrefix)

var regex *regexp.Regexp
Copy link
Member

Choose a reason for hiding this comment

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

move this into the first if.

Copy link
Author

Choose a reason for hiding this comment

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

done.

@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch from 97e00f1 to 6986f91 Compare August 31, 2022 12:21

// ShardFrom returns the value of the shard key in the context, or an empty value if there is no shard key.
func ShardFrom(ctx context.Context) Shard {
shard, ok := ctx.Value(shardContextKey).(Shard)

Choose a reason for hiding this comment

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

nit: this simplifies to

func ShardFrom(ctx context.Context) Shard {
	return ctx.Value(shardContextKey).(Shard)
}

playground

Copy link
Author

Choose a reason for hiding this comment

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

it could, it will just panic if the value under the key cannot be cast to Shard

Choose a reason for hiding this comment

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

The shardContextKey is private to this package. The only way that it would panic is if someone wrote code in this package to add the wrong thing. /shrug

Copy link
Author

Choose a reason for hiding this comment

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

yes, I just don't like when the code panics but sure I can change it.

Copy link
Author

Choose a reason for hiding this comment

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

and it broke the kcp server

I0901 10:30:42.331404   38583 etcd3.go:306] Start monitoring storage db size metric for endpoint https://localhost:45743 with polling interval 30s
        panic: interface conversion: interface {} is nil, not request.Shard
        
        goroutine 1 [running]:
        k8s.io/apiserver/pkg/endpoints/request.ShardFrom(...)
        	/go/pkg/mod/github.com/p0lyn0mial/kubernetes/staging/src/k8s.io/apiserver@v0.0.0-20220901092652-1cf194952b52/pkg/endpoints/request/context_shard.go:56

cluster, err := genericapirequest.ValidClusterFrom(ctx)
if err != nil {
klog.Errorf("invalid context cluster value: %v", err)
return key
}
if cluster.Wildcard {

Choose a reason for hiding this comment

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

This is a bit hard to follow. Is it equivalent:

if !shard.Empty() {
    key += "/" + shard.String()
}

if !cluster.Wildcard {
    key += "/" + cluster.Name.String()
}

return key

Copy link
Author

Choose a reason for hiding this comment

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

it seems it is :)

Copy link
Author

Choose a reason for hiding this comment

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

done

@@ -45,26 +51,31 @@ var (
// adjustClusterNameIfWildcard determines the logical cluster name. If this is not a wildcard list/watch request,
// the cluster name is returned unmodified. Otherwise, the cluster name is extracted from the key based on whether
// it is a CR partial metadata request (prefix/identity/clusterName/remainder) or not (prefix/clusterName/remainder).
func adjustClusterNameIfWildcard(cluster *genericapirequest.Cluster, crdRequest bool, keyPrefix, key string) logicalcluster.Name {
func adjustClusterNameIfWildcard(shard genericapirequest.Shard, cluster *genericapirequest.Cluster, crdRequest bool, keyPrefix, key string) logicalcluster.Name {

Choose a reason for hiding this comment

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

IMHO this doesn't really make sense to do with regex. What about something like:

 func adjustClusterNameIfWildcard(shard genericapirequest.Shard, cluster *genericapirequest.Cluster, crdRequest bool, keyPrefix, key string) logicalcluster.Name {
    if cluster.Name != logicalcluster.Wildcard || !cluster.Wildcard { // TODO: fix this duplicity, as well
        return cluster.Name
    }
    
    keyWithoutPrefix := strings.TrimPrefix(key, keyPrefix)
    
    var extractCluster func([]string) logicalcluster.Name
    extractor := func(numParts, clusterPart int) func([]string) logicalcluster.Name {
        return func([]string) logicalcluster.Name {
            if len(parts) < numParts {
                klog.Warnf("shard=%s cluster=%s invalid key=%s had %d parts, not %d", shard, cluster, keyWIthoutPrefix, len(parts), numParts)
                return logicalcluster.Name{}
            }
            return logicalcluster.New(parts[clusterPart])
        }
    }
    
    switch {
    case shard.Wildcard():
        // expecting shardName/clusterName/<remainder>
        extractCluster = extractor(3,1)
    case cluster.PartialMetadataRequest && crdRequest:
        // expecting 2699f4d273d342adccdc8a32663408226ecf66de7d191113ed3d4dc9bccec2f2/root:org:ws/<remainder>
        // OR customresources/root:org:ws/<remainder>
        extractCluster = extractor(3,1)
    case cluster.Wildcard():
        // expecting root:org:ws/<remainder>
        extractCluster = extractor(2,0)
    default:
        panic("bad programmer")
    }
    
    parts := strings.Split(keyWithoutPrefix, "/")
    return extractCluster(parts)
}

Copy link
Author

@p0lyn0mial p0lyn0mial Aug 31, 2022

Choose a reason for hiding this comment

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

uh, maybe :) It seems like what we have right now requires less code.
Maybe I could revisit this in the future and try to rewrite (refactor) it.

Copy link

@stevekuznetsov stevekuznetsov Aug 31, 2022

Choose a reason for hiding this comment

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

Less code how exactly? Half of what I wrote were comments ;) Existing approach with your diff is L29-L80=51LoC. Mine is 35LoC

I think using regex to strings.Split(key, "/") makes the current approach really hard to follow, especially as we grow more cases.

Copy link
Member

Choose a reason for hiding this comment

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

Is it about taste or a big improvement? Either way is not particularly pretty. But this func does not decide about maintainability of the code, does it? Would be fine with a minimal change. Would pretty much prefer a unit test for either implementation ;-)

Copy link
Author

Choose a reason for hiding this comment

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

@stevekuznetsov would it be okay to add unit tests and leave using the regexps for now?

Copy link
Author

Choose a reason for hiding this comment

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

btw: do we actually run k8s unit tests :) ?

Choose a reason for hiding this comment

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

I don't think it's taste, no, because using regex to split a string on / is really hard to understand - I think we all tripped over understanding this function when reviewing it, no? Indexing into a slice seems ... slightly more straightforward, as well as using switch/case to be exhaustive on options?

No, we do not yet run the tests.

Copy link
Member

Choose a reason for hiding this comment

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

@p0lyn0mial can you create an issue to track Steve's suggestion above? Would not like to block this PR on a refactoring that is kind of independent of this change because pre-existing.

Copy link
Member

Choose a reason for hiding this comment

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

keep the suggested code snipped, or even create the follow-up PR just after if it compiles.

Copy link
Author

Choose a reason for hiding this comment

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

@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch 2 times, most recently from a5ea09c to 4ac288e Compare September 1, 2022 09:07
clusterWildcard bool
shardWildcard bool
prefix string
builtInType bool
Copy link
Member

Choose a reason for hiding this comment

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

can this be pure table test with inputs and outputs? Irritating there is no "expected*" in here. Just put genericapirequest.Cluster and genericapirequest.Shard in here, no need for abstraction.

Copy link
Author

Choose a reason for hiding this comment

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

updated

@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch from 4ac288e to 1cf1949 Compare September 1, 2022 09:27
@sttts
Copy link
Member

sttts commented Sep 1, 2022

/lgtm
/approve

This PR adds support for setting a shard prefix to the storage layer.
The prefix is used by the cache server.
The storage layer has to understand both the cluster and the shard prefixes
because the database can be shared between the kcp and the cache server.

Once a shard name is set, the storage layer will change the key under which a resource is stored, for example:

amber/root:org/default/secret
amber/root:org/secret

Where:

- amber:     is a shard name
- root:org:  is a cluster name
- default:   is a namespace name
- secret:    is a name of a resource
@p0lyn0mial p0lyn0mial force-pushed the kcp-1.24-shard-prefix branch from 1cf1949 to c8d74a9 Compare September 1, 2022 10:37
@sttts sttts merged commit dc9535a into kcp-dev:feature-logical-clusters-1.24 Sep 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants