Skip to content

Commit

Permalink
Remove root:compute deps in controller
Browse files Browse the repository at this point in the history
1. create imported-api APIExport during sync cmd if resources flag is set
2. controller updated imported-api APIExport with updated APIResourceSchema
3. remove auto-generated apibinding
4. add a validation on sync command to avoid setting imported-apis
in --api-export flag

Signed-off-by: Jian Qiu <jqiu@redhat.com>
  • Loading branch information
qiujian16 committed Mar 29, 2023
1 parent b5e3a0e commit 22e9551
Show file tree
Hide file tree
Showing 25 changed files with 223 additions and 428 deletions.
4 changes: 0 additions & 4 deletions config/crds/workload.kcp.io_synctargets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,10 @@ spec:
format: date-time
type: string
supportedAPIExports:
default:
- export: kubernetes
description: SupportedAPIExports defines a set of APIExports supposed
to be supported by this SyncTarget. The SyncTarget will be selected
to deploy the workload only when the resource schema on the SyncTarget
is compatible with the resource schema included in the exports.
If it is not set, the kubernetes export in the same workspace will
be used by default.
items:
description: APIExportReference provides the fields necessary to
resolve an APIExport.
Expand Down
2 changes: 1 addition & 1 deletion config/root-phase0/apiexport-workload.kcp.io.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ metadata:
name: workload.kcp.io
spec:
latestResourceSchemas:
- v230109-773b219c.synctargets.workload.kcp.io
- v230329-c41ff68c.synctargets.workload.kcp.io
status: {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1
kind: APIResourceSchema
metadata:
creationTimestamp: null
name: v230109-773b219c.synctargets.workload.kcp.io
name: v230329-c41ff68c.synctargets.workload.kcp.io
spec:
group: workload.kcp.io
names:
Expand Down Expand Up @@ -75,14 +75,10 @@ spec:
format: date-time
type: string
supportedAPIExports:
default:
- export: kubernetes
description: SupportedAPIExports defines a set of APIExports supposed
to be supported by this SyncTarget. The SyncTarget will be selected
to deploy the workload only when the resource schema on the SyncTarget
is compatible with the resource schema included in the exports. If
it is not set, the kubernetes export in the same workspace will be
used by default.
is compatible with the resource schema included in the exports.
items:
description: APIExportReference provides the fields necessary to resolve
an APIExport.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,17 @@ kubectl kcp workload sync <mycluster> --syncer-image <image name> --resources ro
```

And apply the generated manifests to the physical cluster. The syncer will then import the API schema of `foo.bar`
to the workspace of the SyncTarget, followed by an auto generated kubernetes APIExport and APIBinding in the same workspace.
You can then create `foo.bar` in this workspace, or create an APIBinding in another workspace to bind this APIExport.
to the workspace of the SyncTarget, followed by an auto generated kubernetes APIExport in the same workspace.
The auto generated APIExport name is `imported-apis`, and you can then create an APIBinding to bind this APIExport.

To synchronize resource from another existing APIExport in the KCP server, run:

```sh
kubectl kcp workload sync <mycluster> --syncer-image <image name> --apiexports another-workspace:another-apiexport -o syncer.yaml
```

Syncer will start synchronizing the resources in this `APIExport` as long as the `SyncTarget` has compatible API schemas.
Syncer will start synchronizing the resources in this `APIExport` as long as the `SyncTarget` has compatible API schemas. The auto generated
APIExport `imported-apis` is a reserved APIExport name and should not be set in the `--apiexports`

To see if a certain resource is synchronized by the syncer, you can check the `state` of the `syncedResources` in `SyncTarget` status:

Expand All @@ -121,8 +122,13 @@ After the `SyncTarget` is ready, switch to any workspace containing some workloa
kubectl kcp bind compute <workspace of synctarget>
```

This command will create a `Placement` in the workspace. By default, it will also create `APIBinding`s for global kubernetes `APIExport` and
kubernetes `APIExport` in workspace of `SyncTarget`, if any of these `APIExport`s are supported by the `SyncTarget`.
This command will create a `Placement` in the workspace. By default, it will also create `APIBinding`s for global kubernetes `APIExport`.

You can also bind to the auto generated `imported-apis` APIExport by running

```sh
kubectl kcp bind compute <workspace of synctarget> --apiexports <workspace of synctarget>:imported-apis
```

Alternatively, if you would like to bind other `APIExport`s which are supported by the `SyncerTarget`, run:

Expand Down
90 changes: 80 additions & 10 deletions pkg/cliplugins/workload/plugin/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation"
Expand All @@ -56,7 +57,9 @@ import (
"github.com/kcp-dev/kcp/pkg/cliplugins/base"
"github.com/kcp-dev/kcp/pkg/cliplugins/helpers"
kcpfeatures "github.com/kcp-dev/kcp/pkg/features"
"github.com/kcp-dev/kcp/pkg/reconciler/workload/apiexport"
apiresourcev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apiresource/v1alpha1"
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
workloadv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/workload/v1alpha1"
kcpclient "github.com/kcp-dev/kcp/sdk/client/clientset/versioned"
Expand Down Expand Up @@ -196,6 +199,13 @@ func (o *SyncOptions) Validate() error {
}
}

for _, apiExport := range o.APIExports {
_, name := logicalcluster.NewPath(apiExport).Split()
if name == workloadv1alpha1.ImportedAPISExportName {
errs = append(errs, fmt.Errorf("%s is a reserved APIExport name and should not be set", workloadv1alpha1.ImportedAPISExportName))
}
}

return utilerrors.NewAggregate(errs)
}

Expand Down Expand Up @@ -320,11 +330,45 @@ func (o *SyncOptions) applySyncTarget(ctx context.Context, kcpClient kcpclient.I
})
}

// if ResourcesToSync is not empty, add export in synctarget workspace.
if len(o.ResourcesToSync) > 0 && !sets.NewString(o.APIExports...).Has("kubernetes") {
supportedAPIExports = append(supportedAPIExports, tenancyv1alpha1.APIExportReference{
Export: "kubernetes",
})
// create local apiexport if resources flag is set
if len(o.ResourcesToSync) > 0 {
apiExport, err := kcpClient.ApisV1alpha1().APIExports().Get(ctx, workloadv1alpha1.ImportedAPISExportName, metav1.GetOptions{})
switch {
case apierrors.IsNotFound(err):
fmt.Fprintf(o.ErrOut, "Creating APIExport %q\n", workloadv1alpha1.ImportedAPISExportName)
apiExport = &apisv1alpha1.APIExport{
ObjectMeta: metav1.ObjectMeta{
Name: workloadv1alpha1.ImportedAPISExportName,
Annotations: map[string]string{
workloadv1alpha1.ComputeAPIExportAnnotationKey: "true",
},
},
Spec: apisv1alpha1.APIExportSpec{
LatestResourceSchemas: []string{},
},
}
apiExport, _ = mergeLatestResourceSchema(apiExport, o.ResourcesToSync)
_, err = kcpClient.ApisV1alpha1().APIExports().Create(ctx, apiExport, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err
}
case err != nil:
return nil, err
default:
if apiExport, modified := mergeLatestResourceSchema(apiExport, o.ResourcesToSync); modified {
_, err = kcpClient.ApisV1alpha1().APIExports().Update(ctx, apiExport, metav1.UpdateOptions{})
if err != nil {
return nil, err
}
}
}

// if ResourcesToSync is not empty, add export in synctarget workspace.
if !sets.NewString(o.APIExports...).Has(workloadv1alpha1.ImportedAPISExportName) {
supportedAPIExports = append(supportedAPIExports, tenancyv1alpha1.APIExportReference{
Export: workloadv1alpha1.ImportedAPISExportName,
})
}
}

syncTarget, err := kcpClient.WorkloadV1alpha1().SyncTargets().Get(ctx, syncTargetName, metav1.GetOptions{})
Expand Down Expand Up @@ -418,13 +462,13 @@ func (o *SyncOptions) getResourcesForPermission(ctx context.Context, config *res
return false, nil //nolint:nilerr
}

// skip if there is only the local kubernetes APIExport in the synctarget workspace, since we may not get syncedResources yet.
clusterName := logicalcluster.From(syncTarget)
if len(syncTarget.Spec.SupportedAPIExports) == 0 {
return true, nil
}

// skip if there is only the local imported-apis APIExport in the synctarget workspace, since we may not get syncedResources yet.
if len(syncTarget.Spec.SupportedAPIExports) == 1 &&
syncTarget.Spec.SupportedAPIExports[0].Export == "kubernetes" &&
(len(syncTarget.Spec.SupportedAPIExports[0].Path) == 0 ||
syncTarget.Spec.SupportedAPIExports[0].Path == clusterName.String()) {
syncTarget.Spec.SupportedAPIExports[0].Export == workloadv1alpha1.ImportedAPISExportName {
return true, nil
}

Expand Down Expand Up @@ -674,6 +718,32 @@ func (o *SyncOptions) enableSyncerForWorkspace(ctx context.Context, config *rest
return string(saTokenBytes), syncerID, syncTarget, nil
}

func mergeLatestResourceSchema(apiExport *apisv1alpha1.APIExport, resourceToSync []string) (*apisv1alpha1.APIExport, bool) {
desiredResourceGroup := sets.NewString()
var modified bool
for _, schema := range apiExport.Spec.LatestResourceSchemas {
gr, valid := apiexport.ParseAPIResourceSchemaName(schema)
if !valid {
continue
}
desiredResourceGroup.Insert(gr.String())
}
for _, resource := range resourceToSync {
gr := schema.ParseGroupResource(resource)
if len(gr.Group) == 0 {
gr.Group = "core"
}
if !desiredResourceGroup.Has(gr.String()) {
// the rev-0 here is a placeholder and will be replaced by rv of negotiated APIResourceSchema finally.
schemaName := fmt.Sprintf("rev-0.%s", gr.String())
apiExport.Spec.LatestResourceSchemas = append(apiExport.Spec.LatestResourceSchemas, schemaName)
modified = true
}
}

return apiExport, modified
}

// mergeOwnerReference: merge a slice of ownerReference with a given ownerReferences.
func mergeOwnerReference(ownerReferences, newOwnerReferences []metav1.OwnerReference) []metav1.OwnerReference {
var merged []metav1.OwnerReference
Expand Down
2 changes: 1 addition & 1 deletion pkg/openapi/zz_generated.openapi.go

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

12 changes: 12 additions & 0 deletions pkg/reconciler/workload/apiexport/apiresourceschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ limitations under the License.
package apiexport

import (
"strings"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"

apiresourcev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apiresource/v1alpha1"
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
Expand Down Expand Up @@ -72,3 +75,12 @@ func toAPIResourceSchema(r *apiresourcev1alpha1.NegotiatedAPIResource, name stri

return schema
}

// ParseAPIResourceSchemaName parses name of APIResourceSchema to a gr and schema if it is valid.
func ParseAPIResourceSchemaName(name string) (schema.GroupResource, bool) {
comps := strings.SplitN(name, ".", 3)
if len(comps) < 3 {
return schema.GroupResource{}, false
}
return schema.GroupResource{Resource: comps[1], Group: comps[2]}, true
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ import (

const (
ControllerName = "kcp-workload-apiexport"

// TemporaryComputeServiceExportName is a temporary singleton name of compute service exports.
TemporaryComputeServiceExportName = "kubernetes"
)

// NewController returns a new controller instance.
Expand Down Expand Up @@ -82,7 +79,7 @@ func NewController(
FilterFunc: func(obj interface{}) bool {
switch t := obj.(type) {
case *apisv1alpha1.APIExport:
return t.Name == TemporaryComputeServiceExportName
return t.Name == workloadv1alpha1.ImportedAPISExportName
}
return false
},
Expand Down Expand Up @@ -133,11 +130,11 @@ func (c *controller) enqueueNegotiatedAPIResource(obj interface{}) {
return
}

export, err := c.apiExportsLister.Cluster(logicalcluster.From(resource)).Get(TemporaryComputeServiceExportName)
export, err := c.apiExportsLister.Cluster(logicalcluster.From(resource)).Get(workloadv1alpha1.ImportedAPISExportName)
if errors.IsNotFound(err) {
return // it's gone
} else if err != nil {
runtime.HandleError(fmt.Errorf("failed to get APIExport %s|%s: %w", logicalcluster.From(resource), TemporaryComputeServiceExportName, err))
runtime.HandleError(fmt.Errorf("failed to get APIExport %s|%s: %w", logicalcluster.From(resource), workloadv1alpha1.ImportedAPISExportName, err))
return
}

Expand All @@ -158,11 +155,11 @@ func (c *controller) enqueueSyncTarget(obj interface{}) {
return
}

export, err := c.apiExportsLister.Cluster(logicalcluster.From(resource)).Get(TemporaryComputeServiceExportName)
export, err := c.apiExportsLister.Cluster(logicalcluster.From(resource)).Get(workloadv1alpha1.ImportedAPISExportName)
if errors.IsNotFound(err) {
return // it's gone
} else if err != nil {
runtime.HandleError(fmt.Errorf("failed to get APIExport %s|%s: %w", logicalcluster.From(resource), TemporaryComputeServiceExportName, err))
runtime.HandleError(fmt.Errorf("failed to get APIExport %s|%s: %w", logicalcluster.From(resource), workloadv1alpha1.ImportedAPISExportName, err))
return
}

Expand Down
Loading

0 comments on commit 22e9551

Please sign in to comment.