Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ build: $(CMD)
.PHONY: $(CMD)
$(CMD): %: $(BUILD_DEST)/%

$(BUILD_DEST)/%: cmd/%
$(BUILD_DEST)/%: clean cmd/%
go build $(GOTOOLFLAGS) -o $@ ./cmd/$*

GOLANGCI_LINT = _tools/golangci-lint
Expand Down
50 changes: 49 additions & 1 deletion deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ spec:
type: object
related:
items:
description: RelatedResourceSpec describes a related resource that should be synchronized
properties:
identifier:
description: |-
Expand All @@ -329,6 +330,50 @@ spec:
kind:
description: ConfigMap or Secret
type: string
labelSelector:
description: LabelSelector is used to filter the related resource in the service cluster.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
mutation:
description: |-
Mutation configures optional transformation rules for the related resource.
Expand Down Expand Up @@ -411,6 +456,7 @@ spec:
description: '"service" or "kcp"'
type: string
reference:
description: Reference to the related resource in the service cluster.
properties:
name:
properties:
Expand Down Expand Up @@ -453,8 +499,10 @@ spec:
- identifier
- kind
- origin
- reference
type: object
x-kubernetes-validations:
- message: must specify exactly one of reference or labelSelector
rule: (has(self.reference) && !has(self.labelSelector)) || (!has(self.reference) && has(self.labelSelector))
type: array
resource:
description: |-
Expand Down
36 changes: 28 additions & 8 deletions internal/projection/naming.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,7 @@ func GenerateLocalObjectName(pr *syncagentv1alpha1.PublishedResource, object met
naming = &syncagentv1alpha1.ResourceNaming{}
}

replacer := strings.NewReplacer(
// order of elements is important here, "$fooHash" needs to be defined before "$foo"
syncagentv1alpha1.PlaceholderRemoteClusterName, clusterName.String(),
syncagentv1alpha1.PlaceholderRemoteNamespaceHash, shortSha1Hash(object.GetNamespace()),
syncagentv1alpha1.PlaceholderRemoteNamespace, object.GetNamespace(),
syncagentv1alpha1.PlaceholderRemoteNameHash, shortSha1Hash(object.GetName()),
syncagentv1alpha1.PlaceholderRemoteName, object.GetName(),
)
replacer := getReplacer(object, clusterName)

result := types.NamespacedName{}

Expand All @@ -69,6 +62,33 @@ func GenerateLocalObjectName(pr *syncagentv1alpha1.PublishedResource, object met
return result
}

func GenerateLocalLabelSelector(pr *syncagentv1alpha1.RelatedResourceSpec, object metav1.Object, clusterName logicalcluster.Name) *metav1.LabelSelector {
replacer := getReplacer(object, clusterName)

result := metav1.LabelSelector{}

result.MatchLabels = map[string]string{}

for key, value := range pr.LabelSelector.MatchLabels {
result.MatchLabels[replacer.Replace(key)] = replacer.Replace(value)
}

// TODO: MatchExpressions are not yet supported with the current naming scheme.
result.MatchExpressions = pr.LabelSelector.MatchExpressions
return &result
}

func getReplacer(object metav1.Object, clusterName logicalcluster.Name) *strings.Replacer {
return strings.NewReplacer(
// order of elements is important here, "$fooHash" needs to be defined before "$foo"
syncagentv1alpha1.PlaceholderRemoteClusterName, clusterName.String(),
syncagentv1alpha1.PlaceholderRemoteNamespaceHash, shortSha1Hash(object.GetNamespace()),
syncagentv1alpha1.PlaceholderRemoteNamespace, object.GetNamespace(),
syncagentv1alpha1.PlaceholderRemoteNameHash, shortSha1Hash(object.GetName()),
syncagentv1alpha1.PlaceholderRemoteName, object.GetName(),
)
}

