Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2285d35
prepare to publish more than 1 version of a given GVK in a PublishedR…
xrstf May 15, 2025
1650e9d
codegen
xrstf May 15, 2025
ce77504
refactor crd-puller to pull all versions (i.e. work on GK basis not G…
xrstf May 16, 2025
3c7647a
create new APIResourceSchemas when CRDs or PRs change, improve mutati…
xrstf May 16, 2025
a8bb1e8
correctly merge and update existing ResourceSchemas with new ones whe…
xrstf May 16, 2025
382725f
update remaining controllers to perform the new routine to determine …
xrstf May 16, 2025
4eb8c44
fix discovery of built-in resources (oh Kubernetes, why u like this s…
xrstf May 16, 2025
4fd3f75
lint
xrstf May 16, 2025
f332be3
add API conversion to PublishedResources
xrstf May 16, 2025
193da9a
codegen
xrstf May 16, 2025
d0d8c1f
add reconciling config for APIConversions
xrstf May 16, 2025
3acf0d1
codegen
xrstf May 16, 2025
3640fe5
simplify projecting versions
xrstf May 16, 2025
50e77d1
codegen
xrstf May 16, 2025
92d0af9
add logic to apiresourceschema ctrl to maintain a matching APIConvers…
xrstf May 16, 2025
589d829
add discovery e2e tests
xrstf May 20, 2025
7bf4455
add more unit tests for CRD projections
xrstf May 20, 2025
f03b863
test conversion rule projection
xrstf May 20, 2025
dfccf04
add more e2e tests for the APIExport controller
xrstf May 20, 2025
14023e1
fix: make apiresourceschema controller watch for CRD changes
xrstf May 20, 2025
e911d9b
make selecting ARS easier by labelling them with the agent name
xrstf May 20, 2025
5f5c89d
this test obviously failed since we fixed the CRD watching issue earl…
xrstf May 20, 2025
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
2 changes: 2 additions & 0 deletions cmd/crd-puller/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/crd-puller
*.yaml
9 changes: 5 additions & 4 deletions cmd/crd-puller/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# CRD Puller

The `crd-puller` can be used for testing and development in order to export a
CustomResourceDefinition for any Group/Version/Kind (GVK) in a Kubernetes cluster.
CustomResourceDefinition for any Group/Kind (GK) in a Kubernetes cluster.

The main difference between this and kcp's own `crd-puller` is that this one
works based on GVKs and not resources (i.e. on `apps/v1 Deployment` instead of
works based on GKs and not resources (i.e. on `apps/Deployment` instead of
`apps.deployments`). This is more useful since a PublishedResource publishes a
specific Kind and version.
specific Kind and version. Also, this puller pulls all available versions, not
just the preferred version.

## Usage

```shell
export KUBECONFIG=/path/to/kubeconfig

./crd-puller Deployment.v1.apps.k8s.io
./crd-puller Deployment.apps.k8s.io
```
9 changes: 3 additions & 6 deletions cmd/crd-puller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,10 @@ func main() {
pflag.Parse()

if pflag.NArg() == 0 {
log.Fatal("No argument given. Please specify a GVK in the form 'Kind.version.apigroup.com' to pull.")
log.Fatal("No argument given. Please specify a GroupKind in the form 'Kind.apigroup.com' (case-sensitive) to pull.")
}

gvk, _ := schema.ParseKindArg(pflag.Arg(0))
if gvk == nil {
log.Fatal("Invalid GVK, please use the format 'Kind.version.apigroup.com'.")
}
gk := schema.ParseGroupKind(pflag.Arg(0))

loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeconfigPath
Expand All @@ -67,7 +64,7 @@ func main() {
log.Fatalf("Failed to create discovery client: %v.", err)
}

crd, err := discoveryClient.RetrieveCRD(ctx, *gvk)
crd, err := discoveryClient.RetrieveCRD(ctx, gk)
if err != nil {
log.Fatalf("Failed to pull CRD: %v.", err)
}
Expand Down
101 changes: 97 additions & 4 deletions deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,73 @@ spec:
PublishedResourceSpec describes the desired resource publication from a service
cluster to kcp.
properties:
conversions:
description: |-
Conversions specify rules to convert between different API versions in the selected CRD.
This field is required when more than one version is published into kcp.

The from and to versions in each conversion refer to the local CRD versions, i.e. before
any projection rules are applied. They will be automatically mutated during reconciliation.
items:
description: |-
APIVersionConversion contains rules to convert between two specific API versions in an
APIResourceSchema. Additionally, to avoid data loss when round-tripping from a version that
contains a new field to one that doesn't and back again, you can specify a list of fields to
preserve (these are stored in annotations).
properties:
from:
description: from is the source version.
minLength: 1
pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$
type: string
preserve:
description: |-
preserve contains a list of JSONPath expressions to fields to preserve in the originating version
of the object, relative to its root, such as '.spec.name.first'.
items:
type: string
type: array
rules:
description: rules contains field-specific conversion expressions.
items:
description: APIConversionRule specifies how to convert a single field.
properties:
destination:
description: |-
destination is a JSONPath expression to the field in the target version of the object, relative to
its root, such as '.spec.name.first'.
minLength: 1
type: string
field:
description: |-
field is a JSONPath expression to the field in the originating version of the object, relative to its root, such
as '.spec.name.first'.
minLength: 1
type: string
transformation:
description: |-
transformation is an optional CEL expression used to execute user-specified rules to transform the
originating field -- identified by 'self' -- to the destination field.
type: string
required:
- destination
- field
type: object
type: array
x-kubernetes-list-map-keys:
- destination
x-kubernetes-list-type: map
to:
description: to is the target version.
minLength: 1
pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$
type: string
required:
- from
- rules
- to
type: object
type: array
enableWorkspacePaths:
description: |-
EnableWorkspacePaths toggles whether the Sync Agent will not just store the kcp
Expand Down Expand Up @@ -286,7 +353,7 @@ spec:
type: string
type: array
group:
description: The API group, for example "myservice.example.com".
description: The API group, for example "myservice.example.com". Leave empty to not modify the API group.
type: string
kind:
description: |-
Expand Down Expand Up @@ -316,10 +383,25 @@ spec:
type: string
type: array
version:
description: The API version, for example "v1beta1".
description: |-
The API version, for example "v1beta1". Leave empty to not modify the version.

This field must not be set when multiple versions have been selected.

Deprecated: Use .versions instead.
type: string
versions:
additionalProperties:
type: string
description: |-
Versions allows to map API versions onto new values in kcp. Leave empty to not modify the
versions.
type: object
type: object
related:
description: |-
Related describes additional objects that belong to a primary object. These related objects
can be synced along the primary object in both directions of the sync.
items:
properties:
identifier:
Expand Down Expand Up @@ -674,12 +756,23 @@ spec:
description: The resource Kind, for example "Database".
type: string
version:
description: The API version, for example "v1beta1".
description: |-
The API version, for example "v1beta1". Setting this field will only publish
the given version, otherwise all versions for the group/kind will be
published.

Deprecated: Use .versions instead.
type: string
versions:
description: |-
Versions allows to select a subset of versions to publish. Leave empty
to publish all available versions.
items:
type: string
type: array
required:
- apiGroup
- kind
- version
type: object
required:
- resource
Expand Down
2 changes: 1 addition & 1 deletion hack/reconciling.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ boilerplate: hack/boilerplate/generated/boilerplate.go.txt
resourceTypes:
# kcp-dev/v1alpha1
- { package: github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIExport }
- { package: github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIResourceSchema }
- { package: github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIConversion }
10 changes: 4 additions & 6 deletions internal/controller/apiexport/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package apiexport
import (
"context"
"fmt"
"slices"

"github.com/kcp-dev/logicalcluster/v3"
"go.uber.org/zap"
Expand Down Expand Up @@ -121,12 +122,9 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
}

// filter out those PRs that have not yet been processed into an ARS
filteredPubResources := []syncagentv1alpha1.PublishedResource{}
for i, pubResource := range pubResources.Items {
if pubResource.Status.ResourceSchemaName != "" {
filteredPubResources = append(filteredPubResources, pubResources.Items[i])
}
}
filteredPubResources := slices.DeleteFunc(pubResources.Items, func(pr syncagentv1alpha1.PublishedResource) bool {
return pr.Status.ResourceSchemaName == ""
})

// for each PR, we note down the created ARS and also the GVKs of related resources
arsList := sets.New[string]()
Expand Down
48 changes: 40 additions & 8 deletions internal/controller/apiexport/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ limitations under the License.
package apiexport

import (
"cmp"
"slices"
"strings"

"github.com/kcp-dev/api-syncagent/internal/resources/reconciling"
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
Expand All @@ -34,16 +34,13 @@ import (
func (r *Reconciler) createAPIExportReconciler(availableResourceSchemas sets.Set[string], claimedResourceKinds sets.Set[string], agentName string, apiExportName string) reconciling.NamedAPIExportReconcilerFactory {
return func() (string, reconciling.APIExportReconciler) {
return apiExportName, func(existing *kcpdevv1alpha1.APIExport) (*kcpdevv1alpha1.APIExport, error) {
known := sets.New(existing.Spec.LatestResourceSchemas...)

if existing.Annotations == nil {
existing.Annotations = map[string]string{}
}
existing.Annotations[syncagentv1alpha1.AgentNameAnnotation] = agentName

// we only ever add new schemas
result := known.Union(availableResourceSchemas)
existing.Spec.LatestResourceSchemas = sets.List(result)
// combine existing schemas with new ones
existing.Spec.LatestResourceSchemas = mergeResourceSchemas(existing.Spec.LatestResourceSchemas, availableResourceSchemas)

// To allow admins to configure additional permission claims, sometimes
// useful for debugging, we do not override the permission claims, but
Expand Down Expand Up @@ -73,11 +70,11 @@ func (r *Reconciler) createAPIExportReconciler(availableResourceSchemas sets.Set
// prevent reconcile loops by ensuring a stable order
slices.SortFunc(existing.Spec.PermissionClaims, func(a, b kcpdevv1alpha1.PermissionClaim) int {
if a.Group != b.Group {
return cmp.Compare(a.Group, b.Group)
return strings.Compare(a.Group, b.Group)
}

if a.Resource != b.Resource {
return cmp.Compare(a.Resource, b.Resource)
return strings.Compare(a.Resource, b.Resource)
}

return 0
Expand All @@ -87,3 +84,38 @@ func (r *Reconciler) createAPIExportReconciler(availableResourceSchemas sets.Set
}
}
}

func mergeResourceSchemas(existing []string, configured sets.Set[string]) []string {
var result []string

// first we copy all ARS that are coming from the PublishedResources
knownResources := sets.New[string]()
for _, schema := range configured.UnsortedList() {
result = append(result, schema)
knownResources.Insert(parseResourceGroup(schema))
}

// Now we include all other existing ARS that use unknown resources;
// this both allows an APIExport to contain "unmanaged" ARS, and also
// will purposefully leave behind ARS for deleted PublishedResources,
// allowing cleanup to take place outside of the agent's control.
for _, schema := range existing {
if !knownResources.Has(parseResourceGroup(schema)) {
result = append(result, schema)
}
}

// for stability and beauty, sort the schemas
slices.SortFunc(result, func(a, b string) int {
return strings.Compare(parseResourceGroup(a), parseResourceGroup(b))
})

return result
}

func parseResourceGroup(schema string) string {
// <version>.<resource>.<group>
parts := strings.SplitN(schema, ".", 2)

return parts[1]
}
Loading