Skip to content
This repository was archived by the owner on Jun 22, 2023. It is now read-only.

Commit b488298

Browse files
committed
Add CatalogEntry validation in controller
1. Validate export references in entry spec 2. Aggregate PermissionClaims and API resources info from referenced APIExport to entry status Signed-off-by: Vu Dinh <vudinh@outlook.com>
1 parent 7ee76a1 commit b488298

7 files changed

+366
-72
lines changed

config/crd/catalog.kcp.dev_catalogentries.yaml

+4-5
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,8 @@ spec:
5757
type: string
5858
path:
5959
description: path is an absolute reference to a workspace,
60-
e.g. root:org:ws. The workspace must be some ancestor
61-
or a child of some ancestor. If it is unset, the path
62-
of the APIBinding is used.
60+
e.g. root:org:ws. If it is unset, the path of the APIBinding
61+
is used.
6362
pattern: ^root(:[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
6463
type: string
6564
required:
@@ -125,9 +124,9 @@ spec:
125124
by the API provider(s) for this catalog entry.
126125
items:
127126
description: PermissionClaim identifies an object by GR and identity
128-
hash. It's purpose is to determine the added permisions that a
127+
hash. Its purpose is to determine the added permissions that a
129128
service provider may request and that a consumer may accept and
130-
alllow the service provider access to.
129+
allow the service provider access to.
131130
properties:
132131
group:
133132
description: group is the name of an API group. For core groups
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: apis.kcp.dev/v1alpha1
2+
kind: APIExport
3+
metadata:
4+
name: catalog.kcp.dev
5+
spec:
6+
latestResourceSchemas:
7+
- catalogentry.catalog.kcp.dev
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
apiVersion: apis.kcp.dev/v1alpha1
3+
kind: APIResourceSchema
4+
metadata:
5+
creationTimestamp: null
6+
name: catalogentries.catalog.kcp.dev
7+
spec:
8+
group: catalog.kcp.dev
9+
names:
10+
kind: CatalogEntry
11+
listKind: CatalogEntryList
12+
plural: catalogentries
13+
singular: catalogentry
14+
scope: Cluster
15+
versions:
16+
- name: v1alpha1
17+
schema:
18+
openAPIV3Schema:
19+
description: CatalogEntry is the Schema for the catalogentries API
20+
properties:
21+
apiVersion:
22+
description: 'APIVersion defines the versioned schema of this representation
23+
of an object. Servers should convert recognized schemas to the latest
24+
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
25+
type: string
26+
kind:
27+
description: 'Kind is a string value representing the REST resource this
28+
object represents. Servers may infer this from the endpoint the client
29+
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
30+
type: string
31+
metadata:
32+
type: object
33+
spec:
34+
description: CatalogEntrySpec defines the desired state of CatalogEntry
35+
properties:
36+
description:
37+
description: description is a human-readable message to describe the
38+
information regarding the capabilities and features that the API
39+
provides
40+
type: string
41+
exports:
42+
description: exports is a list of references to APIExports.
43+
items:
44+
description: ExportReference describes a reference to an APIExport.
45+
Exactly one of the fields must be set.
46+
properties:
47+
workspace:
48+
description: workspace is a reference to an APIExport in the
49+
same organization. The creator of the APIBinding needs to
50+
have access to the APIExport with the verb `bind` in order
51+
to bind to it.
52+
properties:
53+
exportName:
54+
description: Name of the APIExport that describes the API.
55+
type: string
56+
path:
57+
description: path is an absolute reference to a workspace,
58+
e.g. root:org:ws. The workspace must be some ancestor
59+
or a child of some ancestor. If it is unset, the path
60+
of the APIBinding is used.
61+
pattern: ^root(:[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
62+
type: string
63+
required:
64+
- exportName
65+
type: object
66+
type: object
67+
minItems: 1
68+
type: array
69+
required:
70+
- exports
71+
type: object
72+
status:
73+
description: CatalogEntryStatus defines the observed state of CatalogEntry
74+
properties:
75+
conditions:
76+
description: conditions is a list of conditions that apply to the
77+
CatalogEntry.
78+
items:
79+
description: Condition defines an observation of a object operational
80+
state.
81+
properties:
82+
lastTransitionTime:
83+
description: Last time the condition transitioned from one status
84+
to another. This should be when the underlying condition changed.
85+
If that is not known, then using the time when the API field
86+
changed is acceptable.
87+
format: date-time
88+
type: string
89+
message:
90+
description: A human readable message indicating details about
91+
the transition. This field may be empty.
92+
type: string
93+
reason:
94+
description: The reason for the condition's last transition
95+
in CamelCase. The specific API may choose whether or not this
96+
field is considered a guaranteed API. This field may not be
97+
empty.
98+
type: string
99+
severity:
100+
description: Severity provides an explicit classification of
101+
Reason code, so the users or machines can immediately understand
102+
the current situation and act accordingly. The Severity field
103+
MUST be set only when Status=False.
104+
type: string
105+
status:
106+
description: Status of the condition, one of True, False, Unknown.
107+
type: string
108+
type:
109+
description: Type of condition in CamelCase or in foo.example.com/CamelCase.
110+
Many .condition.type values are consistent across resources
111+
like Available, but because arbitrary conditions can be useful
112+
(see .node.status.conditions), the ability to deconflict is
113+
important.
114+
type: string
115+
required:
116+
- lastTransitionTime
117+
- status
118+
- type
119+
type: object
120+
type: array
121+
exportPermissionClaims:
122+
description: exportPermissionClaims is a list of permissions requested
123+
by the API provider(s) for this catalog entry.
124+
items:
125+
description: PermissionClaim identifies an object by GR and identity
126+
hash. It's purpose is to determine the added permisions that a
127+
service provider may request and that a consumer may accept and
128+
alllow the service provider access to.
129+
properties:
130+
group:
131+
description: group is the name of an API group. For core groups
132+
this is the empty string '""'.
133+
pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$
134+
type: string
135+
identityHash:
136+
description: This is the identity for a given APIExport that
137+
the APIResourceSchema belongs to. The hash can be found on
138+
APIExport and APIResourceSchema's status. It will be empty
139+
for core types. Note that one must look this up for a particular
140+
KCP instance.
141+
type: string
142+
resource:
143+
description: 'resource is the name of the resource. Note: it
144+
is worth noting that you can not ask for permissions for resource
145+
provided by a CRD not provided by an api export.'
146+
pattern: ^[a-z][-a-z0-9]*[a-z0-9]$
147+
type: string
148+
required:
149+
- resource
150+
type: object
151+
type: array
152+
resources:
153+
description: resources is the list of APIs that are provided by this
154+
catalog entry.
155+
items:
156+
description: GroupResource specifies a Group and a Resource, but
157+
does not force a version. This is useful for identifying concepts
158+
during lookup stages without having partially valid types
159+
properties:
160+
group:
161+
type: string
162+
resource:
163+
type: string
164+
required:
165+
- group
166+
- resource
167+
type: object
168+
type: array
169+
type: object
170+
type: object
171+
served: true
172+
storage: true
173+
subresources:
174+
status: {}

controllers/catalogentry_controller.go

+96-23
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,30 @@ package controllers
1818

1919
import (
2020
"context"
21+
"fmt"
22+
"reflect"
23+
"strings"
2124

2225
"github.com/kcp-dev/catalog/api/v1alpha1"
2326
catalogv1alpha1 "github.com/kcp-dev/catalog/api/v1alpha1"
2427
apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1"
25-
"github.com/kcp-dev/logicalcluster"
28+
conditionsapi "github.com/kcp-dev/kcp/pkg/apis/third_party/conditions/apis/conditions/v1alpha1"
29+
"github.com/kcp-dev/kcp/pkg/apis/third_party/conditions/util/conditions"
30+
"github.com/kcp-dev/kcp/pkg/logging"
31+
"github.com/kcp-dev/logicalcluster/v2"
32+
33+
corev1 "k8s.io/api/core/v1"
2634
"k8s.io/apimachinery/pkg/api/errors"
35+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2736
"k8s.io/apimachinery/pkg/runtime"
2837
"k8s.io/apimachinery/pkg/types"
38+
"k8s.io/klog/v2"
2939
ctrl "sigs.k8s.io/controller-runtime"
3040
"sigs.k8s.io/controller-runtime/pkg/client"
31-
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
41+
)
42+
43+
const (
44+
controllerName = "kcp-catalogentry"
3245
)
3346

3447
// CatalogEntryReconciler reconciles a CatalogEntry object
@@ -41,50 +54,102 @@ type CatalogEntryReconciler struct {
4154
//+kubebuilder:rbac:groups=catalog.kcp.dev,resources=catalogentries/status,verbs=get;update;patch
4255
//+kubebuilder:rbac:groups=catalog.kcp.dev,resources=catalogentries/finalizers,verbs=update
4356

44-
// Reconcile is part of the main kubernetes reconciliation loop which aims to
45-
// move the current state of the cluster closer to the desired state.
46-
// TODO(user): Modify the Reconcile function to compare the state specified by
47-
// the CatalogEntry object against the actual cluster state, and then
48-
// perform operations to make the cluster state reflect the state specified by
49-
// the user.
50-
//
51-
// For more details, check Reconcile and its Result here:
52-
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile
57+
// Reconcile validates exports in CatalogEntry spec and add a condition to status
58+
// to reflect the outcome of the validation.
59+
// It also aggregates all permissionClaims and api resources from referenced APIExport
60+
// to CatalogEntry status
5361
func (r *CatalogEntryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
54-
log := ctrllog.FromContext(ctx)
55-
62+
logger := logging.WithReconciler(klog.Background(), controllerName)
63+
logger = logger.WithValues("clusterName", req.ClusterName)
5664
ctx = logicalcluster.WithCluster(ctx, logicalcluster.New(req.ClusterName))
65+
5766
// Fetch the catalog entry from the request
5867
catalogEntry := &v1alpha1.CatalogEntry{}
5968
err := r.Get(ctx, req.NamespacedName, catalogEntry)
6069
if err != nil {
6170
if errors.IsNotFound(err) {
6271
// Request object not found, could have been deleted after reconcile request.
6372
// Owned objects are automatically garbage collected.
64-
log.Info("Catalog Entry not found. Ignoring since object must be deleted")
73+
logger.Info("CatalogEntry not found")
6574
return ctrl.Result{}, nil
6675
}
6776
// Error reading the object - requeue the request.
68-
log.Error(err, "Failed to get resource")
77+
logger.Error(err, "failed to get resource")
6978
return ctrl.Result{}, err
7079
}
7180

72-
apiExportNameReferences := catalogEntry.Spec.References
73-
74-
for _, exportRef := range apiExportNameReferences {
81+
oldEntry := catalogEntry.DeepCopy()
82+
resources := []metav1.GroupResource{}
83+
exportPermissionClaims := []apisv1alpha1.PermissionClaim{}
84+
invalidExports := []string{}
85+
for _, exportRef := range catalogEntry.Spec.Exports {
7586
// TODO: verify if path contains the entire heirarchy or just the clusterName.
7687
// If it contains the heirarchy then extract the clusterName
7788
path := exportRef.Workspace.Path
7889
name := exportRef.Workspace.ExportName
79-
clusterApiExport := apisv1alpha1.APIExport{}
80-
err := r.Get(logicalcluster.WithCluster(ctx, logicalcluster.New(path)), types.NamespacedName{Name: name, Namespace: req.Namespace}, &clusterApiExport)
90+
logger = logger.WithValues(
91+
"path", path,
92+
"exportName", name,
93+
)
94+
logger.V(2).Info("reconciling CatalogEntry")
95+
export := apisv1alpha1.APIExport{}
96+
err := r.Get(logicalcluster.WithCluster(ctx, logicalcluster.New(path)), types.NamespacedName{Name: name, Namespace: req.Namespace}, &export)
8197
if err != nil {
98+
invalidExports = append(invalidExports, fmt.Sprintf("%s/%s", path, name))
8299
if errors.IsNotFound(err) {
83-
log.Error(err, "APIExport referenced in catalog entry does not exist")
84-
return ctrl.Result{}, err
100+
logger.Error(err, "APIExport referenced in catalog entry does not exist")
101+
continue
85102
}
86103
// Error reading the object - requeue the request.
87-
log.Error(err, "Failed to get resource")
104+
logger.Error(err, "failed to get resource")
105+
continue
106+
}
107+
108+
// Extract permission and API resource info
109+
for _, claim := range export.Spec.PermissionClaims {
110+
exportPermissionClaims = append(exportPermissionClaims, claim)
111+
}
112+
catalogEntry.Status.ExportPermissionClaims = exportPermissionClaims
113+
114+
for _, schemaName := range export.Spec.LatestResourceSchemas {
115+
_, resource, group, ok := split3(schemaName, ".")
116+
if !ok {
117+
continue
118+
}
119+
gr := metav1.GroupResource{
120+
Group: group,
121+
Resource: resource,
122+
}
123+
resources = append(resources, gr)
124+
}
125+
catalogEntry.Status.Resources = resources
126+
}
127+
128+
if len(invalidExports) == 0 {
129+
cond := conditionsapi.Condition{
130+
Type: catalogv1alpha1.APIExportValidType,
131+
Status: corev1.ConditionTrue,
132+
Severity: conditionsapi.ConditionSeverityNone,
133+
LastTransitionTime: metav1.Now(),
134+
}
135+
conditions.Set(catalogEntry, &cond)
136+
} else {
137+
message := fmt.Sprintf("invalid export(s): %s", strings.Join(invalidExports, " ,"))
138+
invalidCond := conditionsapi.Condition{
139+
Type: catalogv1alpha1.APIExportValidType,
140+
Status: corev1.ConditionFalse,
141+
Severity: conditionsapi.ConditionSeverityError,
142+
LastTransitionTime: metav1.Now(),
143+
Message: message,
144+
}
145+
conditions.Set(catalogEntry, &invalidCond)
146+
}
147+
148+
// Update the catalog entry if status is changed
149+
if !reflect.DeepEqual(catalogEntry.Status, oldEntry.Status) {
150+
err = r.Client.Status().Update(context.TODO(), catalogEntry)
151+
if err != nil {
152+
logger.Error(err, "failed to update CatalogEntry")
88153
return ctrl.Result{}, err
89154
}
90155
}
@@ -98,3 +163,11 @@ func (r *CatalogEntryReconciler) SetupWithManager(mgr ctrl.Manager) error {
98163
For(&catalogv1alpha1.CatalogEntry{}).
99164
Complete(r)
100165
}
166+
167+
func split3(s string, sep string) (string, string, string, bool) {
168+
comps := strings.SplitN(s, sep, 3)
169+
if len(comps) != 3 {
170+
return "", "", "", false
171+
}
172+
return comps[0], comps[1], comps[2], true
173+
}

0 commit comments

Comments
 (0)