func shortSha1Hash(value string) string {
hash := sha1.New()
if _, err := hash.Write([]byte(value)); err != nil {
Expand Down
57 changes: 50 additions & 7 deletions internal/sync/syncer_related.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
"go.uber.org/zap"

"github.com/kcp-dev/api-syncagent/internal/mutation"
"github.com/kcp-dev/api-syncagent/internal/projection"
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
"github.com/kcp-dev/logicalcluster/v3"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -70,10 +72,42 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto
dest = local
}

// to find the source related object, we first need to determine its name/namespace
sourceKey, err := resolveResourceReference(source.object, relRes.Reference)
if err != nil {
return false, fmt.Errorf("failed to determine related object's source key: %w", err)
var sourceKey *ctrlruntimeclient.ObjectKey
switch {
case relRes.Reference != nil:
// to find the source related object, we first need to determine its name/namespace
sourceKey, err = resolveResourceReference(source.object, *relRes.Reference)
if err != nil {
return false, fmt.Errorf("failed to determine related object's source key: %w", err)
}
case relRes.LabelSelector != nil:
// if no reference is given, we can use a label selector to find the source object
sourceObjs := &unstructured.UnstructuredList{}
sourceObjs.SetAPIVersion("v1")
sourceObjs.SetKind(relRes.Kind)

// TODO: would need to handle replacer here as well to select right object.
Copy link
Author

Choose a reason for hiding this comment

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

This is where it get tricky. You want to be able to use projections, and I think only place where we can get it is from "owning object" to which this resource is related. So this might need some wiring in...

cn, ok := source.object.GetLabels()[remoteObjectClusterLabel]
if !ok {
return false, fmt.Errorf("missing cluster label on source object")
}
clusterName := logicalcluster.Name(cn)
labels := projection.GenerateLocalLabelSelector(&relRes, source.object, clusterName)

if err := source.client.List(source.ctx, sourceObjs, ctrlruntimeclient.MatchingLabels(labels.MatchLabels)); err != nil {
return false, fmt.Errorf("failed to list related objects: %w", err)
}
if len(sourceObjs.Items) == 0 {
return false, nil
}
if len(sourceObjs.Items) > 1 {
// HACK: we take first one, as we can't select multiple objects or by name only!
log.Warnw("found multiple related objects, taking first one", "count", len(sourceObjs.Items))
}
sourceKey = &ctrlruntimeclient.ObjectKey{
Namespace: sourceObjs.Items[0].GetNamespace(),
Name: sourceObjs.Items[0].GetName(),
}
}

// find the source related object
Expand All @@ -92,9 +126,18 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto
}

// do the same to find the destination object
destKey, err := resolveResourceReference(dest.object, relRes.Reference)
if err != nil {
return false, fmt.Errorf("failed to determine related object's destination key: %w", err)
var destKey *ctrlruntimeclient.ObjectKey
switch {
case relRes.Reference != nil:
destKey, err = resolveResourceReference(dest.object, *relRes.Reference)
if err != nil {
return false, fmt.Errorf("failed to determine related object's destination key: %w", err)
}
case relRes.LabelSelector != nil:
destKey = &ctrlruntimeclient.ObjectKey{
Namespace: sourceObj.GetNamespace(),
Name: sourceObj.GetName(),
}
}

destObj := &unstructured.Unstructured{}
Expand Down
8 changes: 7 additions & 1 deletion sdk/apis/syncagent/v1alpha1/published_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ type ResourceTemplateMutation struct {
Template string `json:"template"`
}

// +kubebuilder:validation:XValidation:rule="(has(self.reference) && !has(self.labelSelector)) || (!has(self.reference) && has(self.labelSelector))",message="must specify exactly one of reference or labelSelector"
// RelatedResourceSpec describes a related resource that should be synchronized
type RelatedResourceSpec struct {
// Identifier is a unique name for this related resource. The name must be unique within one
// PublishedResource and is the key by which consumers (end users) can identify and consume the
Expand All @@ -171,7 +173,11 @@ type RelatedResourceSpec struct {
// ConfigMap or Secret
Kind string `json:"kind"`

Reference RelatedResourceReference `json:"reference"`
// Reference to the related resource in the service cluster.
Reference *RelatedResourceReference `json:"reference,omitempty"`

// LabelSelector is used to filter the related resource in the service cluster.
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`

// Mutation configures optional transformation rules for the related resource.
// Status mutations are only performed when the related resource originates in kcp.
Expand Down
11 changes: 10 additions & 1 deletion sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading