diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index 9d9dc17bd..225618075 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -19,17 +19,39 @@ jobs: with: go-version: v1.24.0 check-latest: true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package-lock.json + # We need this to remove local tags that are not semver so goreleaser doesn't get confused. - name: Delete non-semver tags run: 'git tag -d $(git tag -l | grep -v "^v")' + + # Set up Docker Buildx for multi-platform builds + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # If you notice signing errors, you may need to update the cosign version. - uses: sigstore/cosign-installer@v3.7.0 + - name: Install ko run: go install github.com/google/ko@latest - name: Set LDFLAGS run: echo LDFLAGS="$(make ldflags)" | tee -a >> $GITHUB_ENV + # Login to GitHub Container Registry (used by both ko and Docker) + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # Build ko from HEAD, build and push an image tagged with the commit SHA, # then keylessly sign it with cosign. - name: Publish and sign konnector image @@ -37,7 +59,6 @@ jobs: KO_DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/konnector COSIGN_EXPERIMENTAL: 'true' run: | - echo "${{ github.token }}" | ko login ghcr.io --username "${{ github.actor }}" --password-stdin img=$(ko build --bare --platform=all -t latest -t ${{ github.sha }} -t ${{github.ref_name}} ./cmd/konnector) echo "built ${img}" cosign sign ${img} \ @@ -47,14 +68,39 @@ jobs: -a run_id=${{ github.run_id }} \ -a run_attempt=${{ github.run_attempt }} - - name: Publish and sign example-backend image + # Build and push backend image using Dockerfile (includes frontend) + # Note: Backend image uses Dockerfile to include both Go backend + Vue.js frontend + # while konnector continues to use ko for Go-only builds + - name: Build and push backend image + uses: docker/build-push-action@v5 + id: build + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/backend:latest + ghcr.io/${{ github.repository_owner }}/backend:${{ github.sha }} + ghcr.io/${{ github.repository_owner }}/backend:${{ github.ref_name }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + LDFLAGS=${{ env.LDFLAGS }} + labels: | + org.opencontainers.image.title=Kube Bind Backend + org.opencontainers.image.description=Kube Bind backend with integrated Vue.js frontend + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ github.ref_name }} + + # Sign the backend image + - name: Sign backend image env: - KO_DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/example-backend COSIGN_EXPERIMENTAL: 'true' run: | - echo "${{ github.token }}" | ko login ghcr.io --username "${{ github.actor }}" --password-stdin - img=$(ko build --bare --platform=all -t latest -t ${{ github.sha }} -t ${{github.ref_name}} ./cmd/example-backend) - echo "built ${img}" + img="ghcr.io/${{ github.repository_owner }}/backend@${{ steps.build.outputs.digest }}" + echo "signing ${img}" cosign sign ${img} \ --yes \ -a sha=${{ github.sha }} \ diff --git a/.gitignore b/.gitignore index d419328f1..3cade4700 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,10 @@ coverage.* /dex /bin docs/generators/cli-doc/cli-doc -dex/ \ No newline at end of file +dex/ + +# Frontend dependencies and build +web/node_modules/ +web/dist/ +web/.vite/ +web/*.tsbuildinfo \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7448e03ed..14b9429ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,61 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.24.0 AS builder +FROM golang:1.24.0 AS go-build-env +WORKDIR /app + +# Accept build arguments +ARG LDFLAGS + +RUN apt-get update && apt-get install -y make jq + +# <- COPY go.mod and go.sum files to the workspace +COPY go.mod . +COPY go.sum . + +# COPY the source code as the last step +COPY . . + +# Build with custom LDFLAGS if provided, otherwise use make build +RUN if [ -n "$LDFLAGS" ]; then \ + echo "Building with LDFLAGS: $LDFLAGS"; \ + go build -ldflags="$LDFLAGS" -o bin/backend ./cmd/backend; \ + else \ + make build; \ + fi + +# Use node:lts-alpine for better compatibility and smaller size +FROM node:20.18.0-alpine3.20 AS ui-build-env +WORKDIR /app + +# Install build dependencies needed for native modules +RUN apk add --no-cache python3 make g++ + +# Copy package files +COPY ./web/package*.json ./ +COPY ./web/.npmrc ./ + +RUN npm install + +# Install dependencies with specific flags to handle optional deps and architecture issues +RUN npm ci --prefer-offline --no-audit --no-fund --no-optional + +# Copy the Vue app files +COPY ./web . + +# Set environment to avoid native dependency issues +ENV NODE_ENV=production +ENV VITE_BUILD_TARGET=docker + +# Building UI with Docker-specific config +RUN npm run build + +FROM alpine:3.22.1 +RUN apk --update add ca-certificates + +COPY --from=go-build-env /app/bin/backend /bin +COPY --from=ui-build-env /app/dist /www + + + +ENTRYPOINT ["/bin/backend"] diff --git a/NOTES b/NOTES new file mode 100644 index 000000000..bc814aeb9 --- /dev/null +++ b/NOTES @@ -0,0 +1,27 @@ +How does backend operates: + +1. `--consumer-scope=cluster` - backend will create a namespace per identity in the consumer cluster. +2. `--cluster-scoped-isolation` - {Prefixed,Namespaced,None} + + +Scenario: +1. Consumer operates on namespaced CRDs - cowboys and cluster-scoped CRDs - sheriffs +2. Backend is set to `consumer-scope=namespaced` and `cluster-scoped-isolation=prefixed` + +When binding namespaced CRD - backend will create namespace `kube-bind--` in the provider cluster. Objects will be created in that namespace. Same with claims. + +When binding cluster-scoped CRD - NOT SUPPORTED. TODO: Add condition on konnector side to tell this is the case. + +Scenario: +1. Consumer operates on namespaced CRDs - cowboys and cluster-scoped CRDs - sheriffs +2. Backend is set to `consumer-scope=cluster` and `cluster-scoped-isolation=prefixed` + +When binding namespaced CRD - backend will create namespace `kube-bind--` in the provider cluster. Objects will be created in that namespace. Same with claims. + +When binding cluster-scoped CRD. Oject will be renamed to `kube-bind--`. + +TODO: There is some issue with permissiong setting up. It required fursther investigation as initial sync fails with permission error. + +To make sure claims has right access: +1. If consumer is namespaced - create Role and RoleBinding in the consumer namespace. Watch APIServiceNamespaces for this. +2. If consumer is cluster-scoped - create ClusterRole and ClusterRoleBinding based on APISericeExport spec. \ No newline at end of file diff --git a/README.md b/README.md index 0f1af7403..b35898441 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,36 @@ All the actions shown between the clusters are done by the konnector, except: th To get familiar with setting up the environment, please check out docs at [kube-bind.io](https://docs.kube-bind.io/main/setup). +### Web Frontend + +The project includes a modern Vue.js + TypeScript web frontend that's fully integrated with the Go backend. The frontend provides: + +- **SSO Authentication**: OAuth2/OIDC integration with the backend +- **Multi-cluster Support**: Browse resources across different clusters +- **Resource Management**: Browse and bind available Kubernetes resources +- **Modern UI**: Responsive design built with Vue.js 3 and TypeScript + +#### Quick Start (Integrated) +```bash +# Build frontend and run integrated server +./scripts/run-frontend.sh +go run ./cmd/backend --listen-port=8080 + +# Visit http://localhost:8080 for the complete application +``` + +#### Development Mode +```bash +# Option 1: Integrated (recommended) +cd web && npm run build && cd .. && go run ./cmd/backend + +# Option 2: Separate servers with hot reload +go run ./cmd/backend & +cd web && npm run dev +``` + +See [web/README.md](./web/README.md) for detailed frontend documentation. + ## API Changes in coming v0.5.0 release Version v0.5.0 includes significant architectural improvements to the API structure: diff --git a/apiserviceexport.yaml b/apiserviceexport.yaml new file mode 100644 index 000000000..72a3bd448 --- /dev/null +++ b/apiserviceexport.yaml @@ -0,0 +1,11 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportRequest +metadata: + name: sheriffs.wildwest.dev +spec: + resources: + - group: wildwest.dev + resource: sheriffs + versions: + - v1alpha1 +status: {} diff --git a/backend/controllers/clusterbinding/clusterbinding_controller.go b/backend/controllers/clusterbinding/clusterbinding_controller.go index c7e380b6e..029ff22de 100644 --- a/backend/controllers/clusterbinding/clusterbinding_controller.go +++ b/backend/controllers/clusterbinding/clusterbinding_controller.go @@ -77,11 +77,11 @@ func NewClusterBindingReconciler( } return exports, nil }, - getAPIResourceSchema: func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - result := &kubebindv1alpha2.APIResourceSchema{} - err := cache.Get(ctx, types.NamespacedName{Name: name}, result) + getBoundSchema: func(ctx context.Context, cache cache.Cache, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) { + result := &kubebindv1alpha2.BoundSchema{} + err := cache.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, result) if err != nil { - return nil, fmt.Errorf("failed to get APIResourceSchema %q: %w", name, err) + return nil, fmt.Errorf("failed to get BoundSchema %q: %w", name, err) } return result, nil }, diff --git a/backend/controllers/clusterbinding/clusterbinding_reconcile.go b/backend/controllers/clusterbinding/clusterbinding_reconcile.go index ebf235d53..e934484bf 100644 --- a/backend/controllers/clusterbinding/clusterbinding_reconcile.go +++ b/backend/controllers/clusterbinding/clusterbinding_reconcile.go @@ -40,11 +40,11 @@ import ( type reconciler struct { scope kubebindv1alpha2.InformerScope - listServiceExports func(ctx context.Context, cache cache.Cache, ns string) ([]*kubebindv1alpha2.APIServiceExport, error) - getAPIResourceSchema func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.APIResourceSchema, error) - getClusterRole func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRole, error) - createClusterRole func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error - updateClusterRole func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error + listServiceExports func(ctx context.Context, cache cache.Cache, ns string) ([]*kubebindv1alpha2.APIServiceExport, error) + getBoundSchema func(ctx context.Context, cache cache.Cache, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) + getClusterRole func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRole, error) + createClusterRole func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error + updateClusterRole func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error getClusterRoleBinding func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRoleBinding, error) createClusterRoleBinding func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRoleBinding) error @@ -151,25 +151,26 @@ func (r *reconciler) ensureRBACClusterRole(ctx context.Context, client client.Cl } for _, export := range exports { for _, res := range export.Spec.Resources { - schema, err := r.getAPIResourceSchema(ctx, cache, res.Name) + schema, err := r.getBoundSchema(ctx, cache, clusterBinding.Namespace, res.ResourceGroupName()) if err != nil { - return fmt.Errorf("failed to get APIResourceSchema %w", err) + return fmt.Errorf("failed to get BoundSchema %w", err) } expected.Rules = append(expected.Rules, rbacv1.PolicyRule{ - APIGroups: []string{schema.Spec.APIResourceSchemaCRDSpec.Group}, - Resources: []string{schema.Spec.APIResourceSchemaCRDSpec.Names.Plural}, + APIGroups: []string{schema.Spec.Group}, + Resources: []string{schema.Spec.Names.Plural}, Verbs: []string{"get", "list", "watch", "update", "patch", "delete", "create"}, }, - rbacv1.PolicyRule{ - APIGroups: []string{kubebindv1alpha2.GroupName}, - Resources: []string{"apiresourceschemas"}, - Verbs: []string{"get", "list", "watch"}, - }, ) } } + expected.Rules = append(expected.Rules, + rbacv1.PolicyRule{ + APIGroups: []string{kubebindv1alpha2.GroupName}, + Resources: []string{"boundschemas"}, + Verbs: []string{"get", "list", "watch", "update", "patch"}, + }) if role == nil { if err := r.createClusterRole(ctx, client, expected); err != nil { diff --git a/backend/controllers/serviceexport/serviceexport_controller.go b/backend/controllers/serviceexport/serviceexport_controller.go index f3a455267..af887c4ef 100644 --- a/backend/controllers/serviceexport/serviceexport_controller.go +++ b/backend/controllers/serviceexport/serviceexport_controller.go @@ -58,17 +58,17 @@ func NewAPIServiceExportReconciler( mgr mcmanager.Manager, opts controller.TypedOptions[mcreconcile.Request], ) (*APIServiceExportReconciler, error) { - if err := mgr.GetFieldIndexer().IndexField(ctx, &kubebindv1alpha2.APIServiceExport{}, indexers.ServiceExportByAPIResourceSchema, - indexers.IndexServiceExportByAPIResourceSchema); err != nil { - return nil, fmt.Errorf("failed to setup ServiceExportByAPIResourceSchema indexer: %w", err) + if err := mgr.GetFieldIndexer().IndexField(ctx, &kubebindv1alpha2.APIServiceExport{}, indexers.ServiceExportByBoundSchema, + indexers.IndexServiceExportByBoundSchema); err != nil { + return nil, fmt.Errorf("failed to setup ServiceExportByBoundSchema indexer: %w", err) } r := &APIServiceExportReconciler{ manager: mgr, opts: opts, reconciler: reconciler{ - getAPIResourceSchema: func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - var schema kubebindv1alpha2.APIResourceSchema + getBoundSchema: func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.BoundSchema, error) { + var schema kubebindv1alpha2.BoundSchema key := types.NamespacedName{Name: name} if err := cache.Get(ctx, key, &schema); err != nil { return nil, err @@ -141,14 +141,14 @@ func (r *APIServiceExportReconciler) Reconcile(ctx context.Context, req mcreconc } // getAPIResourceSchemaMapper returns a mapper function that uses the manager to find related APIServiceExports. -func getAPIResourceSchemaMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] { +func getBoundSchemaMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] { return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []mcreconcile.Request { - apiResourceSchema := obj.(*kubebindv1alpha2.APIResourceSchema) - apiResourceSchemaKey := apiResourceSchema.Name + boundSchema := obj.(*kubebindv1alpha2.BoundSchema) + boundSchemaKey := boundSchema.Name c := cl.GetClient() var exports kubebindv1alpha2.APIServiceExportList - if err := c.List(ctx, &exports, client.MatchingFields{indexers.ServiceExportByAPIResourceSchema: apiResourceSchemaKey}); err != nil { + if err := c.List(ctx, &exports, client.MatchingFields{indexers.ServiceExportByBoundSchema: boundSchemaKey}); err != nil { return []mcreconcile.Request{} } @@ -171,8 +171,8 @@ func (r *APIServiceExportReconciler) SetupWithManager(mgr mcmanager.Manager) err return mcbuilder.ControllerManagedBy(mgr). For(&kubebindv1alpha2.APIServiceExport{}). Watches( - &kubebindv1alpha2.APIResourceSchema{}, - getAPIResourceSchemaMapper, + &kubebindv1alpha2.BoundSchema{}, + getBoundSchemaMapper, ). WithOptions(r.opts). Named(controllerName). diff --git a/backend/controllers/serviceexport/serviceexport_reconcile.go b/backend/controllers/serviceexport/serviceexport_reconcile.go index 061fbb967..9b6a53cc2 100644 --- a/backend/controllers/serviceexport/serviceexport_reconcile.go +++ b/backend/controllers/serviceexport/serviceexport_reconcile.go @@ -18,10 +18,6 @@ package serviceexport import ( "context" - "crypto/sha256" - "encoding/hex" - "slices" - "sort" "k8s.io/apimachinery/pkg/api/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -30,13 +26,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" - kubebindhelpers "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" + "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions" ) type reconciler struct { - getAPIResourceSchema func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.APIResourceSchema, error) - deleteServiceExport func(ctx context.Context, client client.Client, namespace, name string) error + getBoundSchema func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.BoundSchema, error) + deleteServiceExport func(ctx context.Context, client client.Client, namespace, name string) error } func (r *reconciler) reconcile(ctx context.Context, cache cache.Cache, export *kubebindv1alpha2.APIServiceExport) error { @@ -56,14 +52,11 @@ func (r *reconciler) reconcile(ctx context.Context, cache cache.Cache, export *k func (r *reconciler) ensureSchema(ctx context.Context, cache cache.Cache, export *kubebindv1alpha2.APIServiceExport) (specChanged bool, err error) { logger := klog.FromContext(ctx) - leafHashes := make([]string, 0, len(export.Spec.Resources)) - for _, resourceRef := range export.Spec.Resources { - if resourceRef.Type != "APIResourceSchema" { - logger.V(1).Info("Skipping unsupported resource type", "type", resourceRef.Type) - continue - } + schemas := make([]*kubebindv1alpha2.BoundSchema, 0, len(export.Spec.Resources)) - schema, err := r.getAPIResourceSchema(ctx, cache, resourceRef.Name) + for _, res := range export.Spec.Resources { + name := res.Resource + "." + res.Group + schema, err := r.getBoundSchema(ctx, cache, name) if err != nil { if errors.IsNotFound(err) { continue @@ -71,19 +64,18 @@ func (r *reconciler) ensureSchema(ctx context.Context, cache cache.Cache, export return false, err } - hash := kubebindhelpers.APIResourceSchemaCRDSpecHash(&schema.Spec.APIResourceSchemaCRDSpec) - leafHashes = append(leafHashes, hash) + schemas = append(schemas, schema) } - hashOfHashes := hashOfHashes(leafHashes) + hash := helpers.BoundSchemasSpecHash(schemas) - if export.Annotations[kubebindv1alpha2.SourceSpecHashAnnotationKey] != hashOfHashes { + if export.Annotations[kubebindv1alpha2.SourceSpecHashAnnotationKey] != hash { // both exist, update APIServiceExport - logger.V(1).Info("Updating APIServiceExport. Hash mismatch", "hash", hashOfHashes, "expected", export.Annotations[kubebindv1alpha2.SourceSpecHashAnnotationKey]) + logger.V(1).Info("Updating APIServiceExport. Hash mismatch", "hash", hash, "expected", export.Annotations[kubebindv1alpha2.SourceSpecHashAnnotationKey]) if export.Annotations == nil { export.Annotations = map[string]string{} } - export.Annotations[kubebindv1alpha2.SourceSpecHashAnnotationKey] = hashOfHashes + export.Annotations[kubebindv1alpha2.SourceSpecHashAnnotationKey] = hash return true, nil } @@ -91,14 +83,3 @@ func (r *reconciler) ensureSchema(ctx context.Context, cache cache.Cache, export return false, nil } - -func hashOfHashes(hashes []string) string { - hexHashes := slices.Clone(hashes) - sort.Strings(hexHashes) - - rootHasher := sha256.New() - for _, h := range hexHashes { - rootHasher.Write([]byte(h)) - } - return hex.EncodeToString(rootHasher.Sum(nil)) -} diff --git a/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go b/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go index f21a71bc6..7f140129c 100644 --- a/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go +++ b/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go @@ -61,6 +61,7 @@ func NewAPIServiceExportRequestReconciler( opts controller.TypedOptions[mcreconcile.Request], scope kubebindv1alpha2.InformerScope, isolation kubebindv1alpha2.Isolation, + schemaSource string, ) (*APIServiceExportRequestReconciler, error) { // Set up field indexers for APIServiceExportRequests if err := mgr.GetFieldIndexer().IndexField(ctx, &kubebindv1alpha2.APIServiceExportRequest{}, indexers.ServiceExportRequestByServiceExport, @@ -81,9 +82,10 @@ func NewAPIServiceExportRequestReconciler( reconciler: reconciler{ informerScope: scope, clusterScopedIsolation: isolation, - getAPIResourceSchema: func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - var schema kubebindv1alpha2.APIResourceSchema - key := types.NamespacedName{Name: name} + schemaSource: schemaSource, + getBoundSchema: func(ctx context.Context, cache cache.Cache, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) { + var schema kubebindv1alpha2.BoundSchema + key := types.NamespacedName{Namespace: namespace, Name: name} if err := cache.Get(ctx, key, &schema); err != nil { return nil, err } @@ -100,11 +102,8 @@ func NewAPIServiceExportRequestReconciler( createServiceExport: func(ctx context.Context, cl client.Client, resource *kubebindv1alpha2.APIServiceExport) error { return cl.Create(ctx, resource) }, - createAPIResourceSchema: func(ctx context.Context, cl client.Client, schema *kubebindv1alpha2.APIResourceSchema) (*kubebindv1alpha2.APIResourceSchema, error) { - if err := cl.Create(ctx, schema); err != nil { - return nil, err - } - return schema, nil + createBoundSchema: func(ctx context.Context, cl client.Client, schema *kubebindv1alpha2.BoundSchema) error { + return cl.Create(ctx, schema) }, deleteServiceExportRequest: func(ctx context.Context, cl client.Client, ns, name string) error { return cl.Delete(ctx, &kubebindv1alpha2.APIServiceExportRequest{ @@ -150,15 +149,15 @@ func getServiceExportRequestMapper(clusterName string, cl cluster.Cluster) handl }) } -// getAPIResourceSchemaMapper creates a mapping function for APIResourceSchema changes. -func getAPIResourceSchemaMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] { +// getBoundSchemaMapper creates a mapping function for BoundSchema changes. +func getBoundSchemaMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] { return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []mcreconcile.Request { - apiResourceSchema := obj.(*kubebindv1alpha2.APIResourceSchema) - apiResourceSchemaKey := apiResourceSchema.Name + boundSchema := obj.(*kubebindv1alpha2.BoundSchema) + boundSchemaKey := boundSchema.Name c := cl.GetClient() var requests kubebindv1alpha2.APIServiceExportRequestList - if err := c.List(ctx, &requests, client.MatchingFields{indexers.ServiceExportRequestByGroupResource: apiResourceSchemaKey}); err != nil { + if err := c.List(ctx, &requests, client.MatchingFields{indexers.ServiceExportRequestByGroupResource: boundSchemaKey}); err != nil { return []mcreconcile.Request{} } @@ -241,8 +240,8 @@ func (r *APIServiceExportRequestReconciler) SetupWithManager(mgr mcmanager.Manag getServiceExportRequestMapper, ). Watches( - &kubebindv1alpha2.APIResourceSchema{}, - getAPIResourceSchemaMapper, + &kubebindv1alpha2.BoundSchema{}, + getBoundSchemaMapper, ). WithOptions(r.opts). Named(controllerName). diff --git a/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go b/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go index 9b80f31aa..bc1b30858 100644 --- a/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go +++ b/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go @@ -18,16 +18,21 @@ package serviceexportrequest import ( "context" + "fmt" + "strings" "time" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/kube-bind/kube-bind/backend/kubernetes/resources" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" @@ -37,33 +42,137 @@ import ( type reconciler struct { informerScope kubebindv1alpha2.InformerScope clusterScopedIsolation kubebindv1alpha2.Isolation + schemaSource string + + getBoundSchema func(ctx context.Context, cache cache.Cache, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) + createBoundSchema func(ctx context.Context, cl client.Client, schema *kubebindv1alpha2.BoundSchema) error - getAPIResourceSchema func(ctx context.Context, cache cache.Cache, name string) (*kubebindv1alpha2.APIResourceSchema, error) getServiceExport func(ctx context.Context, cache cache.Cache, ns, name string) (*kubebindv1alpha2.APIServiceExport, error) createServiceExport func(ctx context.Context, cl client.Client, resource *kubebindv1alpha2.APIServiceExport) error - createAPIResourceSchema func(ctx context.Context, cl client.Client, schema *kubebindv1alpha2.APIResourceSchema) (*kubebindv1alpha2.APIResourceSchema, error) deleteServiceExportRequest func(ctx context.Context, cl client.Client, namespace, name string) error } func (r *reconciler) reconcile(ctx context.Context, cl client.Client, cache cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error { - var errs []error + // We must ensure schemas are created in form of bound schemas first for validation. + // Worst case scenario if validation fails, we will reuse schemas for same consumer once issues are fixed. + if err := r.ensureBoundSchemas(ctx, cl, cache, req); err != nil { + conditions.SetSummary(req) + return err + } + + if err := r.validate(ctx, cl, req); err != nil { + conditions.SetSummary(req) + return err + } if err := r.ensureExports(ctx, cl, cache, req); err != nil { - errs = append(errs, err) + conditions.SetSummary(req) + return err } conditions.SetSummary(req) - return utilerrors.NewAggregate(errs) + return nil +} + +// getExportedSchemas will list all schemas, exported by current backend. +// Important: getExportedSchemas is using client.Client to list resources, not cache. +// This is due to fact we use dynamic client and unstructured.Unstructured to get schemas and it +// does not quite work with dynamic cache informers: +// failed to get informer for *unstructured.UnstructuredList apis.kcp.io/v1alpha1, Kind=APIResourceSchemaList: failed to find newly started informer for apis.kcp.io/v1alpha1, Kind=APIResourceSchema"}. +func (r *reconciler) getExportedSchemas(ctx context.Context, cl client.Client) (kubebindv1alpha2.ExportedSchemas, error) { + parts := strings.SplitN(r.schemaSource, ".", 3) + if len(parts) != 3 { // We check this in validation, but just in case. + return nil, fmt.Errorf("invalid schema source: %q", r.schemaSource) + } + + gvk := schema.GroupVersionKind{ + Kind: parts[0], + Version: parts[1], + Group: parts[2], + } + + // Ensure we have the List kind + listGVK := gvk + if !strings.HasSuffix(listGVK.Kind, "List") { + listGVK.Kind += "List" + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(listGVK) + + // TODO(mjudeikis): This is hardcoded here and in handlers.go for now. + labelSelector := labels.Set{ + resources.ExportedCRDsLabel: "true", + } + + listOpts := []client.ListOption{} + listOpts = append(listOpts, client.MatchingLabelsSelector{Selector: labelSelector.AsSelector()}) + + if err := cl.List(ctx, list, listOpts...); err != nil { + return nil, err + } + + var boundSchemas kubebindv1alpha2.ExportedSchemas = make(map[string]*kubebindv1alpha2.BoundSchema, len(list.Items)) + for _, item := range list.Items { + boundSchema, err := helpers.UnstructuredToBoundSchema(item) + if err != nil { + return nil, err + } + boundSchemas[boundSchema.ResourceGroupName()] = boundSchema + } + + return boundSchemas, nil +} + +func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, cache cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error { + exportedSchemas, err := r.getExportedSchemas(ctx, cl) + if err != nil { + return err + } + + // Ensure all bound schemas exist + for _, res := range req.Spec.Resources { + if len(res.Versions) == 0 { + continue + } + + for _, boundSchema := range exportedSchemas { + if boundSchema.Spec.Group == res.Group && boundSchema.Spec.Names.Plural == res.Resource { + boundSchema.Name = res.ResourceGroupName() + boundSchema.Namespace = req.Namespace + boundSchema.Spec.InformerScope = r.informerScope + boundSchema.ResourceVersion = "" + + obj, err := r.getBoundSchema(ctx, cache, boundSchema.Namespace, boundSchema.Name) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + + // TODO(mjudeikis): https://github.com/kube-bind/kube-bind/issues/297 + if obj != nil { + continue + } + + if err := r.createBoundSchema(ctx, cl, boundSchema); err != nil { + return err + } + } + } + } + + return nil } func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error { logger := klog.FromContext(ctx) + var schemas []*kubebindv1alpha2.BoundSchema + var scope apiextensionsv1.ResourceScope if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhasePending { for _, res := range req.Spec.Resources { - name := res.Resource + "." + res.Group - apiResourceSchema, err := r.getAPIResourceSchema(ctx, cache, name) + name := res.ResourceGroupName() + boundSchema, err := r.getBoundSchema(ctx, cache, req.Namespace, name) if err != nil { if apierrors.IsNotFound(err) { conditions.MarkFalse( @@ -79,39 +188,47 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache return err } - if _, err := r.getServiceExport(ctx, cache, req.Namespace, name); err != nil && !apierrors.IsNotFound(err) { - return err - } else if err == nil { - continue - } + // Collect all schemas for hashing. + // TODO(mjudeikis) Scope is same for all crds so we keep stamping it over. We might want to change this + scope = boundSchema.Spec.Scope + schemas = append(schemas, boundSchema) + } - hash := helpers.APIResourceSchemaCRDSpecHash(&apiResourceSchema.Spec.APIResourceSchemaCRDSpec) - export := &kubebindv1alpha2.APIServiceExport{ - ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, - Namespace: req.Namespace, - Annotations: map[string]string{ - kubebindv1alpha2.SourceSpecHashAnnotationKey: hash, - }, + if _, err := r.getServiceExport(ctx, cache, req.Namespace, req.Name); err != nil && !apierrors.IsNotFound(err) { + return err + } + + hash := helpers.BoundSchemasSpecHash(schemas) + export := &kubebindv1alpha2.APIServiceExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Namespace: req.Namespace, + Annotations: map[string]string{ + kubebindv1alpha2.SourceSpecHashAnnotationKey: hash, }, - Spec: kubebindv1alpha2.APIServiceExportSpec{ - Resources: []kubebindv1alpha2.APIResourceSchemaReference{ - { - Type: "APIResourceSchema", - Name: apiResourceSchema.Name, - }, - }, - InformerScope: r.informerScope, + }, + Spec: kubebindv1alpha2.APIServiceExportSpec{ + InformerScope: r.informerScope, + }, + } + if scope == apiextensionsv1.ClusterScoped { + export.Spec.ClusterScopedIsolation = r.clusterScopedIsolation + } + + for _, res := range req.Spec.Resources { + export.Spec.Resources = append(export.Spec.Resources, kubebindv1alpha2.APIServiceExportResource{ + GroupResource: kubebindv1alpha2.GroupResource{ + Group: res.Group, + Resource: res.Resource, }, - } - if apiResourceSchema.Spec.Scope == apiextensionsv1.ClusterScoped { - export.Spec.ClusterScopedIsolation = r.clusterScopedIsolation - } + Versions: res.Versions, + }) + } + export.Spec.PermissionClaims = req.Spec.PermissionClaims - logger.V(1).Info("Creating APIServiceExport", "name", export.Name, "namespace", export.Namespace) - if err = r.createServiceExport(ctx, cl, export); err != nil { - return err - } + logger.V(1).Info("Creating APIServiceExport", "name", export.Name, "namespace", export.Namespace) + if err := r.createServiceExport(ctx, cl, export); err != nil { + return err } conditions.MarkTrue(req, kubebindv1alpha2.APIServiceExportRequestConditionExportsReady) @@ -121,8 +238,6 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache req.Status.Phase = kubebindv1alpha2.APIServiceExportRequestPhaseFailed req.Status.TerminalMessage = conditions.GetMessage(req, kubebindv1alpha2.APIServiceExportRequestConditionExportsReady) } - - return nil } if time.Since(req.CreationTimestamp.Time) > 10*time.Minute { @@ -132,3 +247,85 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache return nil } + +// Validate validates if the APIServiceExportRequest is in a valid state. +// Currently it validates if all requested schemas are of the same scope and +// if claimable apis are allowed and valid. +func (r *reconciler) validate(ctx context.Context, cl client.Client, req *kubebindv1alpha2.APIServiceExportRequest) error { + exportedSchemas, err := r.getExportedSchemas(ctx, cl) + if err != nil { + return err + } + + if len(exportedSchemas) == 0 { + conditions.MarkFalse( + req, + kubebindv1alpha2.APIServiceExportRequestConditionExportsReady, + "SchemaNotFound", + conditionsapi.ConditionSeverityError, + "SchemaNotFound not found", + ) + return fmt.Errorf("no exported schemas found") + } + + scopes := make([]apiextensionsv1.ResourceScope, 0, len(exportedSchemas)) + for _, res := range req.Spec.Resources { + boundSchema, ok := exportedSchemas[res.ResourceGroupName()] + if !ok { + conditions.MarkFalse( + req, + kubebindv1alpha2.APIServiceExportRequestConditionExportsReady, + "SchemaNotFound", + conditionsapi.ConditionSeverityError, + "Schema %s not found", + res.ResourceGroupName(), + ) + return fmt.Errorf("schema %s not found", res.ResourceGroupName()) + } + scopes = append(scopes, boundSchema.Spec.Scope) + } + + // Check CRD scopes matches. + if len(scopes) > 1 { + first := scopes[0] + for _, scope := range scopes[1:] { + if scope != first { + conditions.MarkFalse(req, + kubebindv1alpha2.APIServiceExportRequestConditionExportsReady, + "DifferentScopes", + conditionsapi.ConditionSeverityError, + "Different scopes found: %v", + scopes, + ) + return fmt.Errorf("different scopes found for claimed resources: %v", scopes) + } + } + } + + // Add validation if claimable apis are valid here + for _, claim := range req.Spec.PermissionClaims { + if !isClaimableAPI(claim) { + conditions.MarkFalse( + req, + kubebindv1alpha2.APIServiceExportConditionPermissionClaim, + "InvalidPermissionClaim", + conditionsapi.ConditionSeverityError, + "Resource %s is not a valid claimable API", + claim.GroupResource.String(), + ) + return fmt.Errorf("resource %s is not a valid claimable API", claim.GroupResource.String()) + } + } + + return nil +} + +// isClaimableAPI checks if a permission claim is for a claimable API. +func isClaimableAPI(claim kubebindv1alpha2.PermissionClaim) bool { + for _, api := range kubebindv1alpha2.ClaimableAPIsData { + if claim.Group == api.GroupVersionResource.Group && claim.Resource == api.Names.Plural { + return true + } + } + return false +} diff --git a/backend/controllers/servicenamespace/servicenamespace_controller.go b/backend/controllers/servicenamespace/servicenamespace_controller.go index de8ca9882..e9f4e52b0 100644 --- a/backend/controllers/servicenamespace/servicenamespace_controller.go +++ b/backend/controllers/servicenamespace/servicenamespace_controller.go @@ -230,7 +230,7 @@ func (r *APIServiceNamespaceReconciler) Reconcile(ctx context.Context, req mcrec // Fetch the APIServiceNamespace instance apiServiceNamespace := &kubebindv1alpha2.APIServiceNamespace{} - if err := client.Get(ctx, req.NamespacedName, apiServiceNamespace); err != nil { + if err := cache.Get(ctx, req.NamespacedName, apiServiceNamespace); err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Handle deletion logic here diff --git a/backend/controllers/servicenamespace/servicenamespace_reconcile.go b/backend/controllers/servicenamespace/servicenamespace_reconcile.go index ffdb1bd53..1058a6179 100644 --- a/backend/controllers/servicenamespace/servicenamespace_reconcile.go +++ b/backend/controllers/servicenamespace/servicenamespace_reconcile.go @@ -25,6 +25,8 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,6 +47,7 @@ type reconciler struct { } func (c *reconciler) reconcile(ctx context.Context, client client.Client, cache cache.Cache, sns *kubebindv1alpha2.APIServiceNamespace) error { + logger := klog.FromContext(ctx) var ns *corev1.Namespace nsName := sns.Namespace + "-" + sns.Name if sns.Status.Namespace != "" { @@ -75,6 +78,131 @@ func (c *reconciler) reconcile(ctx context.Context, client client.Client, cache sns.Status.Namespace = nsName } + // List APIServiceExports in the namespace + apiServiceExports, err := c.listAPIServiceExports(ctx, cache, sns.Namespace) + if err != nil { + return fmt.Errorf("failed to list APIServiceExports: %w", err) + } + + for _, export := range apiServiceExports.Items { + name := "kube-binder-export-" + export.Name // unique name for the service export related permissions + permissions := []rbacv1.PolicyRule{} + for _, claim := range export.Spec.PermissionClaims { + permissions = append(permissions, rbacv1.PolicyRule{ + APIGroups: []string{claim.Group}, + Resources: []string{claim.Resource}, + // We need list and watch for informers to be able to start. And create to create initial object. + Verbs: []string{"*"}, + }) + } + if c.scope == kubebindv1alpha2.ClusterScope { + role, err := c.getPermissionClaimsClusterRole(ctx, cache, name) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get Role %s: %w", name, err) + } + if role == nil { + role = &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Rules: permissions, + } + // Create new ClusterRole + if err := client.Create(ctx, role); err != nil { + return fmt.Errorf("failed to create ClusterRole %s: %w", name, err) + } + } else { + role.Rules = permissions + if err := client.Update(ctx, role); err != nil { + return fmt.Errorf("failed to update ClusterRole %s: %w", name, err) + } + } + + clusterBinding, err := c.getPermissionClaimsClusterRoleBinding(ctx, cache, name) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get ClusterRoleBinding %s: %w", name, err) + } + if clusterBinding == nil { + clusterBinding = &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: sns.Namespace, + Name: kuberesources.ServiceAccountName, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: name, + APIGroup: "rbac.authorization.k8s.io", + }, + } + if err := client.Create(ctx, clusterBinding); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding %s: %w", name, err) + } + } else { + logger.Info("ClusterRoleBinding already exists, update not implemented.", "name", name) + } + } else { + role, err := c.getPermissionClaimsRole(ctx, cache, sns.Status.Namespace, name) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get Role %s: %w", name, err) + } + if role == nil { + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: sns.Status.Namespace, + }, + Rules: permissions, + } + // Create new Role + if err := client.Create(ctx, role); err != nil { + return fmt.Errorf("failed to create Role %s: %w", name, err) + } + } else { + role.Rules = permissions + if err := client.Update(ctx, role); err != nil { + return fmt.Errorf("failed to update Role %s: %w", name, err) + } + } + + rolebinding, err := c.getPermissionClaimsRoleBinding(ctx, cache, sns.Status.Namespace, name) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get Role %s: %w", name, err) + } + + if rolebinding == nil { + rolebinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: sns.Status.Namespace, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: sns.Namespace, + Name: kuberesources.ServiceAccountName, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: name, + APIGroup: "rbac.authorization.k8s.io", + }, + } + if err := client.Create(ctx, rolebinding); err != nil { + return fmt.Errorf("failed to create Role %s: %w", name, err) + } + } else { + logger.Info("Role already exists, update not implemented.", "name", name) + } + } + } + return nil } @@ -119,3 +247,47 @@ func (c *reconciler) ensureRBACRoleBinding(ctx context.Context, client client.Cl return nil } + +func (c *reconciler) getPermissionClaimsClusterRole(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRole, error) { + var role rbacv1.ClusterRole + key := types.NamespacedName{Name: name} + if err := cache.Get(ctx, key, &role); err != nil { + return nil, err + } + return &role, nil +} + +func (c *reconciler) getPermissionClaimsRole(ctx context.Context, cache cache.Cache, namespace, name string) (*rbacv1.Role, error) { + var role rbacv1.Role + key := types.NamespacedName{Namespace: namespace, Name: name} + if err := cache.Get(ctx, key, &role); err != nil { + return nil, err + } + return &role, nil +} + +func (c *reconciler) getPermissionClaimsClusterRoleBinding(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRoleBinding, error) { + var roleBinding rbacv1.ClusterRoleBinding + key := types.NamespacedName{Name: name} + if err := cache.Get(ctx, key, &roleBinding); err != nil { + return nil, err + } + return &roleBinding, nil +} + +func (c *reconciler) getPermissionClaimsRoleBinding(ctx context.Context, cache cache.Cache, namespace, name string) (*rbacv1.RoleBinding, error) { + var roleBinding rbacv1.RoleBinding + key := types.NamespacedName{Name: name, Namespace: namespace} + if err := cache.Get(ctx, key, &roleBinding); err != nil { + return nil, err + } + return &roleBinding, nil +} + +func (c *reconciler) listAPIServiceExports(ctx context.Context, cache cache.Cache, namespace string) (*kubebindv1alpha2.APIServiceExportList, error) { + exports := &kubebindv1alpha2.APIServiceExportList{} + if err := cache.List(ctx, exports, client.InNamespace(namespace)); err != nil { + return nil, err + } + return exports, nil +} diff --git a/backend/http/handler.go b/backend/http/handler.go index 3ca2e8be1..2bc89e462 100644 --- a/backend/http/handler.go +++ b/backend/http/handler.go @@ -17,25 +17,20 @@ limitations under the License. package http import ( - "bytes" "context" "encoding/base64" "encoding/json" "errors" "fmt" - htmltemplate "html/template" "net/http" "net/url" - "sort" "strings" "time" "github.com/gorilla/mux" "github.com/gorilla/securecookie" "golang.org/x/oauth2" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -45,15 +40,11 @@ import ( "github.com/kube-bind/kube-bind/backend/kubernetes" "github.com/kube-bind/kube-bind/backend/kubernetes/resources" "github.com/kube-bind/kube-bind/backend/session" - "github.com/kube-bind/kube-bind/backend/template" + "github.com/kube-bind/kube-bind/backend/spaserver" bindversion "github.com/kube-bind/kube-bind/pkg/version" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" ) -var ( - resourcesTemplate = htmltemplate.Must(htmltemplate.New("resource").Parse(mustRead(template.Files.ReadFile, "resources.gohtml"))) -) - // See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en var noCacheHeaders = map[string]string{ "Expires": time.Unix(0, 0).Format(time.RFC1123), @@ -76,6 +67,8 @@ type handler struct { client *http.Client kubeManager *kubernetes.Manager + + frontend string } func NewHandler( @@ -85,6 +78,7 @@ func NewHandler( schemaSource string, scope kubebindv1alpha2.InformerScope, mgr *kubernetes.Manager, + frontend string, ) (*handler, error) { return &handler{ oidc: provider, @@ -93,6 +87,7 @@ func NewHandler( providerPrettyName: providerPrettyName, testingAutoSelect: testingAutoSelect, schemaSource: schemaSource, + frontend: frontend, scope: scope, client: http.DefaultClient, kubeManager: mgr, @@ -102,21 +97,43 @@ func NewHandler( } func (h *handler) AddRoutes(mux *mux.Router) { - // Server contains double routes for when backend is multi-cluster aware or single cluster. + // API routes - Server contains double routes for when backend is multi-cluster aware or single cluster. // When called multi-cluster aware route in single cluster mode, it will ignore cluster parameter. - mux.HandleFunc("/clusters/{cluster}/exports", h.handleServiceExport).Methods("GET") - mux.HandleFunc("/exports", h.handleServiceExport).Methods("GET") + mux.HandleFunc("/api/clusters/{cluster}/exports", h.handleServiceExport).Methods(http.MethodGet) + mux.HandleFunc("/api/exports", h.handleServiceExport).Methods(http.MethodGet) + + mux.HandleFunc("/api/clusters/{cluster}/resources", h.handleResources).Methods(http.MethodGet) + mux.HandleFunc("/api/resources", h.handleResources).Methods(http.MethodGet) + + mux.HandleFunc("/api/clusters/{cluster}/bind", h.handleBind).Methods(http.MethodGet) + mux.HandleFunc("/api/bind", h.handleBind).Methods(http.MethodGet) - mux.HandleFunc("/clusters/{cluster}/resources", h.handleResources).Methods("GET") - mux.HandleFunc("/resources", h.handleResources).Methods("GET") + mux.HandleFunc("/api/clusters/{cluster}/bind", h.handleBindPost).Methods(http.MethodPost) + mux.HandleFunc("/api/bind", h.handleBindPost).Methods(http.MethodPost) - mux.HandleFunc("/clusters/{cluster}/bind", h.handleBind).Methods("GET") - mux.HandleFunc("/bind", h.handleBind).Methods("GET") + mux.HandleFunc("/api/clusters/{cluster}/authorize", h.handleAuthorize).Methods(http.MethodGet) + mux.HandleFunc("/api/authorize", h.handleAuthorize).Methods(http.MethodGet) - mux.HandleFunc("/clusters/{cluster}/authorize", h.handleAuthorize).Methods("GET") - mux.HandleFunc("/authorize", h.handleAuthorize).Methods("GET") + mux.HandleFunc("/api/callback", h.handleCallback).Methods(http.MethodGet) + mux.HandleFunc("/api/healthz", h.handleHealthz).Methods(http.MethodGet) + mux.HandleFunc("/api/bindable-resources", h.handleBindableResources).Methods(http.MethodGet) + + if strings.HasPrefix(h.frontend, "http://") { + spaserver, err := spaserver.NewSPAReverseProxyServer(h.frontend) + if err != nil { + panic(fmt.Sprintf("failed to create SPA reverse proxy server: %v", err)) // Development only. + } + mux.PathPrefix("/").Handler(spaserver) + } else { + fileSystem := http.Dir(h.frontend) + mux.PathPrefix("/").Handler(spaserver.NewSPAFileServer(fileSystem)) + } - mux.HandleFunc("/callback", h.handleCallback).Methods("GET") +} + +func (h *handler) handleHealthz(w http.ResponseWriter, r *http.Request) { + prepareNoCache(w) + w.WriteHeader(http.StatusOK) } func (h *handler) handleServiceExport(w http.ResponseWriter, r *http.Request) { @@ -127,9 +144,9 @@ func (h *handler) handleServiceExport(w http.ResponseWriter, r *http.Request) { oidcAuthorizeURL := h.oidcAuthorizeURL if oidcAuthorizeURL == "" { if singleClusterScoped { - oidcAuthorizeURL = fmt.Sprintf("http://%s/authorize", r.Host) + oidcAuthorizeURL = fmt.Sprintf("http://%s/api/authorize", r.Host) } else { - oidcAuthorizeURL = fmt.Sprintf("http://%s/clusters/%s/authorize", r.Host, cluster) + oidcAuthorizeURL = fmt.Sprintf("http://%s/api/clusters/%s/authorize", r.Host, cluster) } } @@ -145,7 +162,7 @@ func (h *handler) handleServiceExport(w http.ResponseWriter, r *http.Request) { Kind: "BindingProvider", }, Version: ver, - ProviderPrettyName: "example-backend", + ProviderPrettyName: "backend", AuthenticationMethods: []kubebindv1alpha2.AuthenticationMethod{ { Method: "OAuth2CodeGrant", @@ -202,7 +219,7 @@ func (h *handler) handleAuthorize(w http.ResponseWriter, r *http.Request) { ProviderClusterID: providerCluster, // used in multicluster-runtime providers } if callbackPort != "" && code.RedirectURL == "" { - code.RedirectURL = fmt.Sprintf("http://localhost:%s/callback", callbackPort) + code.RedirectURL = fmt.Sprintf("http://localhost:%s/api/callback", callbackPort) } if code.RedirectURL == "" || code.SessionID == "" || code.ClusterID == "" { @@ -289,6 +306,7 @@ func (h *handler) handleCallback(w http.ResponseWriter, r *http.Request) { secure := false http.SetCookie(w, session.MakeCookie(r, cookieName, encoded, secure, 1*time.Hour)) + // These are UI paths, not API paths, hence no /api/ prefix. UI will call the API for data. if authCode.ProviderClusterID == "" { http.Redirect(w, r, "/resources?s="+cookieName, http.StatusFound) } else { @@ -369,73 +387,42 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { return } - apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster) + exportedSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster) if err != nil { logger.Error(err, "failed to get dynamic resources") http.Error(w, "internal error", http.StatusInternalServerError) return } - var result []UISchema - for _, item := range apiResourceSchemas.Items { - scope := item.UnstructuredContent()["spec"].(map[string]interface{})["scope"] - if scope == nil { - scope = "-" - } - - group := item.UnstructuredContent()["spec"].(map[string]interface{})["group"] - if group == nil { - group = "-" - } - resource := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["plural"] - if resource == nil { - resource = "-" - } - - kind := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["kind"] - if kind == nil { - kind = "-" - } + result := kubebindv1alpha2.BindableResourcesResponse{} + for _, item := range exportedSchemas { - versions := item.UnstructuredContent()["spec"].(map[string]interface{})["versions"] - if versions == nil { - versions = []interface{}{""} - } - for _, v := range versions.([]interface{}) { - version := v.(map[string]interface{})["name"] - result = append(result, UISchema{ - Name: item.GetName(), - Kind: kind.(string), - Scope: scope.(string), - Version: version.(string), - Group: group.(string), - // Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it. - Resource: resource.(string), - SessionID: sessionID, - }) - } + result.Resources = append(result.Resources, kubebindv1alpha2.BindableResource{ + Name: item.GetName(), + Kind: item.Spec.Names.Kind, + Scope: string(item.Spec.Scope), + APIVersion: item.Spec.Versions[0].Name, + Group: item.Spec.Group, + // Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it. + Resource: item.Spec.Names.Plural, + SessionID: sessionID, + }) } - bs := bytes.Buffer{} - if err := resourcesTemplate.Execute(&bs, struct { - Cluster string - Schemas []UISchema - }{ - Cluster: providerCluster, - Schemas: result, - }); err != nil { - logger.Error(err, "failed to execute template") + bs, err := json.Marshal(&result) + if err != nil { + logger.Error(err, "failed to marshal resources") http.Error(w, "internal error", http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write(bs.Bytes()) //nolint:errcheck + w.Header().Set("Content-Type", "application/json") + w.Write(bs) //nolint:errcheck + } func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { logger := getLogger(r) - name := r.URL.Query().Get("name") group := r.URL.Query().Get("group") resource := r.URL.Query().Get("resource") version := r.URL.Query().Get("version") @@ -459,37 +446,6 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { return } - // There is an intent to bind. We need to create APIResourceSchema if one does not exists. - { - apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster) - if err != nil { - logger.Error(err, "failed to get dynamic resources") - http.Error(w, "internal error", http.StatusInternalServerError) - return - } - - schema := &unstructured.Unstructured{} - for _, item := range apiResourceSchemas.Items { - if item.GetName() == name { - schema = &item - break - } - } - if schema == nil || schema.GetName() != name { - logger.Error(nil, "no APIResourceSchema found", "name", name, "group", group, "resource", resource, "version", version) - http.Error(w, fmt.Sprintf("no APIResourceSchema found for %s.%s.%s/%s", group, resource, version, name), http.StatusNotFound) - return - } - - // create apiResourceSchema if not exists - err = h.kubeManager.CreateAPIResourceSchema(r.Context(), providerCluster, name, schema) - if err != nil && !apierrors.IsAlreadyExists(err) { - logger.Error(err, "failed to create APIResourceSchema") - http.Error(w, "internal error", http.StatusInternalServerError) - return - } - } - kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, providerCluster) if err != nil { logger.Error(err, "failed to handle resources") @@ -566,15 +522,168 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, parsedAuthURL.String(), http.StatusFound) } -func mustRead(f func(name string) ([]byte, error), name string) string { - bs, err := f(name) +func (h *handler) handleBindPost(w http.ResponseWriter, r *http.Request) { + logger := getLogger(r) + providerCluster := mux.Vars(r)["cluster"] + + prepareNoCache(w) + + // Get session ID from query parameter + sessionID := r.URL.Query().Get("s") + if sessionID == "" { + logger.Error(errors.New("missing session parameter"), "failed to get session from query") + http.Error(w, "missing session parameter 's'", http.StatusBadRequest) + return + } + + // Get session cookie + ck, err := r.Cookie(sessionID) + if err != nil { + logger.Error(err, "failed to get session cookie") + http.Error(w, "no session cookie found", http.StatusBadRequest) + return + } + + // Decode session state + state := session.State{} + s := securecookie.New(h.cookieSigningKey, h.cookieEncryptionKey) + if err := s.Decode(sessionID, ck.Value, &state); err != nil { + logger.Error(err, "failed to decode session cookie") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + // Parse JSON request body + var bindRequest kubebindv1alpha2.BindableResourcesRequest + if err := json.NewDecoder(r.Body).Decode(&bindRequest); err != nil { + logger.Error(err, "failed to parse JSON request body") + http.Error(w, "invalid JSON request body", http.StatusBadRequest) + return + } + + logger.V(1).Info("received bind request", "resources", len(bindRequest.Resources), "permissionClaims", len(bindRequest.PermissionClaims)) + + // Validate request + if len(bindRequest.Resources) == 0 { + logger.Error(errors.New("no resources specified"), "validation failed") + http.Error(w, "at least one resource must be specified", http.StatusBadRequest) + return + } + + // Handle kubeconfig setup + kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, providerCluster) + if err != nil { + logger.Error(err, "failed to handle resources") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + exportRequest := kubebindv1alpha2.APIServiceExportRequestResponse{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), + Kind: "APIServiceExportRequest", + }, + ObjectMeta: kubebindv1alpha2.NameObjectMeta{ + Name: bindRequest.Name, + }, + Spec: kubebindv1alpha2.APIServiceExportRequestSpec{ + Resources: []kubebindv1alpha2.APIServiceExportRequestResource{}, + PermissionClaims: []kubebindv1alpha2.PermissionClaim{}, + }, + } + + for _, resource := range bindRequest.Resources { + exportRequest.Spec.Resources = append(exportRequest.Spec.Resources, kubebindv1alpha2.APIServiceExportRequestResource{ + GroupResource: kubebindv1alpha2.GroupResource{ + Group: resource.Group, + Resource: resource.Resource, + }, + Versions: []string{resource.APIVersion}, + }) + } + + for _, claim := range bindRequest.PermissionClaims { + _, err := kubebindv1alpha2.ResolveClaimableAPI(claim) + if err != nil { + logger.Error(err, "invalid permission claim", "claim", claim) + http.Error(w, fmt.Sprintf("invalid permission claim: %v", err), http.StatusBadRequest) + return + } + exportRequest.Spec.PermissionClaims = append(exportRequest.Spec.PermissionClaims, kubebindv1alpha2.PermissionClaim{ + GroupResource: kubebindv1alpha2.GroupResource{ + Group: claim.Group, + Resource: claim.Resource, + }, + Selector: kubebindv1alpha2.Selector{ + All: claim.Selector.All, + LabelSelector: claim.Selector.LabelSelector, + }, + }) + } + + requestBytes, err := json.Marshal(&exportRequest) + if err != nil { + logger.Error(err, "failed to marshal export request") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + // Create binding response + response := kubebindv1alpha2.BindingResponse{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), + Kind: "BindingResponse", + }, + Authentication: kubebindv1alpha2.BindingResponseAuthentication{ + OAuth2CodeGrant: &kubebindv1alpha2.BindingResponseAuthenticationOAuth2CodeGrant{ + SessionID: state.SessionID, + ID: state.Token.Issuer + "/" + state.Token.Subject, + }, + }, + Kubeconfig: kfg, + Requests: []runtime.RawExtension{{Raw: requestBytes}}, + } + + payload, err := json.Marshal(&response) if err != nil { - panic(err) + logger.Error(err, "failed to marshal binding response") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + encoded := base64.URLEncoding.EncodeToString(payload) + + parsedAuthURL, err := url.Parse(state.RedirectURL) + if err != nil { + logger.Error(err, "failed to parse redirect URL") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + values := parsedAuthURL.Query() + values.Add("response", encoded) + + parsedAuthURL.RawQuery = values.Encode() + + logger.V(1).Info("redirecting to auth callback after POST bind", + "url", state.RedirectURL+"?response=", + "resourceCount", len(bindRequest.Resources)) + + // For POST requests, we can either redirect or return JSON response + // Since this is called from frontend JavaScript, return the redirect URL as JSON + w.Header().Set("Content-Type", "application/json") + redirectResponse := map[string]string{ + "redirectURL": parsedAuthURL.String(), + } + + if err := json.NewEncoder(w).Encode(redirectResponse); err != nil { + logger.Error(err, "failed to encode redirect response") + http.Error(w, "internal error", http.StatusInternalServerError) + return } - return string(bs) } -func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (*unstructured.UnstructuredList, error) { +func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (kubebindv1alpha2.ExportedSchemas, error) { labelSelector := labels.Set{ resources.ExportedCRDsLabel: "true", } @@ -589,12 +698,20 @@ func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) Version: parts[1], Group: parts[2], } - apiResourceSchemas, err := h.kubeManager.ListDynamicResources(ctx, cluster, gvk, labelSelector.AsSelector()) + return h.kubeManager.ListDynamicResources(ctx, cluster, gvk, labelSelector.AsSelector()) +} + +// handleBindableResources returns a static list of resources that can be claimed/bound by users. +func (h *handler) handleBindableResources(w http.ResponseWriter, r *http.Request) { + logger := getLogger(r) + + bs, err := json.Marshal(&kubebindv1alpha2.ClaimableAPIsData) if err != nil { - return nil, fmt.Errorf("failed to list crds: %w", err) + logger.Error(err, "failed to marshal resources") + http.Error(w, "internal error", http.StatusInternalServerError) + return } - sort.SliceStable(apiResourceSchemas.Items, func(i, j int) bool { - return apiResourceSchemas.Items[i].GetName() < apiResourceSchemas.Items[j].GetName() - }) - return apiResourceSchemas, nil + + w.Header().Set("Content-Type", "application/json") + w.Write(bs) //nolint:errcheck } diff --git a/backend/kubernetes/manager.go b/backend/kubernetes/manager.go index ff7795301..7a6d0ea63 100644 --- a/backend/kubernetes/manager.go +++ b/backend/kubernetes/manager.go @@ -26,7 +26,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" @@ -35,6 +34,7 @@ import ( kuberesources "github.com/kube-bind/kube-bind/backend/kubernetes/resources" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" ) type Manager struct { @@ -150,21 +150,6 @@ func (m *Manager) HandleResources(ctx context.Context, identity, cluster string) return kfgSecret.Data["kubeconfig"], nil } -func (m *Manager) ListAPIResourceSchemas(ctx context.Context, cluster string) (*kubebindv1alpha2.APIResourceSchemaList, error) { - cl, err := m.manager.GetCluster(ctx, cluster) - if err != nil { - return nil, err - } - cache := cl.GetCache() - - var schemas kubebindv1alpha2.APIResourceSchemaList - err = cache.List(ctx, &schemas) - if err != nil { - return nil, err - } - return &schemas, nil -} - func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster string, selector labels.Selector) (*apiextensionsv1.CustomResourceDefinitionList, error) { cl, err := m.manager.GetCluster(ctx, cluster) if err != nil { @@ -181,7 +166,7 @@ func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster str return &crds, nil } -func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk schema.GroupVersionKind, selector labels.Selector) (*unstructured.UnstructuredList, error) { +func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk schema.GroupVersionKind, selector labels.Selector) (kubebindv1alpha2.ExportedSchemas, error) { cl, err := m.manager.GetCluster(ctx, cluster) if err != nil { return nil, err @@ -206,25 +191,14 @@ func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk return nil, err } - return list, nil -} - -func (m *Manager) CreateAPIResourceSchema(ctx context.Context, cluster string, name string, u *unstructured.Unstructured) error { - cl, err := m.manager.GetCluster(ctx, cluster) - if err != nil { - return err - } - c := cl.GetClient() - - apiResourceSchema := &kubebindv1alpha2.APIResourceSchema{} - err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), apiResourceSchema) - if err != nil { - return err + var boundSchemas kubebindv1alpha2.ExportedSchemas = make(map[string]*kubebindv1alpha2.BoundSchema, len(list.Items)) + for _, item := range list.Items { + boundSchema, err := helpers.UnstructuredToBoundSchema(item) + if err != nil { + return nil, err + } + boundSchemas[boundSchema.ResourceGroupName()] = boundSchema } - apiResourceSchema.ResourceVersion = "" - apiResourceSchema.Name = name - apiResourceSchema.Spec.InformerScope = m.scope - - return c.Create(ctx, apiResourceSchema) + return boundSchemas, nil } diff --git a/backend/kubernetes/resources/namespace.go b/backend/kubernetes/resources/namespace.go index 4e3f01a26..59a4bcb09 100644 --- a/backend/kubernetes/resources/namespace.go +++ b/backend/kubernetes/resources/namespace.go @@ -28,7 +28,7 @@ import ( ) const ( - IdentityAnnotationKey = "example-backend.kube-bind.io/identity" + IdentityAnnotationKey = "backend.kube-bind.io/identity" ) func CreateNamespace(ctx context.Context, client client.Client, generateName, id string) (*corev1.Namespace, error) { diff --git a/backend/kubernetes/resources/rbac.go b/backend/kubernetes/resources/rbac.go index 082e62a2f..6e140fe63 100644 --- a/backend/kubernetes/resources/rbac.go +++ b/backend/kubernetes/resources/rbac.go @@ -104,23 +104,13 @@ func EnsureBinderClusterRole(ctx context.Context, client client.Client) error { }, { APIGroups: []string{"kube-bind.io"}, - Resources: []string{"apiresourceschemas"}, - Verbs: []string{"create", "delete", "patch", "update", "get", "list", "watch"}, - }, - { - APIGroups: []string{"kube-bind.io"}, - Resources: []string{"apiresourceschemas/status"}, - Verbs: []string{"get", "patch", "update"}, + Resources: []string{"boundschemas"}, + Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{"kube-bind.io"}, - Resources: []string{"boundapiresourceschemas"}, - Verbs: []string{"create", "delete", "patch", "update", "get", "list", "watch"}, - }, - { - APIGroups: []string{"kube-bind.io"}, - Resources: []string{"boundapiresourceschemas/status"}, - Verbs: []string{"get", "patch", "update"}, + Resources: []string{"boundschemas/status"}, + Verbs: []string{"get", "list", "patch", "update"}, }, }, } diff --git a/backend/options/options.go b/backend/options/options.go index 65d0ba651..0c4279f58 100644 --- a/backend/options/options.go +++ b/backend/options/options.go @@ -56,11 +56,15 @@ type ExtraOptions struct { // Defines the source of the schema for the bind screen. // Options are: // CustomResourceDefinition.v1.apiextensions.k8s.io - // APIResourceSchema.v1alpha2.kube-bind.io + // APIResourceSchema.v1alpha2.apis.kcp.io SchemaSource string TestingAutoSelect string TestingSkipNameValidation bool + + // If ControllerFrontend starts with http:// it is treated as a URL to a SPA server + // Else - it is treated as a path to static files to be served. + Frontend string } type completedOptions struct { @@ -95,6 +99,7 @@ func NewOptions() *Options { ClusterScopedIsolation: string(kubebindv1alpha2.IsolationPrefixed), ServerURL: "", SchemaSource: CustomResourceDefinitionSource.String(), + Frontend: "/www", }, } } @@ -112,14 +117,16 @@ func (s SchemaSource) String() string { } var ( - APIResourceSchemaSource = SchemaSource("APIResourceSchema.v1alpha2.kube-bind.io") + KCPAPIResourceSchemaSource = SchemaSource("APIResourceSchema.v1alpha1.apis.kcp.io") CustomResourceDefinitionSource = SchemaSource("CustomResourceDefinition.v1.apiextensions.k8s.io") ) +// TODO(mjudeikis): https://github.com/kube-bind/kube-bind/issues/298 +// We should relax these once we happy they work with any schema. var schemaSourceAliases = map[string]string{ - CustomResourceDefinitionSource.String(): CustomResourceDefinitionSource.String(), - "apiresourceschema": APIResourceSchemaSource.String(), - "customresourcedefinition": CustomResourceDefinitionSource.String(), + CustomResourceDefinitionSource.String(): CustomResourceDefinitionSource.String(), // mostrly for e2e tests + "customresourcedefinitions": CustomResourceDefinitionSource.String(), + "apiresourceschemas": KCPAPIResourceSchemaSource.String(), } func (options *Options) AddFlags(fs *pflag.FlagSet) { @@ -136,6 +143,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&options.ExternalAddress, "external-address", options.ExternalAddress, "The external address for the service provider cluster, including https:// and port. If not specified, service account's hosts are used.") fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.") fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.") + fs.StringVar(&options.Frontend, "frontend", options.Frontend, "If starts with http:// it is treated as a URL to a SPA server Else - it is treated as a path to static files to be served.") fs.StringVar(&options.Provider, "multicluster-runtime-provider", options.Provider, fmt.Sprintf("The multicluster runtime provider. Possible values are: %v", sets.List(sets.Set[string](sets.StringKeySet(providerAliases)))), diff --git a/backend/server.go b/backend/server.go index d16eb4a32..1183d6c2b 100644 --- a/backend/server.go +++ b/backend/server.go @@ -68,7 +68,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { // setup oidc backend callback := c.Options.OIDC.CallbackURL if callback == "" { - callback = fmt.Sprintf("http://%s/callback", s.WebServer.Addr().String()) + callback = fmt.Sprintf("http://%s/api/callback", s.WebServer.Addr().String()) } s.OIDC, err = http.NewOIDCServiceProvider( c.Options.OIDC.IssuerClientID, @@ -118,6 +118,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { c.Options.SchemaSource, kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), s.Kubernetes, + s.Config.Options.Frontend, ) if err != nil { return nil, fmt.Errorf("error setting up HTTP Handler: %w", err) @@ -179,6 +180,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { opts, kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), kubebindv1alpha2.Isolation(c.Options.ClusterScopedIsolation), + c.Options.SchemaSource, ) if err != nil { return nil, fmt.Errorf("error setting up ServiceExportRequest Controller: %w", err) diff --git a/backend/spaserver/spaserver.go b/backend/spaserver/spaserver.go new file mode 100644 index 000000000..822b7d792 --- /dev/null +++ b/backend/spaserver/spaserver.go @@ -0,0 +1,64 @@ +package spaserver + +import ( + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" +) + +type SPAFileServer struct { + fileSystem http.FileSystem + fileServer http.Handler +} + +func NewSPAFileServer(fileSystem http.FileSystem) *SPAFileServer { + return &SPAFileServer{ + fileSystem: fileSystem, + fileServer: http.FileServer(fileSystem), + } +} + +func (s *SPAFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path, err := filepath.Abs(r.URL.Path) + if err != nil { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + + if f, err := s.fileSystem.Open(path); err == nil { + if err = f.Close(); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + s.fileServer.ServeHTTP(w, r) + } else if os.IsNotExist(err) { + r.URL.Path = "" + s.fileServer.ServeHTTP(w, r) + return + } else { + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} + +// SPAReverseProxyServer is used for local development or in theory it could be used +// if the frontend is running on a different container +type SPAReverseProxyServer struct { + reverseProxy *httputil.ReverseProxy +} + +func NewSPAReverseProxyServer(frontend string) (*SPAReverseProxyServer, error) { + u, err := url.Parse(frontend) + if err != nil { + return nil, err + } + + return &SPAReverseProxyServer{ + reverseProxy: httputil.NewSingleHostReverseProxy(u), + }, nil +} + +func (s *SPAReverseProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.reverseProxy.ServeHTTP(w, r) +} diff --git a/backend/template/resources.gohtml b/backend/template/resources.gohtml index 4a3b87109..1b9818c8d 100644 --- a/backend/template/resources.gohtml +++ b/backend/template/resources.gohtml @@ -20,7 +20,7 @@
  • Scope: {{.Scope}}
  • - Bind + Bind
    {{end}} diff --git a/cli/cmd/crd2apiresourceschema/cmd/crd2apiresourceschema.go b/cli/cmd/crd2apiresourceschema/cmd/crd2apiresourceschema.go deleted file mode 100644 index 11c132cbc..000000000 --- a/cli/cmd/crd2apiresourceschema/cmd/crd2apiresourceschema.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - goflags "flag" - "fmt" - "os" - - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/component-base/version" - "k8s.io/klog/v2" - - crd2apiresourceschemacmd "github.com/kube-bind/kube-bind/cli/pkg/crd2apiresourceschema/cmd" -) - -func CRD2APIResourceSchemaCmd() *cobra.Command { - rootCmd, err := crd2apiresourceschemacmd.New(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v", err) - os.Exit(1) - } - // setup klog - fs := goflags.NewFlagSet("klog", goflags.PanicOnError) - klog.InitFlags(fs) - rootCmd.PersistentFlags().AddGoFlagSet(fs) - - if v := version.Get().String(); len(v) == 0 { - rootCmd.Version = "" - } else { - rootCmd.Version = v - } - - return rootCmd -} diff --git a/cli/cmd/crd2apiresourceschema/main.go b/cli/cmd/crd2apiresourceschema/main.go deleted file mode 100644 index f4c7207e3..000000000 --- a/cli/cmd/crd2apiresourceschema/main.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "fmt" - "os" - - "github.com/spf13/pflag" - - cmd "github.com/kube-bind/kube-bind/cli/cmd/crd2apiresourceschema/cmd" -) - -func main() { - flags := pflag.NewFlagSet("crd2apiresourceschema", pflag.ExitOnError) - pflag.CommandLine = flags - - command := cmd.CRD2APIResourceSchemaCmd() - if err := command.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} diff --git a/cli/pkg/crd2apiresourceschema/cmd/cmd.go b/cli/pkg/crd2apiresourceschema/cmd/cmd.go deleted file mode 100644 index 68e80b079..000000000 --- a/cli/pkg/crd2apiresourceschema/cmd/cmd.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - _ "k8s.io/client-go/plugin/pkg/client/auth/exec" - _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" - logsv1 "k8s.io/component-base/logs/api/v1" - - "github.com/kube-bind/kube-bind/cli/pkg/crd2apiresourceschema/plugin" -) - -var ( - CRD2APIResourceSchemaUses = ` -# Generate APIResourceSchemas from provided CRDs in the cluster and save them to YAML files in the specified output directory -crd2apiresourceschema --output-dir /output/dir - -# Generate APIResourceSchemas from provided CRDs in the cluster and save them to YAML files, specifying a different kubeconfig and output directory -crd2apiresourceschema --kubeconfig /path/to/your/kubeconfig --output-dir /path/to/output/dir - -# Generate and create APIResourceSchema objects for all CRDs in the cluster -crd2apiresourceschema --generate-in-cluster - -# Generate and create APIResourceSchema objects for all CRDs in the cluster, specifying a different kubeconfig -crd2apiresourceschema --kubeconfig /path/to/your/kubeconfig --generate-in-cluster -` -) - -func New(streams genericclioptions.IOStreams) (*cobra.Command, error) { - opts := plugin.NewCRD2APIResourceSchemaOptions(streams) - cmd := &cobra.Command{ - Use: "crd2apiresourceschema", - Short: "Create APIResourceSchema from provided CRDs in the cluster.", - Example: CRD2APIResourceSchemaUses, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - if err := logsv1.ValidateAndApply(opts.Logs, nil); err != nil { - return err - } - - if len(args) > 1 { - return cmd.Help() - } - if err := opts.Complete(args); err != nil { - return err - } - - if err := opts.Validate(); err != nil { - return err - } - - return opts.Run(cmd.Context()) - }, - } - opts.AddCmdFlags(cmd) - - return cmd, nil -} diff --git a/cli/pkg/crd2apiresourceschema/plugin/crd2apiresourceschema.go b/cli/pkg/crd2apiresourceschema/plugin/crd2apiresourceschema.go deleted file mode 100644 index e5421c9ff..000000000 --- a/cli/pkg/crd2apiresourceschema/plugin/crd2apiresourceschema.go +++ /dev/null @@ -1,257 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package plugin - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "time" - - "github.com/spf13/cobra" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/dynamic" - "k8s.io/component-base/logs" - logsv1 "k8s.io/component-base/logs/api/v1" - "k8s.io/component-base/version" - - "github.com/kube-bind/kube-bind/cli/pkg/kubectl/base" - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" - "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" -) - -type CRD2APIResourceSchemaOptions struct { - Options *base.Options - Logs *logs.Options - Print *genericclioptions.PrintFlags - - // GenerateInCluster indicates whether to generate the APIResourceSchema in-cluster. - GenerateInCluster bool - // File containing the CRD to convert to APIResourceSchema. - File string - // OutputDir is the directory where the APIResourceSchemas will be written. - OutputDir string -} - -func NewCRD2APIResourceSchemaOptions(streams genericclioptions.IOStreams) *CRD2APIResourceSchemaOptions { - return &CRD2APIResourceSchemaOptions{ - Options: base.NewOptions(streams), - Logs: logs.NewOptions(), - Print: genericclioptions.NewPrintFlags("crd2apiresourceschema"), - } -} - -func (b *CRD2APIResourceSchemaOptions) AddCmdFlags(cmd *cobra.Command) { - b.Options.BindFlags(cmd) - logsv1.AddFlags(b.Logs, cmd.Flags()) - b.Print.AddFlags(cmd) - - cmd.Flags().BoolVar(&b.GenerateInCluster, "generate-in-cluster", b.GenerateInCluster, "Generate the APIResourceSchema in-cluster.") - cmd.Flags().StringVar(&b.File, "file", b.File, "File with CRD to convert to APIResourceSchema") - cmd.Flags().StringVar(&b.OutputDir, "output-dir", b.OutputDir, "Directory where APIResourceSchemas will be written.") -} - -func (b *CRD2APIResourceSchemaOptions) Complete(args []string) error { - return b.Options.Complete() -} - -func (b *CRD2APIResourceSchemaOptions) Validate() error { - if b.GenerateInCluster && b.OutputDir != "" { - return errors.New("output-dir and generate-in-cluster cannot be used together") - } - - return b.Options.Validate() -} - -// Run starts the process of converting CRDs to APIResourceSchema objects. -func (b *CRD2APIResourceSchemaOptions) Run(ctx context.Context) error { - config, err := b.Options.ClientConfig.ClientConfig() - if err != nil { - return err - } - client, err := dynamic.NewForConfig(config) - if err != nil { - return fmt.Errorf("failed to create dynamic client: %w", err) - } - - if b.OutputDir == "" { - b.OutputDir = "." - } - - var crdList []*apiextensionsv1.CustomResourceDefinition - if b.File != "" { - fileList, err := b.readCRDsFromFile() - if err != nil { - return fmt.Errorf("failed to read CRDs from file: %w", err) - } - crdList = fileList - } else { - clusterList, err := b.listCRDsFromCluster(ctx, client) - if err != nil { - return fmt.Errorf("failed to list CRDs from cluster: %w", err) - } - crdList = clusterList - } - - for _, crdObj := range crdList { - if crdObj.Spec.Group == "kube-bind.io" { - fmt.Fprintf(b.Options.ErrOut, "skipping CRD %s: belongs to group kube-bind.io\n", crdObj.Name) - continue - } - - prefix := fmt.Sprintf("v%s-%s", time.Now().Format("060102"), string(version.Get().GitCommit)) - apiResourceSchema, err := helpers.CRDToAPIResourceSchema(crdObj, prefix) - if err != nil { - fmt.Fprintf(b.Options.ErrOut, "failed to convert CRD %s to APIResourceSchema: %v\n", crdObj.Name, err) - continue - } - - if apiResourceSchema == nil { - fmt.Fprintf(b.Options.ErrOut, "skipping CRD %s: no schema found\n", crdObj.Name) - continue - } - - if b.GenerateInCluster { - if err := generateAPIResourceSchemaInCluster(ctx, client, apiResourceSchema, b.Options.ErrOut, b.Options.Out); err != nil { - continue - } - } - if err := writeObjectToYAML(b.OutputDir, apiResourceSchema, b.Options.Out); err != nil { - return err - } - } - - return nil -} - -func generateAPIResourceSchemaInCluster(ctx context.Context, client dynamic.Interface, apiResourceSchema *kubebindv1alpha2.APIResourceSchema, errOut, out io.Writer) error { - apiResourceSchemaGVR := kubebindv1alpha2.SchemeGroupVersion.WithResource("apiresourceschemas") - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(apiResourceSchema) - if err != nil { - return fmt.Errorf("failed to convert APIResourceSchema to unstructured: %w", err) - } - unstructuredResource := &unstructured.Unstructured{Object: unstructuredObj} - - _, err = client.Resource(apiResourceSchemaGVR).Create(ctx, unstructuredResource, metav1.CreateOptions{}) - if err != nil { - fmt.Fprintf(errOut, "Failed to create APIResourceSchema for CRD %s: %v\n", apiResourceSchema.Name, err) - return err - } - - fmt.Fprintf(out, "Successfully created APIResourceSchema for CRD %s\n", apiResourceSchema.Name) - return nil -} - -func writeObjectToYAML(outputDir string, apiResourceSchema *kubebindv1alpha2.APIResourceSchema, logger io.Writer) error { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) - } - - scheme := runtime.NewScheme() - if err := kubebindv1alpha2.AddToScheme(scheme); err != nil { - return fmt.Errorf("failed to register kubebindv1alpha2 API group: %w", err) - } - - codecs := serializer.NewCodecFactory(scheme) - info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeYAML) - if !ok { - return fmt.Errorf("unsupported media type %q", runtime.ContentTypeYAML) - } - encoder := codecs.EncoderForVersion(info.Serializer, kubebindv1alpha2.SchemeGroupVersion) - - out, err := runtime.Encode(encoder, apiResourceSchema) - if err != nil { - return fmt.Errorf("failed to encode APIResourceSchema %s: %w", apiResourceSchema.Name, err) - } - outputPath := filepath.Join(outputDir, fmt.Sprintf("%s.yaml", apiResourceSchema.Name)) - if err := os.WriteFile(outputPath, out, 0644); err != nil { - return fmt.Errorf("failed to write APIResourceSchema to file %s: %w", outputPath, err) - } - - fmt.Fprintf(logger, "wrote APIResourceSchema %s to %s\n", apiResourceSchema.Name, outputPath) - return nil -} - -func (b *CRD2APIResourceSchemaOptions) readCRDsFromFile() ([]*apiextensionsv1.CustomResourceDefinition, error) { - data, err := os.ReadFile(b.File) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", b.File, err) - } - - scheme := runtime.NewScheme() - if err := apiextensionsv1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register apiextensions v1 scheme: %w", err) - } - - decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() - var crdList []*apiextensionsv1.CustomResourceDefinition - - objects := strings.Split(string(data), "---") - for i, obj := range objects { - obj = strings.TrimSpace(obj) - if obj == "" { - continue - } - - decodedObj, gvk, err := decoder.Decode([]byte(obj), nil, nil) - if err != nil { - fmt.Fprintf(b.Options.ErrOut, "warning: failed to decode object %d: %v\n", i+1, err) - continue - } - - if crd, ok := decodedObj.(*apiextensionsv1.CustomResourceDefinition); ok { - crdList = append(crdList, crd) - fmt.Fprintf(b.Options.Out, "found CRD: %s\n", crd.Name) - } else { - return nil, fmt.Errorf("error: non-CRD object of type %s", gvk.String()) - } - } - - if len(crdList) == 0 { - return nil, fmt.Errorf("no CustomResourceDefinition objects found in file %s", b.File) - } - - return crdList, nil -} - -func (b *CRD2APIResourceSchemaOptions) listCRDsFromCluster(ctx context.Context, client dynamic.Interface) ([]*apiextensionsv1.CustomResourceDefinition, error) { - crdGVR := apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions") - crdList, err := client.Resource(crdGVR).List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to list CRDs: %w", err) - } - - var result []*apiextensionsv1.CustomResourceDefinition - for _, crd := range crdList.Items { - crdObj := &apiextensionsv1.CustomResourceDefinition{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(crd.UnstructuredContent(), crdObj); err != nil { - return nil, fmt.Errorf("failed to convert CRD: %w", err) - } - result = append(result, crdObj) - } - - return result, nil -} diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go index 47ef89fd7..1649680cf 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go @@ -89,6 +89,7 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co }, Namespace: "kube-bind", }, + PermissionClaims: request.Spec.PermissionClaims, }, }, metav1.CreateOptions{}) if err != nil { diff --git a/cli/pkg/kubectl/bind/authenticator/authenticator.go b/cli/pkg/kubectl/bind/authenticator/authenticator.go index 69a38d419..c30721bbc 100644 --- a/cli/pkg/kubectl/bind/authenticator/authenticator.go +++ b/cli/pkg/kubectl/bind/authenticator/authenticator.go @@ -72,7 +72,7 @@ func (d *LocalhostCallbackAuthenticator) Start() error { } mux := http.NewServeMux() - mux.HandleFunc("/callback", d.callback) + mux.HandleFunc("/api/callback", d.callback) d.server = &http.Server{Handler: mux} listener, err := net.ListenTCP("tcp", address) diff --git a/cli/pkg/kubectl/bind/plugin/bind.go b/cli/pkg/kubectl/bind/plugin/bind.go index d6cdc36b6..b79546c1d 100644 --- a/cli/pkg/kubectl/bind/plugin/bind.go +++ b/cli/pkg/kubectl/bind/plugin/bind.go @@ -159,7 +159,7 @@ func (b *BindOptions) Run(ctx context.Context, urlCh chan<- string) error { } if provider.APIVersion != kubebindv1alpha2.GroupVersion { - return fmt.Errorf("unsupported binding provider version: %q", provider.APIVersion) + return fmt.Errorf("unsupported binding provider version: %q != %q", provider.APIVersion, kubebindv1alpha2.GroupVersion) } ns, err := kubeClient.CoreV1().Namespaces().Get(ctx, "kube-bind", metav1.GetOptions{}) diff --git a/deploy/crd/kube-bind.io_apiresourceschemas.yaml b/deploy/crd/kube-bind.io_apiresourceschemas.yaml deleted file mode 100644 index 6e3875153..000000000 --- a/deploy/crd/kube-bind.io_apiresourceschemas.yaml +++ /dev/null @@ -1,362 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.17.3 - name: apiresourceschemas.kube-bind.io -spec: - conversion: - strategy: None - group: kube-bind.io - names: - categories: - - kube-bindings - kind: APIResourceSchema - listKind: APIResourceSchemaList - plural: apiresourceschemas - shortNames: - - as - singular: apiresourceschema - scope: Cluster - versions: - - additionalPrinterColumns: - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha2 - schema: - openAPIV3Schema: - description: APIResourceSchema - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - properties: - conversion: - description: conversion defines conversion settings for the defined - custom resource. - properties: - strategy: - description: |- - strategy specifies how custom resources are converted between versions. Allowed values are: - - `"None"`: The converter only change the apiVersion and would not touch any other field in the custom resource. - - `"Webhook"`: API Server will call to an external webhook to do the conversion. Additional information - is needed for this option. This requires spec.preserveUnknownFields to be false, and spec.conversion.webhook to be set. - enum: - - None - - Webhook - type: string - webhook: - description: webhook describes how to call the conversion webhook. - Required when `strategy` is set to `"Webhook"`. - properties: - clientConfig: - description: clientConfig is the instructions for how to call - the webhook if strategy is `Webhook`. - properties: - caBundle: - description: |- - caBundle is a PEM encoded CA bundle which will be used to validate the webhook's server certificate. - If unspecified, system trust roots on the apiserver are used. - format: byte - type: string - url: - description: |- - url gives the location of the webhook, in standard URL form - (`scheme://host:port/path`). - - Please note that using `localhost` or `127.0.0.1` as a `host` is - risky unless you take great care to run this webhook on all hosts - which run an apiserver which might need to make calls to this - webhook. Such installs are likely to be non-portable, i.e., not easy - to turn up in a new cluster. - - The scheme must be "https"; the URL must begin with "https://". - - A path is optional, and if present may be any string permissible in - a URL. You may use the path to pass an arbitrary string to the - webhook, for example, a cluster identifier. - - Attempting to use a user or basic auth e.g. "user:password@" is not - allowed. Fragments ("#...") and query parameters ("?...") are not - allowed, either. - format: uri - type: string - type: object - conversionReviewVersions: - description: |- - conversionReviewVersions is an ordered list of preferred `ConversionReview` - versions the Webhook expects. The API server will use the first version in - the list which it supports. If none of the versions specified in this list - are supported by API server, conversion will fail for the custom resource. - If a persisted Webhook configuration specifies allowed versions and does not - include any versions known to the API Server, calls to the webhook will fail. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - type: object - x-kubernetes-validations: - - message: Webhook must be specified if strategy=Webhook - rule: (self.strategy == 'None' && !has(self.webhook)) || (self.strategy - == 'Webhook' && has(self.webhook)) - group: - description: "group is the API group of the defined custom resource. - Empty string means the\ncore API group. \tThe resources are served - under `/apis//...` or `/api` for the core group." - type: string - informerScope: - allOf: - - enum: - - Cluster - - Namespaced - - enum: - - Cluster - - Namespaced - description: |- - InformerScope indicates whether the informer for defined custom resource is cluster- or namespace-scoped. - Allowed values are `Cluster` and `Namespaced`. - type: string - names: - description: names specify the resource and kind names for the custom - resource. - properties: - categories: - description: |- - categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). - This is published in API discovery documents, and used by clients to support invocations like - `kubectl get all`. - items: - type: string - type: array - x-kubernetes-list-type: atomic - kind: - description: |- - kind is the serialized kind of the resource. It is normally CamelCase and singular. - Custom resource instances will use this value as the `kind` attribute in API calls. - type: string - listKind: - description: listKind is the serialized kind of the list for this - resource. Defaults to "`kind`List". - type: string - plural: - description: |- - plural is the plural name of the resource to serve. - The custom resources are served under `/apis///.../`. - Must match the name of the CustomResourceDefinition (in the form `.`). - Must be all lowercase. - type: string - shortNames: - description: |- - shortNames are short names for the resource, exposed in API discovery documents, - and used by clients to support invocations like `kubectl get `. - It must be all lowercase. - items: - type: string - type: array - x-kubernetes-list-type: atomic - singular: - description: singular is the singular name of the resource. It - must be all lowercase. Defaults to lowercased `kind`. - type: string - required: - - kind - - plural - type: object - scope: - description: |- - scope indicates whether the defined custom resource is cluster- or namespace-scoped. - Allowed values are `Cluster` and `Namespaced`. - enum: - - Cluster - - Namespaced - type: string - versions: - description: |- - versions is the API version of the defined custom resource. - - Note: the OpenAPI v3 schemas must be equal for all versions until CEL - version migration is supported. - items: - description: APIResourceVersion describes one API version of a resource. - properties: - additionalPrinterColumns: - description: |- - additionalPrinterColumns specifies additional columns returned in Table output. - See https://kubernetes.io/docs/reference/using-api/api-concepts/#receiving-resources-as-tables for details. - If no columns are specified, a single column displaying the age of the custom resource is used. - items: - description: CustomResourceColumnDefinition specifies a column - for server side printing. - properties: - description: - description: description is a human readable description - of this column. - type: string - format: - description: |- - format is an optional OpenAPI type definition for this column. The 'name' format is applied - to the primary identifier column to assist in clients identifying column is the resource name. - See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. - type: string - jsonPath: - description: |- - jsonPath is a simple JSON path (i.e. with array notation) which is evaluated against - each custom resource to produce the value for this column. - type: string - name: - description: name is a human readable name for the column. - type: string - priority: - description: |- - priority is an integer defining the relative importance of this column compared to others. Lower - numbers are considered higher priority. Columns that may be omitted in limited space scenarios - should be given a priority greater than 0. - format: int32 - type: integer - type: - description: |- - type is an OpenAPI type definition for this column. - See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. - type: string - required: - - jsonPath - - name - - type - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - deprecated: - description: |- - deprecated indicates this version of the custom resource API is deprecated. - When set to true, API requests to this version receive a warning header in the server response. - Defaults to false. - type: boolean - deprecationWarning: - description: |- - deprecationWarning overrides the default warning returned to API clients. - May only be set when `deprecated` is true. - The default warning indicates this version is deprecated and recommends use - of the newest served version of equal or greater stability, if one exists. - type: string - name: - description: |- - name is the version name, e.g. “v1”, “v2beta1”, etc. - The custom resources are served under this version at `/apis///...` if `served` is true. - minLength: 1 - pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ - type: string - schema: - description: |- - schema describes the structural schema used for validation, pruning, and defaulting - of this version of the custom resource. - properties: - openAPIV3Schema: - description: openAPIV3Schema is the OpenAPI v3 schema to - use for validation and pruning. - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - required: - - openAPIV3Schema - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - served: - default: true - description: served is a flag enabling/disabling this version - from being served via REST APIs - type: boolean - storage: - description: |- - storage indicates this version should be used when persisting custom resources to storage. - There must be exactly one version with storage=true. - type: boolean - subresources: - description: subresources specify what subresources this version - of the defined custom resource have. - properties: - scale: - description: scale indicates the custom resource should - serve a `/scale` subresource that returns an `autoscaling/v1` - Scale object. - properties: - labelSelectorPath: - description: |- - labelSelectorPath defines the JSON path inside of a custom resource that corresponds to Scale `status.selector`. - Only JSON paths without the array notation are allowed. - Must be a JSON Path under `.status` or `.spec`. - Must be set to work with HorizontalPodAutoscaler. - The field pointed by this JSON path must be a string field (not a complex selector struct) - which contains a serialized label selector in string form. - More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource - If there is no value under the given path in the custom resource, the `status.selector` value in the `/scale` - subresource will default to the empty string. - type: string - specReplicasPath: - description: |- - specReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `spec.replicas`. - Only JSON paths without the array notation are allowed. - Must be a JSON Path under `.spec`. - If there is no value under the given path in the custom resource, the `/scale` subresource will return an error on GET. - type: string - statusReplicasPath: - description: |- - statusReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `status.replicas`. - Only JSON paths without the array notation are allowed. - Must be a JSON Path under `.status`. - If there is no value under the given path in the custom resource, the `status.replicas` value in the `/scale` subresource - will default to 0. - type: string - required: - - specReplicasPath - - statusReplicasPath - type: object - status: - description: |- - status indicates the custom resource should serve a `/status` subresource. - When enabled: - 1. requests to the custom resource primary endpoint ignore changes to the `status` stanza of the object. - 2. requests to the custom resource `/status` subresource ignore changes to anything other than the `status` stanza of the object. - type: object - type: object - required: - - name - - schema - - served - - storage - type: object - minItems: 1 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - required: - - group - - informerScope - - names - - scope - - versions - type: object - type: object - served: true - storage: true - subresources: {} diff --git a/deploy/crd/kube-bind.io_apiservicebindings.yaml b/deploy/crd/kube-bind.io_apiservicebindings.yaml index c68ca4e9f..f545d38f3 100644 --- a/deploy/crd/kube-bind.io_apiservicebindings.yaml +++ b/deploy/crd/kube-bind.io_apiservicebindings.yaml @@ -227,6 +227,90 @@ spec: x-kubernetes-validations: - message: kubeconfigSecretRef is immutable rule: self == oldSelf + permissionClaims: + description: |- + PermissionClaims records decisions about permission claims requested by the service provider. + Access is granted per GroupResource. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array required: - kubeconfigSecretRef type: object diff --git a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml index b7aaf85c8..b9998dc95 100644 --- a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml @@ -221,6 +221,90 @@ spec: x-kubernetes-validations: - message: parameters are immutable rule: self == oldSelf + permissionClaims: + description: |- + PermissionClaims records decisions about permission claims requested by the service provider. + Access is granted per GroupResource. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array resources: description: resources is a list of resources that should be exported. items: diff --git a/deploy/crd/kube-bind.io_apiserviceexports.yaml b/deploy/crd/kube-bind.io_apiserviceexports.yaml index 6a0a9eed2..cdb9a2f77 100644 --- a/deploy/crd/kube-bind.io_apiserviceexports.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexports.yaml @@ -490,27 +490,123 @@ spec: x-kubernetes-validations: - message: informerScope is immutable rule: self == oldSelf + permissionClaims: + description: |- + PermissionClaims records decisions about permission claims requested by the service provider. + Access is granted per GroupResource. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array resources: - description: resources specifies the API resources to export + description: resources is a list of resources that should be exported. items: - description: APIResourceSchemaReference is a list of references - to APIResourceSchemas. properties: - name: - description: Name is the name of the resource to export + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ type: string - type: + resource: description: |- - Type of the resource to export - Currently only APIResourceSchema is supported - enum: - - APIResourceSchema + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ type: string + versions: + description: |- + versions is a list of versions that should be exported. If this is empty + a sensible default is chosen by the service provider. + items: + type: string + type: array required: - - name - - type + - resource type: object + minItems: 1 type: array + x-kubernetes-validations: + - message: resources are immutable + rule: self == oldSelf required: - informerScope - resources @@ -518,53 +614,6 @@ spec: status: description: status contains reconciliation information for the resource. properties: - acceptedNames: - description: |- - acceptedNames are the names that are actually being used to serve discovery. - They may be different than the names in spec. - properties: - categories: - description: |- - categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). - This is published in API discovery documents, and used by clients to support invocations like - `kubectl get all`. - items: - type: string - type: array - x-kubernetes-list-type: atomic - kind: - description: |- - kind is the serialized kind of the resource. It is normally CamelCase and singular. - Custom resource instances will use this value as the `kind` attribute in API calls. - type: string - listKind: - description: listKind is the serialized kind of the list for this - resource. Defaults to "`kind`List". - type: string - plural: - description: |- - plural is the plural name of the resource to serve. - The custom resources are served under `/apis///.../`. - Must match the name of the CustomResourceDefinition (in the form `.`). - Must be all lowercase. - type: string - shortNames: - description: |- - shortNames are short names for the resource, exposed in API discovery documents, - and used by clients to support invocations like `kubectl get `. - It must be all lowercase. - items: - type: string - type: array - x-kubernetes-list-type: atomic - singular: - description: singular is the singular name of the resource. It - must be all lowercase. Defaults to lowercased `kind`. - type: string - required: - - kind - - plural - type: object conditions: description: |- conditions is a list of conditions that apply to the APIServiceExport. It is @@ -612,17 +661,6 @@ spec: - type type: object type: array - storedVersions: - description: |- - storedVersions lists all versions of CustomResources that were ever persisted. Tracking these - versions allows a migration path for stored versions in etcd. The field is mutable - so a migration controller can finish a migration to another version (ensuring - no old objects are left in storage), and then remove the rest of the - versions from this list. - Versions may not be removed from `spec.versions` while they exist in this list. - items: - type: string - type: array type: object required: - spec diff --git a/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml b/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml new file mode 100644 index 000000000..c6a5530ca --- /dev/null +++ b/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml @@ -0,0 +1,176 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: bindableresourcesrequests.kube-bind.io +spec: + group: kube-bind.io + names: + kind: BindableResourcesRequest + listKind: BindableResourcesRequestList + plural: bindableresourcesrequests + singular: bindableresourcesrequest + scope: Namespaced + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + BindableResourcesRequest is sent by the consumer to the service provider + to indicate which resources the user wants to bind to. It is sent after + authentication and resource selection on the service provider website. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + permissionClaims: + description: PermissionClaims are additional permissions that the user + wants to have. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array + resources: + description: resources is a list of resources that the user can select + from. + items: + description: BindableResource describes a resource that the user can + select to bind to. + properties: + apiVersion: + description: apiVersion is the API version of the resource. + type: string + description: + description: description is a human friendly description of the + resource. + type: string + group: + description: group is the API group of the resource. + type: string + kind: + description: kind is the kind of the resource. + type: string + name: + description: name is the name of the resource. + type: string + resource: + description: resource is the plural name of the resource. + type: string + scope: + description: scope is the scope of the resource, either "Cluster" + or "Namespaced". + type: string + sessionID: + description: |- + sessionID is a session ID that the consumer must pass back to the service provider + during the binding step. If multiple backends are aggregated, this can be used to + to authenticate the user to the correct backend. + type: string + required: + - apiVersion + - group + - kind + - name + - resource + - scope + - sessionID + type: object + minItems: 1 + type: array + required: + - resources + type: object + served: true + storage: true diff --git a/deploy/crd/kube-bind.io_boundapiresourceschemas.yaml b/deploy/crd/kube-bind.io_boundschemas.yaml similarity index 96% rename from deploy/crd/kube-bind.io_boundapiresourceschemas.yaml rename to deploy/crd/kube-bind.io_boundschemas.yaml index 16ba489b1..c04946073 100644 --- a/deploy/crd/kube-bind.io_boundapiresourceschemas.yaml +++ b/deploy/crd/kube-bind.io_boundschemas.yaml @@ -3,7 +3,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.3 - name: boundapiresourceschemas.kube-bind.io + name: boundschemas.kube-bind.io spec: conversion: strategy: None @@ -11,12 +11,12 @@ spec: names: categories: - kube-bindings - kind: BoundAPIResourceSchema - listKind: BoundAPIResourceSchemaList - plural: boundapiresourceschemas + kind: BoundSchema + listKind: BoundSchemaList + plural: boundschemas shortNames: - bas - singular: boundapiresourceschema + singular: boundschema scope: Namespaced versions: - additionalPrinterColumns: @@ -26,7 +26,7 @@ spec: name: v1alpha2 schema: openAPIV3Schema: - description: BoundAPIResourceSchema + description: BoundSchema properties: apiVersion: description: |- @@ -46,8 +46,7 @@ spec: metadata: type: object spec: - description: BoundAPIResourceSchemaSpec defines the desired state of the - BoundAPIResourceSchema. + description: BoundSchemaSpec defines the desired state of the BoundSchema. properties: conversion: description: conversion defines conversion settings for the defined @@ -270,15 +269,6 @@ spec: description: |- schema describes the structural schema used for validation, pruning, and defaulting of this version of the custom resource. - properties: - openAPIV3Schema: - description: openAPIV3Schema is the OpenAPI v3 schema to - use for validation and pruning. - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - required: - - openAPIV3Schema type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true @@ -359,8 +349,7 @@ spec: - versions type: object status: - description: BoundAPIResourceSchemaStatus defines the observed state of - the BoundAPIResourceSchema. + description: BoundSchemaStatus defines the observed state of the BoundSchema. properties: acceptedNames: description: |- diff --git a/deploy/examples/apiresourceschema.yaml b/deploy/examples/apiresourceschema.yaml index 5479f3de5..aa8f1277e 100644 --- a/deploy/examples/apiresourceschema.yaml +++ b/deploy/examples/apiresourceschema.yaml @@ -2,6 +2,8 @@ apiVersion: kube-bind.io/v1alpha2 kind: APIResourceSchema metadata: name: mangodbs.mangodb.com + labels: + kube-bind.io/exported: "true" spec: informerScope: Namespaced group: mangodb.com diff --git a/deploy/patches/kube-bind.io_boundapiresourceschemas.yaml-patch b/deploy/patches/kube-bind.io_boundapiresourceschemas.yaml-patch deleted file mode 100644 index 15feab278..000000000 --- a/deploy/patches/kube-bind.io_boundapiresourceschemas.yaml-patch +++ /dev/null @@ -1,5 +0,0 @@ -# schema's Convert functions directly, but the CRD still needs to define a conversion. -- op: add - path: /spec/conversion - value: - strategy: None \ No newline at end of file diff --git a/deploy/patches/kube-bind.io_apiresourceschemas.yaml-patch b/deploy/patches/kube-bind.io_boundschemas.yaml-patch similarity index 87% rename from deploy/patches/kube-bind.io_apiresourceschemas.yaml-patch rename to deploy/patches/kube-bind.io_boundschemas.yaml-patch index 15feab278..aed790a21 100644 --- a/deploy/patches/kube-bind.io_apiresourceschemas.yaml-patch +++ b/deploy/patches/kube-bind.io_boundschemas.yaml-patch @@ -2,4 +2,4 @@ - op: add path: /spec/conversion value: - strategy: None \ No newline at end of file + strategy: None diff --git a/hack/dex-config-dev.yaml b/hack/dex-config-dev.yaml index 9e2bdbe91..ebb982fdc 100644 --- a/hack/dex-config-dev.yaml +++ b/hack/dex-config-dev.yaml @@ -110,6 +110,7 @@ staticClients: - id: kube-bind redirectURIs: - 'http://127.0.0.1:8080/callback' + - 'http://127.0.0.1:8080/api/callback' name: 'Kube Bind' secret: ZXhhbXBsZS1hcHAtc2VjcmV0 diff --git a/kcp/README.md b/kcp/README.md index 2f100f725..bac60ee96 100644 --- a/kcp/README.md +++ b/kcp/README.md @@ -23,6 +23,11 @@ It will do the following: # How to run +1. Start Dex: +```bash +./bin/dex serve ./kcp/deploy/examples/dex-config.yaml +``` + 1. Start kcp 2. Bootstrap kcp: ```bash @@ -40,23 +45,25 @@ bin/backend \ --oidc-issuer-client-secret=ZXhhbXBsZS1hcHAtc2VjcmV0 \ --oidc-issuer-client-id=kube-bind \ --oidc-issuer-url=http://127.0.0.1:5556/dex \ - --oidc-callback-url=http://127.0.0.1:8080/callback \ + --oidc-callback-url=http://127.0.0.1:8080/api/callback \ --pretty-name="BigCorp.com" \ --namespace-prefix="kube-bind-" \ --cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= \ - --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= + --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= \ + --schema-source apiresourceschemas \ + --consumer-scope=namespaced --frontend http://localhost:3000 ``` -4. Copy the kubeconfig to the provider: +4. Copy the kubeconfig to the provider and create provider workspace: ```bash cp .kcp/admin.kubeconfig .kcp/provider.kubeconfig export KUBECONFIG=.kcp/provider.kubeconfig k ws use :root +kubectl ws create provider --enter ``` -5. Run `kubectl ws create provider --enter` -6. Bind the APIExport to the workspace +5. Bind the APIExport to the workspace ```bash kubectl kcp bind apiexport root:kube-bind:kube-bind.io --accept-permission-claim clusterrolebindings.rbac.authorization.k8s.io \ --accept-permission-claim clusterroles.rbac.authorization.k8s.io \ @@ -65,14 +72,18 @@ kubectl kcp bind apiexport root:kube-bind:kube-bind.io --accept-permission-claim --accept-permission-claim configmaps.core \ --accept-permission-claim secrets.core \ --accept-permission-claim namespaces.core \ - --accept-permission-claim serviceaccounts.rbac.authorization.k8s.io \ --accept-permission-claim roles.rbac.authorization.k8s.io \ - --accept-permission-claim rolebindings.rbac.authorization.k8s.io + --accept-permission-claim rolebindings.rbac.authorization.k8s.io \ + --accept-permission-claim apiresourceschemas.apis.kcp.io ``` 7. Create CRD: ```bash -kubectl apply -f deploy/examples/crd-mangodb.yaml +kubectl create -f kcp/deploy/examples/apiexport.yaml +kubectl create -f kcp/deploy/examples/apiresourceschema-cowboys.yaml +kubectl create -f kcp/deploy/examples/apiresourceschema-sheriffs.yaml +# recursive bind +kubectl kcp bind apiexport root:provider:cowboys-stable ``` 8. Get LogicalCluster: @@ -80,7 +91,7 @@ kubectl apply -f deploy/examples/crd-mangodb.yaml ```bash kubectl get logicalcluster # NAME PHASE URL AGE -# cluster Ready https://192.168.2.166:6443/clusters/2xh2v3gzjhn4tmve +# cluster Ready https://192.168.2.166:6443/clusters/h3tp9nwm225v19nz ``` 9. Now we gonna initiate consumer: @@ -94,42 +105,27 @@ kubectl ws create consumer --enter 10. Bind the thing: ```bash -./bin/kubectl-bind http://127.0.0.1:8080/clusters/2vgrh380y0cq38du/exports --dry-run -o yaml > apiserviceexport.yaml +./bin/kubectl-bind http://127.0.0.1:8080/api/clusters/h3tp9nwm225v19nz/exports --dry-run -o yaml > apiserviceexport.yaml # Extract secret for binding process. Note that secret name is not the same as output from command above. Check secret # name by running `kubectl get secret -n kube-bind` -kubectl get secret kubeconfig-wvvsb -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote.kubeconfig +kubectl get secret kubeconfig-zjx72 -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote.kubeconfig -./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f apiserviceexport.yaml --skip-konnector --remote-namespace kube-bind-m5zx4 +./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f kcp/deploy/examples/apiserviceexport-namespaced.yaml --skip-konnector --remote-namespace kube-bind-v9jlf -export KUBECONFIG=.kcp/consumer.kubeconfig -go run ./cmd/konnector/ --lease-namespace default +./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f kcp/deploy/examples/apiserviceexport-cluster.yaml --skip-konnector --remote-namespace kube-bind-2nkhf -11. (Optional) Add second consumer to test - -```bash -cp .kcp/admin.kubeconfig .kcp/consumer2.kubeconfig -export KUBECONFIG=.kcp/consumer2.kubeconfig -kubectl ws use :root -kubectl ws create consumer2 --enter - -./bin/kubectl-bind http://127.0.0.1:8080/clusters/2vgrh380y0cq38du/exports --dry-run -o yaml > apiserviceexport2.yaml -kubectl get secret kubeconfig-wvvsb -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote2.kubeconfig - -./bin/kubectl-bind apiservice --remote-kubeconfig remote2.kubeconfig -f apiserviceexport.yaml --skip-konnector --remote-namespace kube-bind-m5zx4 - - -export KUBECONFIG=.kcp/consumer2.kubeconfig -go run ./cmd/konnector/ --lease-namespace default --server-address :8091 +export KUBECONFIG=.kcp/consumer.kubeconfig +go run ./cmd/konnector/ --lease-namespace default ``` -Create objects: +Create objects: ``` -kubectl create -f deploy/examples/mangodb.yaml +kubectl apply -f kcp/deploy/examples/cowboy.yaml +kubectl apply -f kcp/deploy/examples/sheriff.yaml ``` - ## Debug ```bash @@ -138,4 +134,11 @@ export KUBECONFIG=.kcp/debug.kubeconfig k ws use :root:kube-bind k -s "$(kubectl get apiexportendpointslice kube-bind.io -o jsonpath="{.status.endpoints[0].url}")/clusters/*" api-resources -k -s "$(kubectl get apiexportendpointslice kube-bind.io -o jsonpath="{.status.endpoints[0].url}")/clusters/*" get crd \ No newline at end of file +k -s "$(kubectl get apiexportendpointslice kube-bind.io -o jsonpath="{.status.endpoints[0].url}")/clusters/*" get crd + +# some claimed objects +kubectl create cm provider -n kube-bind-lxj5k-default +kubectl label cm provider app=wildwest -n kube-bind-lxj5k-default + +kubectl create cm consumer -n default +kubectl label cm consumer app=wildwest -n default \ No newline at end of file diff --git a/kcp/deploy/bootstrap.go b/kcp/deploy/bootstrap.go index 86d02c335..710a14af2 100644 --- a/kcp/deploy/bootstrap.go +++ b/kcp/deploy/bootstrap.go @@ -234,6 +234,21 @@ func bindAPIExport(ctx context.Context, kcpClient kcpclient.Interface, exportNam }, State: apisv1alpha2.ClaimAccepted, }, + { + ScopedPermissionClaim: apisv1alpha2.ScopedPermissionClaim{ + PermissionClaim: apisv1alpha2.PermissionClaim{ + GroupResource: apisv1alpha2.GroupResource{ + Group: "apis.kcp.io", + Resource: "apiresourceschemas", + }, + Verbs: []string{"*"}, + }, + Selector: apisv1alpha2.PermissionClaimSelector{ + MatchAll: true, + }, + }, + State: apisv1alpha2.ClaimAccepted, + }, } _, err := kcpClient.ApisV1alpha2().APIBindings().Create(ctx, binding, metav1.CreateOptions{}) diff --git a/kcp/deploy/examples/apiexport.yaml b/kcp/deploy/examples/apiexport.yaml new file mode 100644 index 000000000..ec68ee67b --- /dev/null +++ b/kcp/deploy/examples/apiexport.yaml @@ -0,0 +1,16 @@ +apiVersion: apis.kcp.io/v1alpha2 +kind: APIExport +metadata: + name: cowboys-stable +spec: + resources: + - group: wildwest.dev + name: cowboys + schema: today.cowboys.wildwest.dev + storage: + crd: {} + - group: wildwest.dev + name: sheriffs + schema: today.sheriffs.wildwest.dev + storage: + crd: {} \ No newline at end of file diff --git a/kcp/deploy/examples/apiresourceschema-cowboys.yaml b/kcp/deploy/examples/apiresourceschema-cowboys.yaml new file mode 100644 index 000000000..8eeccbd44 --- /dev/null +++ b/kcp/deploy/examples/apiresourceschema-cowboys.yaml @@ -0,0 +1,48 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + name: today.cowboys.wildwest.dev + labels: + kube-bind.io/exported: "true" +spec: + group: wildwest.dev + names: + kind: Cowboy + listKind: CowboyList + plural: cowboys + singular: cowboy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + description: Cowboy is part of the wild west + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: CowboySpec holds the desired state of the Cowboy. + properties: + intent: + type: string + type: object + status: + description: CowboyStatus communicates the observed state of the Cowboy. + properties: + result: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} \ No newline at end of file diff --git a/kcp/deploy/examples/apiresourceschema-sheriffs.yaml b/kcp/deploy/examples/apiresourceschema-sheriffs.yaml new file mode 100644 index 000000000..dfbdb00ba --- /dev/null +++ b/kcp/deploy/examples/apiresourceschema-sheriffs.yaml @@ -0,0 +1,48 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + name: today.sheriffs.wildwest.dev + labels: + kube-bind.io/exported: "true" +spec: + group: wildwest.dev + names: + kind: Sheriff + listKind: SheriffList + plural: sheriffs + singular: sheriff + scope: Cluster + versions: + - name: v1alpha1 + schema: + description: Sheriff is part of the wild west + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SheriffSpec holds the desired state of the Sheriff. + properties: + intent: + type: string + type: object + status: + description: SheriffStatus communicates the observed state of the Sheriff. + properties: + result: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} \ No newline at end of file diff --git a/kcp/deploy/examples/apiserviceexport-cluster.yaml b/kcp/deploy/examples/apiserviceexport-cluster.yaml new file mode 100644 index 000000000..380e4518c --- /dev/null +++ b/kcp/deploy/examples/apiserviceexport-cluster.yaml @@ -0,0 +1,18 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportRequest +metadata: + name: sheriffs.wildwest.dev +spec: + resources: + - group: wildwest.dev + resource: sheriffs + versions: + - v1alpha1 + permissionClaims: + - apiGroup: "" + resource: configmaps + selector: + labelSelector: + matchLabels: + app: wildwest +status: {} diff --git a/kcp/deploy/examples/apiserviceexport-namespaced.yaml b/kcp/deploy/examples/apiserviceexport-namespaced.yaml new file mode 100644 index 000000000..ba214e0b2 --- /dev/null +++ b/kcp/deploy/examples/apiserviceexport-namespaced.yaml @@ -0,0 +1,18 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportRequest +metadata: + name: cowboys.wildwest.dev +spec: + resources: + - group: wildwest.dev + resource: cowboys + versions: + - v1alpha1 + permissionClaims: + - apiGroup: "" + resource: configmaps + selector: + labelSelector: + matchLabels: + app: wildwest +status: {} diff --git a/kcp/deploy/examples/cowboy.yaml b/kcp/deploy/examples/cowboy.yaml new file mode 100644 index 000000000..3fed03145 --- /dev/null +++ b/kcp/deploy/examples/cowboy.yaml @@ -0,0 +1,9 @@ +apiVersion: wildwest.dev/v1alpha1 +kind: Cowboy +metadata: + name: my-cowboy + namespace: default +spec: + intent: "ride into the sunset" +status: + result: "ready to ride" \ No newline at end of file diff --git a/kcp/deploy/examples/sheriff.yaml b/kcp/deploy/examples/sheriff.yaml new file mode 100644 index 000000000..2e7ce2a59 --- /dev/null +++ b/kcp/deploy/examples/sheriff.yaml @@ -0,0 +1,8 @@ +apiVersion: wildwest.dev/v1alpha1 +kind: Sheriff +metadata: + name: sheriff +spec: + intent: "ride into the sunset" +status: + result: "ready to ride" \ No newline at end of file diff --git a/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/kcp/deploy/resources/apiexport-kube-bind.io.yaml index e3f8463c9..b37b8a75c 100644 --- a/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -37,30 +37,29 @@ spec: resource: customresourcedefinitions verbs: - '*' + - group: apis.kcp.io + resource: apiresourceschemas + verbs: + - '*' resources: - group: kube-bind.io name: apiconversions schema: v250809-5ed76a1.apiconversions.kube-bind.io storage: crd: {} - - group: kube-bind.io - name: apiresourceschemas - schema: v250809-5ed76a1.apiresourceschemas.kube-bind.io - storage: - crd: {} - group: kube-bind.io name: apiservicebindings - schema: v250809-5ed76a1.apiservicebindings.kube-bind.io + schema: v250911-d9cca98.apiservicebindings.kube-bind.io storage: crd: {} - group: kube-bind.io name: apiserviceexportrequests - schema: v250809-5ed76a1.apiserviceexportrequests.kube-bind.io + schema: v250911-d9cca98.apiserviceexportrequests.kube-bind.io storage: crd: {} - group: kube-bind.io name: apiserviceexports - schema: v250809-5ed76a1.apiserviceexports.kube-bind.io + schema: v250911-d9cca98.apiserviceexports.kube-bind.io storage: crd: {} - group: kube-bind.io @@ -69,8 +68,13 @@ spec: storage: crd: {} - group: kube-bind.io - name: boundapiresourceschemas - schema: v250809-5ed76a1.boundapiresourceschemas.kube-bind.io + name: bindableresourcesrequests + schema: v250917-8ab6385.bindableresourcesrequests.kube-bind.io + storage: + crd: {} + - group: kube-bind.io + name: boundschemas + schema: v250917-8ab6385.boundschemas.kube-bind.io storage: crd: {} - group: kube-bind.io diff --git a/kcp/deploy/resources/apiresourceschema-apiresourceschemas.kube-bind.io.yaml b/kcp/deploy/resources/apiresourceschema-apiresourceschemas.kube-bind.io.yaml deleted file mode 100644 index ff7a771db..000000000 --- a/kcp/deploy/resources/apiresourceschema-apiresourceschemas.kube-bind.io.yaml +++ /dev/null @@ -1,360 +0,0 @@ -apiVersion: apis.kcp.io/v1alpha1 -kind: APIResourceSchema -metadata: - creationTimestamp: null - name: v250809-5ed76a1.apiresourceschemas.kube-bind.io -spec: - conversion: - strategy: None - group: kube-bind.io - names: - categories: - - kube-bindings - kind: APIResourceSchema - listKind: APIResourceSchemaList - plural: apiresourceschemas - shortNames: - - as - singular: apiresourceschema - scope: Cluster - versions: - - additionalPrinterColumns: - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha2 - schema: - description: APIResourceSchema - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - properties: - conversion: - description: conversion defines conversion settings for the defined - custom resource. - properties: - strategy: - description: |- - strategy specifies how custom resources are converted between versions. Allowed values are: - - `"None"`: The converter only change the apiVersion and would not touch any other field in the custom resource. - - `"Webhook"`: API Server will call to an external webhook to do the conversion. Additional information - is needed for this option. This requires spec.preserveUnknownFields to be false, and spec.conversion.webhook to be set. - enum: - - None - - Webhook - type: string - webhook: - description: webhook describes how to call the conversion webhook. - Required when `strategy` is set to `"Webhook"`. - properties: - clientConfig: - description: clientConfig is the instructions for how to call - the webhook if strategy is `Webhook`. - properties: - caBundle: - description: |- - caBundle is a PEM encoded CA bundle which will be used to validate the webhook's server certificate. - If unspecified, system trust roots on the apiserver are used. - format: byte - type: string - url: - description: |- - url gives the location of the webhook, in standard URL form - (`scheme://host:port/path`). - - Please note that using `localhost` or `127.0.0.1` as a `host` is - risky unless you take great care to run this webhook on all hosts - which run an apiserver which might need to make calls to this - webhook. Such installs are likely to be non-portable, i.e., not easy - to turn up in a new cluster. - - The scheme must be "https"; the URL must begin with "https://". - - A path is optional, and if present may be any string permissible in - a URL. You may use the path to pass an arbitrary string to the - webhook, for example, a cluster identifier. - - Attempting to use a user or basic auth e.g. "user:password@" is not - allowed. Fragments ("#...") and query parameters ("?...") are not - allowed, either. - format: uri - type: string - type: object - conversionReviewVersions: - description: |- - conversionReviewVersions is an ordered list of preferred `ConversionReview` - versions the Webhook expects. The API server will use the first version in - the list which it supports. If none of the versions specified in this list - are supported by API server, conversion will fail for the custom resource. - If a persisted Webhook configuration specifies allowed versions and does not - include any versions known to the API Server, calls to the webhook will fail. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - type: object - x-kubernetes-validations: - - message: Webhook must be specified if strategy=Webhook - rule: (self.strategy == 'None' && !has(self.webhook)) || (self.strategy - == 'Webhook' && has(self.webhook)) - group: - description: "group is the API group of the defined custom resource. - Empty string means the\ncore API group. \tThe resources are served - under `/apis//...` or `/api` for the core group." - type: string - informerScope: - allOf: - - enum: - - Cluster - - Namespaced - - enum: - - Cluster - - Namespaced - description: |- - InformerScope indicates whether the informer for defined custom resource is cluster- or namespace-scoped. - Allowed values are `Cluster` and `Namespaced`. - type: string - names: - description: names specify the resource and kind names for the custom - resource. - properties: - categories: - description: |- - categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). - This is published in API discovery documents, and used by clients to support invocations like - `kubectl get all`. - items: - type: string - type: array - x-kubernetes-list-type: atomic - kind: - description: |- - kind is the serialized kind of the resource. It is normally CamelCase and singular. - Custom resource instances will use this value as the `kind` attribute in API calls. - type: string - listKind: - description: listKind is the serialized kind of the list for this - resource. Defaults to "`kind`List". - type: string - plural: - description: |- - plural is the plural name of the resource to serve. - The custom resources are served under `/apis///.../`. - Must match the name of the CustomResourceDefinition (in the form `.`). - Must be all lowercase. - type: string - shortNames: - description: |- - shortNames are short names for the resource, exposed in API discovery documents, - and used by clients to support invocations like `kubectl get `. - It must be all lowercase. - items: - type: string - type: array - x-kubernetes-list-type: atomic - singular: - description: singular is the singular name of the resource. It must - be all lowercase. Defaults to lowercased `kind`. - type: string - required: - - kind - - plural - type: object - scope: - description: |- - scope indicates whether the defined custom resource is cluster- or namespace-scoped. - Allowed values are `Cluster` and `Namespaced`. - enum: - - Cluster - - Namespaced - type: string - versions: - description: |- - versions is the API version of the defined custom resource. - - Note: the OpenAPI v3 schemas must be equal for all versions until CEL - version migration is supported. - items: - description: APIResourceVersion describes one API version of a resource. - properties: - additionalPrinterColumns: - description: |- - additionalPrinterColumns specifies additional columns returned in Table output. - See https://kubernetes.io/docs/reference/using-api/api-concepts/#receiving-resources-as-tables for details. - If no columns are specified, a single column displaying the age of the custom resource is used. - items: - description: CustomResourceColumnDefinition specifies a column - for server side printing. - properties: - description: - description: description is a human readable description - of this column. - type: string - format: - description: |- - format is an optional OpenAPI type definition for this column. The 'name' format is applied - to the primary identifier column to assist in clients identifying column is the resource name. - See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. - type: string - jsonPath: - description: |- - jsonPath is a simple JSON path (i.e. with array notation) which is evaluated against - each custom resource to produce the value for this column. - type: string - name: - description: name is a human readable name for the column. - type: string - priority: - description: |- - priority is an integer defining the relative importance of this column compared to others. Lower - numbers are considered higher priority. Columns that may be omitted in limited space scenarios - should be given a priority greater than 0. - format: int32 - type: integer - type: - description: |- - type is an OpenAPI type definition for this column. - See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. - type: string - required: - - jsonPath - - name - - type - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - deprecated: - description: |- - deprecated indicates this version of the custom resource API is deprecated. - When set to true, API requests to this version receive a warning header in the server response. - Defaults to false. - type: boolean - deprecationWarning: - description: |- - deprecationWarning overrides the default warning returned to API clients. - May only be set when `deprecated` is true. - The default warning indicates this version is deprecated and recommends use - of the newest served version of equal or greater stability, if one exists. - type: string - name: - description: |- - name is the version name, e.g. “v1”, “v2beta1”, etc. - The custom resources are served under this version at `/apis///...` if `served` is true. - minLength: 1 - pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ - type: string - schema: - description: |- - schema describes the structural schema used for validation, pruning, and defaulting - of this version of the custom resource. - properties: - openAPIV3Schema: - description: openAPIV3Schema is the OpenAPI v3 schema to use - for validation and pruning. - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - required: - - openAPIV3Schema - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - served: - default: true - description: served is a flag enabling/disabling this version - from being served via REST APIs - type: boolean - storage: - description: |- - storage indicates this version should be used when persisting custom resources to storage. - There must be exactly one version with storage=true. - type: boolean - subresources: - description: subresources specify what subresources this version - of the defined custom resource have. - properties: - scale: - description: scale indicates the custom resource should serve - a `/scale` subresource that returns an `autoscaling/v1` - Scale object. - properties: - labelSelectorPath: - description: |- - labelSelectorPath defines the JSON path inside of a custom resource that corresponds to Scale `status.selector`. - Only JSON paths without the array notation are allowed. - Must be a JSON Path under `.status` or `.spec`. - Must be set to work with HorizontalPodAutoscaler. - The field pointed by this JSON path must be a string field (not a complex selector struct) - which contains a serialized label selector in string form. - More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource - If there is no value under the given path in the custom resource, the `status.selector` value in the `/scale` - subresource will default to the empty string. - type: string - specReplicasPath: - description: |- - specReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `spec.replicas`. - Only JSON paths without the array notation are allowed. - Must be a JSON Path under `.spec`. - If there is no value under the given path in the custom resource, the `/scale` subresource will return an error on GET. - type: string - statusReplicasPath: - description: |- - statusReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `status.replicas`. - Only JSON paths without the array notation are allowed. - Must be a JSON Path under `.status`. - If there is no value under the given path in the custom resource, the `status.replicas` value in the `/scale` subresource - will default to 0. - type: string - required: - - specReplicasPath - - statusReplicasPath - type: object - status: - description: |- - status indicates the custom resource should serve a `/status` subresource. - When enabled: - 1. requests to the custom resource primary endpoint ignore changes to the `status` stanza of the object. - 2. requests to the custom resource `/status` subresource ignore changes to anything other than the `status` stanza of the object. - type: object - type: object - required: - - name - - schema - - served - - storage - type: object - minItems: 1 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - required: - - group - - informerScope - - names - - scope - - versions - type: object - type: object - served: true - storage: true - subresources: {} diff --git a/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml b/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml index 33d76eda1..2516d2ef2 100644 --- a/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml +++ b/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250809-5ed76a1.apiservicebindings.kube-bind.io + name: v250911-d9cca98.apiservicebindings.kube-bind.io spec: conversion: strategy: None @@ -222,6 +222,90 @@ spec: x-kubernetes-validations: - message: kubeconfigSecretRef is immutable rule: self == oldSelf + permissionClaims: + description: |- + PermissionClaims records decisions about permission claims requested by the service provider. + Access is granted per GroupResource. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array required: - kubeconfigSecretRef type: object diff --git a/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml b/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml index d1d26d448..fcefcf15c 100644 --- a/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml +++ b/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250809-5ed76a1.apiserviceexportrequests.kube-bind.io + name: v250911-d9cca98.apiserviceexportrequests.kube-bind.io spec: conversion: strategy: None @@ -217,6 +217,90 @@ spec: x-kubernetes-validations: - message: parameters are immutable rule: self == oldSelf + permissionClaims: + description: |- + PermissionClaims records decisions about permission claims requested by the service provider. + Access is granted per GroupResource. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array resources: description: resources is a list of resources that should be exported. items: diff --git a/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml b/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml index 8eb68ef26..6cd85d624 100644 --- a/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml +++ b/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250809-5ed76a1.apiserviceexports.kube-bind.io + name: v250911-d9cca98.apiserviceexports.kube-bind.io spec: conversion: strategy: None @@ -487,27 +487,123 @@ spec: x-kubernetes-validations: - message: informerScope is immutable rule: self == oldSelf + permissionClaims: + description: |- + PermissionClaims records decisions about permission claims requested by the service provider. + Access is granted per GroupResource. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + 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 + type: object + required: + - resource + type: object + type: array resources: - description: resources specifies the API resources to export + description: resources is a list of resources that should be exported. items: - description: APIResourceSchemaReference is a list of references to - APIResourceSchemas. properties: - name: - description: Name is the name of the resource to export + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ type: string - type: + resource: description: |- - Type of the resource to export - Currently only APIResourceSchema is supported - enum: - - APIResourceSchema + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ type: string + versions: + description: |- + versions is a list of versions that should be exported. If this is empty + a sensible default is chosen by the service provider. + items: + type: string + type: array required: - - name - - type + - resource type: object + minItems: 1 type: array + x-kubernetes-validations: + - message: resources are immutable + rule: self == oldSelf required: - informerScope - resources @@ -515,53 +611,6 @@ spec: status: description: status contains reconciliation information for the resource. properties: - acceptedNames: - description: |- - acceptedNames are the names that are actually being used to serve discovery. - They may be different than the names in spec. - properties: - categories: - description: |- - categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). - This is published in API discovery documents, and used by clients to support invocations like - `kubectl get all`. - items: - type: string - type: array - x-kubernetes-list-type: atomic - kind: - description: |- - kind is the serialized kind of the resource. It is normally CamelCase and singular. - Custom resource instances will use this value as the `kind` attribute in API calls. - type: string - listKind: - description: listKind is the serialized kind of the list for this - resource. Defaults to "`kind`List". - type: string - plural: - description: |- - plural is the plural name of the resource to serve. - The custom resources are served under `/apis///.../`. - Must match the name of the CustomResourceDefinition (in the form `.`). - Must be all lowercase. - type: string - shortNames: - description: |- - shortNames are short names for the resource, exposed in API discovery documents, - and used by clients to support invocations like `kubectl get `. - It must be all lowercase. - items: - type: string - type: array - x-kubernetes-list-type: atomic - singular: - description: singular is the singular name of the resource. It must - be all lowercase. Defaults to lowercased `kind`. - type: string - required: - - kind - - plural - type: object conditions: description: |- conditions is a list of conditions that apply to the APIServiceExport. It is @@ -609,17 +658,6 @@ spec: - type type: object type: array - storedVersions: - description: |- - storedVersions lists all versions of CustomResources that were ever persisted. Tracking these - versions allows a migration path for stored versions in etcd. The field is mutable - so a migration controller can finish a migration to another version (ensuring - no old objects are left in storage), and then remove the rest of the - versions from this list. - Versions may not be removed from `spec.versions` while they exist in this list. - items: - type: string - type: array type: object required: - spec diff --git a/kcp/deploy/resources/apiresourceschema-bindableresourcesrequests.kube-bind.io.yaml b/kcp/deploy/resources/apiresourceschema-bindableresourcesrequests.kube-bind.io.yaml new file mode 100644 index 000000000..8178221ad --- /dev/null +++ b/kcp/deploy/resources/apiresourceschema-bindableresourcesrequests.kube-bind.io.yaml @@ -0,0 +1,172 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v250917-8ab6385.bindableresourcesrequests.kube-bind.io +spec: + group: kube-bind.io + names: + kind: BindableResourcesRequest + listKind: BindableResourcesRequestList + plural: bindableresourcesrequests + singular: bindableresourcesrequest + scope: Namespaced + versions: + - name: v1alpha2 + schema: + description: |- + BindableResourcesRequest is sent by the consumer to the service provider + to indicate which resources the user wants to bind to. It is sent after + authentication and resource selection on the service provider website. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + permissionClaims: + description: PermissionClaims are additional permissions that the user wants + to have. + items: + description: |- + permissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + all: + description: |- + all claims all resources for the given group/resource. + This is mutually exclusive with resourceSelector. + type: boolean + labelSelector: + description: LabelSelector is a label selector that selects objects + of a GVR. + 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 + type: object + required: + - resource + type: object + type: array + resources: + description: resources is a list of resources that the user can select from. + items: + description: BindableResource describes a resource that the user can select + to bind to. + properties: + apiVersion: + description: apiVersion is the API version of the resource. + type: string + description: + description: description is a human friendly description of the resource. + type: string + group: + description: group is the API group of the resource. + type: string + kind: + description: kind is the kind of the resource. + type: string + name: + description: name is the name of the resource. + type: string + resource: + description: resource is the plural name of the resource. + type: string + scope: + description: scope is the scope of the resource, either "Cluster" + or "Namespaced". + type: string + sessionID: + description: |- + sessionID is a session ID that the consumer must pass back to the service provider + during the binding step. If multiple backends are aggregated, this can be used to + to authenticate the user to the correct backend. + type: string + required: + - apiVersion + - group + - kind + - name + - resource + - scope + - sessionID + type: object + minItems: 1 + type: array + required: + - resources + type: object + served: true + storage: true + subresources: {} diff --git a/kcp/deploy/resources/apiresourceschema-boundapiresourceschemas.kube-bind.io.yaml b/kcp/deploy/resources/apiresourceschema-boundschemas.kube-bind.io.yaml similarity index 96% rename from kcp/deploy/resources/apiresourceschema-boundapiresourceschemas.kube-bind.io.yaml rename to kcp/deploy/resources/apiresourceschema-boundschemas.kube-bind.io.yaml index 279671445..4798b5c4f 100644 --- a/kcp/deploy/resources/apiresourceschema-boundapiresourceschemas.kube-bind.io.yaml +++ b/kcp/deploy/resources/apiresourceschema-boundschemas.kube-bind.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250809-5ed76a1.boundapiresourceschemas.kube-bind.io + name: v250917-8ab6385.boundschemas.kube-bind.io spec: conversion: strategy: None @@ -10,12 +10,12 @@ spec: names: categories: - kube-bindings - kind: BoundAPIResourceSchema - listKind: BoundAPIResourceSchemaList - plural: boundapiresourceschemas + kind: BoundSchema + listKind: BoundSchemaList + plural: boundschemas shortNames: - bas - singular: boundapiresourceschema + singular: boundschema scope: Namespaced versions: - additionalPrinterColumns: @@ -24,7 +24,7 @@ spec: type: date name: v1alpha2 schema: - description: BoundAPIResourceSchema + description: BoundSchema properties: apiVersion: description: |- @@ -44,8 +44,7 @@ spec: metadata: type: object spec: - description: BoundAPIResourceSchemaSpec defines the desired state of the - BoundAPIResourceSchema. + description: BoundSchemaSpec defines the desired state of the BoundSchema. properties: conversion: description: conversion defines conversion settings for the defined @@ -268,15 +267,6 @@ spec: description: |- schema describes the structural schema used for validation, pruning, and defaulting of this version of the custom resource. - properties: - openAPIV3Schema: - description: openAPIV3Schema is the OpenAPI v3 schema to use - for validation and pruning. - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - required: - - openAPIV3Schema type: object x-kubernetes-map-type: atomic x-kubernetes-preserve-unknown-fields: true @@ -357,8 +347,7 @@ spec: - versions type: object status: - description: BoundAPIResourceSchemaStatus defines the observed state of - the BoundAPIResourceSchema. + description: BoundSchemaStatus defines the observed state of the BoundSchema. properties: acceptedNames: description: |- diff --git a/kcp/go.mod b/kcp/go.mod index ef779ae68..f42500e17 100644 --- a/kcp/go.mod +++ b/kcp/go.mod @@ -4,35 +4,54 @@ go 1.24.0 replace github.com/kube-bind/kube-bind => ../ +replace github.com/kube-bind/kube-bind/sdk/apis => ../sdk/apis + +replace github.com/kube-bind/kube-bind/sdk/client => ../sdk/client + +replace github.com/kube-bind/kube-bind/cli => ../cli + require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/kcp-dev/client-go v0.0.0-20250728134101-0355faa9361b github.com/kcp-dev/kcp v0.28.0 github.com/kcp-dev/kcp/sdk v0.28.0 github.com/kcp-dev/logicalcluster/v3 v3.0.5 + github.com/kube-bind/kube-bind v0.4.6 github.com/spf13/pflag v1.0.7 + github.com/stretchr/testify v1.10.0 + gopkg.in/headzoo/surf.v1 v1.0.1 k8s.io/apiextensions-apiserver v0.33.3 k8s.io/apimachinery v0.33.3 k8s.io/apiserver v0.33.3 + k8s.io/cli-runtime v0.32.0 k8s.io/client-go v0.33.3 k8s.io/component-base v0.33.3 k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/yaml v1.4.0 ) require ( cel.dev/expr v0.19.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-oidc v2.3.0+incompatible // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dexidp/dex/api/v2 v2.1.0 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -44,27 +63,48 @@ require ( github.com/google/cel-go v0.23.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect + github.com/headzoo/surf v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250728122101-adbf20db3e51 // indirect + github.com/kcp-dev/kcp/pkg/apis v0.11.0 // indirect + github.com/kcp-dev/multicluster-provider v0.1.0 // indirect + github.com/kube-bind/kube-bind/cli v0.0.0-20250515145715-d9f20e7c840d // indirect + github.com/kube-bind/kube-bind/sdk/apis v0.4.8 // indirect + github.com/kube-bind/kube-bind/sdk/client v0.4.6 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/martinlindhe/base36 v1.1.1 // indirect + github.com/mdp/qrterminal/v3 v3.2.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.36.2 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect + github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect go.etcd.io/etcd/api/v3 v3.5.21 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect go.etcd.io/etcd/client/v3 v3.5.21 // indirect @@ -89,21 +129,28 @@ require ( golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/grpc v1.69.2 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.33.3 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + rsc.io/qr v0.2.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/controller-runtime v0.21.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) replace ( @@ -141,3 +188,7 @@ replace ( k8s.io/sample-cli-plugin => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-cli-plugin v0.0.0-20250816165010-ffe1d7c8649b k8s.io/sample-controller => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-controller v0.0.0-20250816165010-ffe1d7c8649b ) + +replace sigs.k8s.io/multicluster-runtime => github.com/mjudeikis/sigs-multicluster-runtime v0.0.0-20250818101434-d8ebc45e169b + +replace github.com/kcp-dev/multicluster-provider => github.com/mjudeikis/kcp-multicluster-provider v0.0.0-20250818102159-3d31cbb06ebe diff --git a/kcp/go.sum b/kcp/go.sum index 72a6d12a7..7f722d970 100644 --- a/kcp/go.sum +++ b/kcp/go.sum @@ -1,7 +1,13 @@ cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -12,25 +18,37 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0= +github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dexidp/dex/api/v2 v2.1.0 h1:V7XTnG2HM2bqWZMABDQpf4EA6F+0jWPsv9pGaUIDo+k= +github.com/dexidp/dex/api/v2 v2.1.0/go.mod h1:s91/6CI290JhYN1F8aiRifLF71qRGLVZvzq68uC6Ln4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -51,6 +69,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -63,12 +83,22 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -77,6 +107,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/headzoo/surf v1.0.1 h1:wk3+LT8gjnCxEwfBJl6MhaNg154En5KjgmgzAG9uMS0= +github.com/headzoo/surf v1.0.1/go.mod h1:/bct0m/iMNEqpn520y01yoaWxsAEigGFPnvyR1ewR5M= +github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca h1:utFgFwgxaqx5OthzE3DSGrtOq7rox5r2sxZ2wbfTuK0= +github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca/go.mod h1:8926sG02TCOX4RFRzIMFIzRw4xuc/TwO2gtN7teMJZ4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= @@ -91,6 +125,8 @@ github.com/kcp-dev/client-go v0.0.0-20250728134101-0355faa9361b h1:2LGrXvY9sc4l5 github.com/kcp-dev/client-go v0.0.0-20250728134101-0355faa9361b/go.mod h1:QdO8AaGAZPr/rIZ1iVanCM3tUOiiuX897GWv7WTByLE= github.com/kcp-dev/kcp v0.28.0 h1:J3oaOPqc4A2Q+wZveL0iVElAuOLivFmKTCpaKVx8iXA= github.com/kcp-dev/kcp v0.28.0/go.mod h1:q28Fx8sU/KA8kz8HGwtaqA7Iom8oR90ydoPK39jMaxo= +github.com/kcp-dev/kcp/pkg/apis v0.11.0 h1:K6p+tNHNcvfACCPLcHgY0EMLeaIwR1jS491FyLfXMII= +github.com/kcp-dev/kcp/pkg/apis v0.11.0/go.mod h1:8cUAmfMJcksauz53UtsLYG8Phhx62rvuCnd/5t/Zihk= github.com/kcp-dev/kcp/sdk v0.28.0 h1:AOgGrgpqhrplbXMSbcvjFwCqwg4UlysTwIFZ0LvFxlk= github.com/kcp-dev/kcp/sdk v0.28.0/go.mod h1:8oZpWxkoMu2TDpx5DgdIGDigByKHKkeqVMA4GiWneoI= github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250816165010-ffe1d7c8649b h1:CyQuxPfhWg8KdwfmY5aE6KABsh/QhkDXTH2msezxCFY= @@ -101,6 +137,8 @@ github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250816165 github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250816165010-ffe1d7c8649b/go.mod h1:6XMZJoNYwuMArBvS2acFkTR1KqyHSp2QXRLRx9eTk5w= github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250816165010-ffe1d7c8649b h1:C21pLvKT2MUE38+ZNDXeucEbRdb7rewRpBp4C5lzz6M= github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250816165010-ffe1d7c8649b/go.mod h1:STCgTiD+xCCHsfLOPHn5sNVsyktakX/ctW3dMv3erh0= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20250816165010-ffe1d7c8649b h1:sQ7otAwO/YMn5cFt4Fftzt6P0cz2gmW7OnTx2i1Qfis= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20250816165010-ffe1d7c8649b/go.mod h1:m/w/xSuYRnkX3ocgdBKM/TX7B8aNzScl6qCp1Z3XIGw= github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250816165010-ffe1d7c8649b h1:kEieYK/XCUycPf5DCEUZNPvDVHr4ao+rxZvdOQXlMQk= github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250816165010-ffe1d7c8649b/go.mod h1:omt22adyHpxAelVTfG1bssg+xoAUc+Cg+0CXn0Oaim0= github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250816165010-ffe1d7c8649b h1:OazHpbyl1+WvViAUEZw2PxMZNrd5LOPDD+bhnfL5cQM= @@ -113,30 +151,51 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/martinlindhe/base36 v1.1.1 h1:1F1MZ5MGghBXDZ2KJ3QfxmiydlWOGB8HCEtkap5NkVg= +github.com/martinlindhe/base36 v1.1.1/go.mod h1:vMS8PaZ5e/jV9LwFKlm0YLnXl/hpOihiBxKkIoc3g08= +github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= +github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= +github.com/mjudeikis/kcp-multicluster-provider v0.0.0-20250818102159-3d31cbb06ebe h1:rSMxNO43EhRCu49OxJrcueT3x8QJdtDgg9QNsjj8UCI= +github.com/mjudeikis/kcp-multicluster-provider v0.0.0-20250818102159-3d31cbb06ebe/go.mod h1:AQbVcrm76lpSFQ/8Gkbf0ev1eTqbk+dynDw6IW8oprA= +github.com/mjudeikis/sigs-multicluster-runtime v0.0.0-20250818101434-d8ebc45e169b h1:rWXhKkj+BFmR08VYCRVW1/5n+PgKAzcrueYVPjN3K/g= +github.com/mjudeikis/sigs-multicluster-runtime v0.0.0-20250818101434-d8ebc45e169b/go.mod h1:CpBzLMLQKdm+UCchd2FiGPiDdCxM5dgCCPKuaQ6Fsv0= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -148,6 +207,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -165,6 +226,8 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -172,10 +235,16 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= @@ -232,9 +301,12 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5N golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= @@ -247,12 +319,18 @@ golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= @@ -267,6 +345,11 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= @@ -278,10 +361,15 @@ google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/headzoo/surf.v1 v1.0.1 h1:oDBy9b5NlTb2Hvl3hF8NN+Qy7ypC9/g5YDP85pPh13k= +gopkg.in/headzoo/surf.v1 v1.0.1/go.mod h1:T0BH8276y+OPL0E4tisxCFjBVIAKGbwdYU7AS7/EpQQ= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -295,10 +383,18 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/pkg/indexers/servicebinding.go b/pkg/indexers/servicebinding.go index 4c2ce544f..cd467bc0d 100644 --- a/pkg/indexers/servicebinding.go +++ b/pkg/indexers/servicebinding.go @@ -17,12 +17,15 @@ limitations under the License. package indexers import ( + "fmt" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" ) const ( //nolint:gosec ByServiceBindingKubeconfigSecret = "byKubeconfigSecret" + ByAPIServiceBindingCRD = "byCRD" ) func IndexServiceBindingByKubeconfigSecret(obj any) ([]string, error) { @@ -37,3 +40,17 @@ func ByServiceBindingKubeconfigSecretKey(binding *kubebindv1alpha2.APIServiceBin ref := &binding.Spec.KubeconfigSecretRef return ref.Namespace + "/" + ref.Name } + +func IndexAPIServiceBindingByCRD(obj any) ([]string, error) { + binding, ok := obj.(*kubebindv1alpha2.APIServiceBinding) + if !ok { + return nil, nil + } + + keys := make([]string, 0, len(binding.Status.BoundSchemas)) + for _, bound := range binding.Status.BoundSchemas { + keys = append(keys, fmt.Sprintf("%s.%s", bound.Resource, bound.Group)) + } + + return keys, nil +} diff --git a/pkg/indexers/serviceexport.go b/pkg/indexers/serviceexport.go index f449af6a1..af8c05915 100644 --- a/pkg/indexers/serviceexport.go +++ b/pkg/indexers/serviceexport.go @@ -24,7 +24,7 @@ import ( const ( ServiceExportByCustomResourceDefinition = "serviceExportByCustomResourceDefinition" - ServiceExportByAPIResourceSchema = "ServiceExportByAPIResourceSchema" + ServiceExportByBoundSchema = "serviceExportByBoundSchema" ) func IndexServiceExportByCustomResourceDefinition(obj any) ([]string, error) { @@ -36,16 +36,17 @@ func IndexServiceExportByCustomResourceDefinition(obj any) ([]string, error) { return []string{export.Name}, nil } -// IndexServiceExportByAPIResourceSchema is a controller-runtime compatible indexer function. -func IndexServiceExportByAPIResourceSchema(obj client.Object) []string { +// IndexServiceExportByBoundSchema is a controller-runtime compatible indexer function. +func IndexServiceExportByBoundSchema(obj client.Object) []string { export, ok := obj.(*v1alpha2.APIServiceExport) if !ok { return nil } names := make([]string, 0, len(export.Spec.Resources)) - for _, resource := range export.Spec.Resources { - names = append(names, resource.Name) + for _, res := range export.Spec.Resources { + name := res.Resource + "." + res.Group + names = append(names, name) } return names diff --git a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go new file mode 100644 index 000000000..0caeb6e75 --- /dev/null +++ b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go @@ -0,0 +1,399 @@ +/* +Copyright 2022 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package claimedresources + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + dynamicclient "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamiclister" + "k8s.io/client-go/informers" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/pkg/indexers" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/multinsinformer" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + bindlisters "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2" +) + +const ( + controllerName = "kube-bind-konnector-claimed-object" +) + +// NewController returns a new controller reconciling downstream objects to upstream. +func NewController( + gvr schema.GroupVersionResource, + claim kubebindv1alpha2.PermissionClaim, + providerNamespace string, + consumerConfig, providerConfig *rest.Config, + consumerDynamicInformer informers.GenericInformer, + providerDynamicInformer multinsinformer.GetterInformer, + serviceNamespaceInformer dynamic.Informer[bindlisters.APIServiceNamespaceLister], +) (*controller, error) { + queue := workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](), workqueue.TypedRateLimitingQueueConfig[string]{Name: controllerName}) + + logger := klog.Background().WithValues("controller", controllerName, "gvr", gvr) + + providerConfig = rest.CopyConfig(providerConfig) + providerConfig = rest.AddUserAgent(providerConfig, controllerName) + + providerClient, err := dynamicclient.NewForConfig(providerConfig) + if err != nil { + return nil, err + } + consumerClient, err := dynamicclient.NewForConfig(consumerConfig) + if err != nil { + return nil, err + } + + dynamicConsumerLister := dynamiclister.New(consumerDynamicInformer.Informer().GetIndexer(), gvr) + c := &controller{ + queue: queue, + + claim: claim, + + consumerClient: consumerClient, + providerClient: providerClient, + + consumerDynamicLister: dynamicConsumerLister, + consumerDynamicIndexer: consumerDynamicInformer.Informer().GetIndexer(), + + providerDynamicInformer: providerDynamicInformer, + + serviceNamespaceInformer: serviceNamespaceInformer, + + providerNamespace: providerNamespace, + + readReconciler: readReconciler{ + getServiceNamespace: func(upstreamNamespace string) (*kubebindv1alpha2.APIServiceNamespace, error) { + sns, err := serviceNamespaceInformer.Informer().GetIndexer().ByIndex(indexers.ServiceNamespaceByNamespace, upstreamNamespace) + if err != nil { + return nil, err + } + if len(sns) == 0 { + return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("APIServiceNamespace").GroupResource(), upstreamNamespace) + } + return sns[0].(*kubebindv1alpha2.APIServiceNamespace), nil + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + return consumerClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + }, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + obj, err := providerDynamicInformer.Get(ns, name) + if err != nil { + return nil, err + } + return obj.(*unstructured.Unstructured), nil + }, + createProviderObject: func(ctx context.Context, obj *unstructured.Unstructured) error { + _, err := providerClient.Resource(gvr).Namespace(obj.GetNamespace()).Create(ctx, obj, metav1.CreateOptions{}) + return err + }, + updateProviderObject: func(ctx context.Context, obj *unstructured.Unstructured) error { + _, err := providerClient.Resource(gvr).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{}) + return err + }, + deleteProviderObject: func(ctx context.Context, ns, name string) error { + return providerClient.Resource(gvr).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}) + }, + deleteConsumerObject: func(ctx context.Context, ns, name string) error { + return consumerClient.Resource(gvr).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}) + }, + updateConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return consumerClient.Resource(gvr).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{}) + }, + createConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return consumerClient.Resource(gvr).Namespace(obj.GetNamespace()).Create(ctx, obj, metav1.CreateOptions{}) + }, + }, + } + + if _, err = consumerDynamicInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueConsumer(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueConsumer(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueConsumer(logger, obj) + }, + }); err != nil { + return nil, err + } + + if err := providerDynamicInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueProvider(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueProvider(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueProvider(logger, obj) + }, + }); err != nil { + return nil, err + } + + return c, nil +} + +// controller reconciles upstream objects to downstream. +type controller struct { + queue workqueue.TypedRateLimitingInterface[string] + + claim kubebindv1alpha2.PermissionClaim + + consumerClient dynamicclient.Interface + providerClient dynamicclient.Interface + + consumerDynamicLister dynamiclister.Lister + consumerDynamicIndexer cache.Indexer + + providerDynamicInformer multinsinformer.GetterInformer + + serviceNamespaceInformer dynamic.Informer[bindlisters.APIServiceNamespaceLister] + + providerNamespace string + + readReconciler +} + +func (c *controller) isClaimed(obj *unstructured.Unstructured) bool { + if c.claim.Selector.All { + return true + } + + // Check if obj is selected by label selector + if c.claim.Selector.LabelSelector != nil { + selector, err := metav1.LabelSelectorAsSelector(c.claim.Selector.LabelSelector) + if err != nil { + return false + } + l := obj.GetLabels() + if l == nil { + l = make(map[string]string) + } + + return selector.Matches(labels.Set(l)) + } + + return false +} + +func (c *controller) enqueueConsumer(logger klog.Logger, obj interface{}) { + o := obj.(*unstructured.Unstructured) + if !c.isClaimed(o) { + return + } + logger.V(2).Info("queueing consumer object", "gvr", o.GroupVersionKind().String(), "key", fmt.Sprintf("%s/%s", o.GetNamespace(), o.GetName())) + + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + ns, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + runtime.HandleError(err) + return + } + + if ns != "" { + sn, err := c.serviceNamespaceInformer.Lister().APIServiceNamespaces(c.providerNamespace).Get(ns) + if err != nil { + if !errors.IsNotFound(err) { + runtime.HandleError(err) + } + return + } + if sn.Namespace == c.providerNamespace && sn.Status.Namespace != "" { + key := fmt.Sprintf("%s/%s", sn.Status.Namespace, name) + logger.V(2).Info("queueing Unstructured", "key", key) + c.queue.Add(key) + return + } + return + } + + logger.V(2).Info("queueing Unstructured", "key", key) + c.queue.Add(key) +} + +func (c *controller) enqueueProvider(logger klog.Logger, obj interface{}) { + upstreamKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + ns, name, err := cache.SplitMetaNamespaceKey(upstreamKey) + if err != nil { + runtime.HandleError(err) + return + } + + if ns != "" { + sns, err := c.serviceNamespaceInformer.Informer().GetIndexer().ByIndex(indexers.ServiceNamespaceByNamespace, ns) + if err != nil { + if !errors.IsNotFound(err) { + runtime.HandleError(err) + } + return + } + for _, obj := range sns { + sn := obj.(*kubebindv1alpha2.APIServiceNamespace) + if sn.Namespace == c.providerNamespace { + key := fmt.Sprintf("%s/%s", sn.Name, name) + logger.V(2).Info("queueing Unstructured", "key", key) + c.queue.Add(upstreamKey) + return + } + } + return + } + + logger.V(2).Info("queueing Unstructured", "key", upstreamKey) + c.queue.Add(upstreamKey) +} + +func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj interface{}) { + snKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + ns, name, err := cache.SplitMetaNamespaceKey(snKey) + if err != nil { + runtime.HandleError(err) + return + } + if ns != c.providerNamespace { + return // not for us + } + + sn, err := c.serviceNamespaceInformer.Lister().APIServiceNamespaces(ns).Get(name) + if err != nil { + runtime.HandleError(err) + return + } + + if sn.Namespace == "" { + return // not ready + } + + logger.Info("enqueueing service namespace", "name", sn.Status.Namespace) + objs, err := c.providerDynamicInformer.List(sn.Status.Namespace) + if err != nil { + runtime.HandleError(err) + return + } + for _, obj := range objs { + logger.Info("enqueueing provider object", "obj", obj) + + key, err := cache.MetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + continue + } + logger.V(2).Info("queueing Unstructured", "key", key, "reason", "APIServiceNamespace", "ServiceNamespaceKey", key) + c.queue.Add(key) + } +} + +// Start starts the controller, which stops when ctx.Done() is closed. +func (c *controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + defer c.queue.ShutDown() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + c.serviceNamespaceInformer.Informer().AddDynamicEventHandler(ctx, controllerName, cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceNamespace(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueServiceNamespace(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceNamespace(logger, obj) + }, + }) + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *controller) startWorker(ctx context.Context) { + defer runtime.HandleCrash() + + for c.processNextWorkItem(ctx) { + } +} + +func (c *controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + key, quit := c.queue.Get() + if quit { + return false + } + + logger := klog.FromContext(ctx).WithValues("key", key) + ctx = klog.NewContext(ctx, logger) + logger.V(2).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *controller) process(ctx context.Context, key string) error { + ns, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + runtime.HandleError(err) + return nil // we cannot do anything + } + + return c.reconcile(ctx, ns, name) +} diff --git a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler.go b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler.go new file mode 100644 index 000000000..5dca3ef57 --- /dev/null +++ b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler.go @@ -0,0 +1,266 @@ +/* +Copyright 2023 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package claimedresources + +import ( + "context" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog/v2" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +const label = "kube-bind.io/owner" + +type readReconciler struct { + getServiceNamespace func(upstreamNamespace string) (*kubebindv1alpha2.APIServiceNamespace, error) + getProviderObject func(ns, name string) (*unstructured.Unstructured, error) + createProviderObject func(ctx context.Context, obj *unstructured.Unstructured) error + updateProviderObject func(ctx context.Context, obj *unstructured.Unstructured) error + deleteProviderObject func(ctx context.Context, ns, name string) error + + getConsumerObject func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) + updateConsumerObject func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) + createConsumerObject func(ctx context.Context, ob *unstructured.Unstructured) (*unstructured.Unstructured, error) + deleteConsumerObject func(ctx context.Context, ns, name string) error +} + +// reconcile syncs upstream claimed resources to downstream. +func (r *readReconciler) reconcile(ctx context.Context, providerNS, name string) error { + logger := klog.FromContext(ctx) + logger = logger.WithValues("name", name, "providerNamespace", providerNS) + + logger.Info("reconciling object") + consumerNS := "" + if providerNS != "" { + sn, err := r.getServiceNamespace(providerNS) + if err != nil && !errors.IsNotFound(err) { + return err + } else if errors.IsNotFound(err) { + runtime.HandleError(err) + return err // hoping the APIServiceNamespace will be created soon. Otherwise, this item goes into backoff. + } + if sn.Status.Namespace == "" { + runtime.HandleError(err) + return err // hoping the status is set soon. + } + + logger = logger.WithValues("providerNamespace", sn.Status.Namespace) + consumerNS = sn.Name + logger = logger.WithValues("consumerNamespace", consumerNS) + + ctx = klog.NewContext(ctx, logger) + } + + providerObj, providerErr := r.getProviderObject(providerNS, name) + if providerErr != nil && !errors.IsNotFound(providerErr) { + return providerErr + } + consumerObj, consumerErr := r.getConsumerObject(ctx, consumerNS, name) + if consumerErr != nil && !errors.IsNotFound(consumerErr) { + return consumerErr + } + + if errors.IsNotFound(providerErr) && errors.IsNotFound(consumerErr) { + // Nothing to do + return nil + } + + // Determine owner + owner, err := determineOwner(providerObj, consumerObj) + if err != nil { // nothing we can do + logger.Error(err, "could not determine owner") + return nil + } + logger = logger.WithValues("owner", owner) + + switch owner { + case kubebindv1alpha2.OwnerProvider: + if errors.IsNotFound(providerErr) { + err := r.deleteConsumerObject(ctx, consumerNS, name) + if errors.IsNotFound(err) { + return nil + } + return err + } + ownerCandidate := providerObj.DeepCopy() + + // Set owner label if needed + r.makeProviderOwner(ownerCandidate) + if !equality.Semantic.DeepEqual(providerObj, ownerCandidate) { + if err := r.updateProviderObject(ctx, ownerCandidate); err != nil { + return err + } + } + + if errors.IsNotFound(consumerErr) { + logger.Info("Creating missing downstream object", "downstreamNamespace", providerNS, "downstreamName", providerObj.GetName()) + + candidate := candidateFromOwnerObj(consumerNS, providerObj) + r.makeProviderOwner(candidate) + + if _, err := r.createConsumerObject(ctx, candidate); err != nil { + return err + } + + return nil + } + + if providerObj.GetDeletionTimestamp() != nil && !providerObj.GetDeletionTimestamp().IsZero() { + logger.Info("Deleting downstream object because it has been deleted upstream", "downStreamNamespace", providerNS, "downstreamName", providerObj.GetName()) + if err := r.deleteConsumerObject(ctx, providerNS, providerObj.GetName()); err != nil { + return err + } + } + + candidate := candidateFromOwnerObj(consumerNS, providerObj) + current := candidateFromOwnerObj(consumerNS, consumerObj) + if !equality.Semantic.DeepEqual(candidate, current) { + logger.Info("Updating downstream object data", "downstreamNamespace", consumerNS, "downstreamName", consumerObj.GetName()) + if _, err := r.updateConsumerObject(ctx, candidate); err != nil { + logger.Error(err, "error updating consumer object") + return err + } + } + + case kubebindv1alpha2.OwnerConsumer: + if errors.IsNotFound(consumerErr) { + logger.Info("Owner copy of the object is gone, deleting downstream object", "name", name, "namespace", providerNS) + err := r.deleteProviderObject(ctx, providerNS, name) + if errors.IsNotFound(err) { + return nil + } + return err + } + + ownerCandidate := consumerObj.DeepCopy() + r.makeConsumerOwner(ownerCandidate) + if !equality.Semantic.DeepEqual(consumerObj, ownerCandidate) { + logger.Info("setting owner annotation for Consumer object") + if _, err := r.updateConsumerObject(ctx, ownerCandidate); err != nil { + return err + } + } + + candidate := candidateFromOwnerObj(providerNS, ownerCandidate) + r.makeConsumerOwner(candidate) + + if errors.IsNotFound(providerErr) { + logger.Info("creating consumer owned object at provider") + return r.createProviderObject(ctx, candidate) + } + + providerObj := candidateFromOwnerObj(providerNS, providerObj) + if !equality.Semantic.DeepEqual(providerObj, candidate) { + logger.Info("updating consumer owned object at provider") + return r.updateProviderObject(ctx, candidate) + } + } + + return nil +} + +func (r readReconciler) makeConsumerOwner(obj *unstructured.Unstructured) { + a := obj.GetLabels() + if a == nil { + a = map[string]string{} + } + a[label] = string(kubebindv1alpha2.OwnerConsumer) + obj.SetLabels(a) +} + +func (r readReconciler) makeProviderOwner(obj *unstructured.Unstructured) { + a := obj.GetLabels() + if a == nil { + a = map[string]string{} + } + a[label] = string(kubebindv1alpha2.OwnerProvider) + obj.SetLabels(a) +} + +func candidateFromOwnerObj(downstreamNS string, obj *unstructured.Unstructured) *unstructured.Unstructured { + // clean up object + candidate := obj.DeepCopy() + candidate.SetUID("") + candidate.SetResourceVersion("") + candidate.SetNamespace(downstreamNS) + candidate.SetManagedFields(nil) + candidate.SetDeletionTimestamp(nil) + candidate.SetDeletionGracePeriodSeconds(nil) + candidate.SetOwnerReferences(nil) + candidate.SetFinalizers(nil) + candidate.SetNamespace(downstreamNS) + candidate.SetCreationTimestamp(v1.Time{}) + + labels := map[string]string{} + for key, label := range obj.GetLabels() { + if strings.Contains(key, "claimed.internal.apis.kcp.io") { + continue + } + labels[key] = label + } + candidate.SetLabels(labels) + + annotations := map[string]string{} + for key, annotation := range obj.GetAnnotations() { + if strings.Contains(key, "kcp.io/cluster") { + continue + } + annotations[key] = annotation + } + candidate.SetAnnotations(annotations) + + return candidate +} + +// determineOwner determines the owner of a resource given at least one object exists either on the +// consumer or provider side. +func determineOwner(providerObj, consumerObj *unstructured.Unstructured) (kubebindv1alpha2.Owner, error) { + if providerObj != nil { + ownerLabel := providerObj.GetLabels()[label] + switch ownerLabel { + case kubebindv1alpha2.OwnerProvider.String(): + return kubebindv1alpha2.OwnerProvider, nil + case kubebindv1alpha2.OwnerConsumer.String(): + return kubebindv1alpha2.OwnerConsumer, nil + } + if ownerLabel == "" && consumerObj == nil { + return kubebindv1alpha2.OwnerProvider, nil + } + } + + if consumerObj != nil { + ownerLabel := consumerObj.GetLabels()[label] + switch ownerLabel { + case kubebindv1alpha2.OwnerProvider.String(): + return kubebindv1alpha2.OwnerProvider, nil + case kubebindv1alpha2.OwnerConsumer.String(): + return kubebindv1alpha2.OwnerConsumer, nil + } + if ownerLabel == "" && providerObj == nil { + return kubebindv1alpha2.OwnerConsumer, nil + } + } + return "", fmt.Errorf("unable to determine owner") +} diff --git a/pkg/konnector/controllers/cluster/servicebinding/servicebinding_controller.go b/pkg/konnector/controllers/cluster/servicebinding/servicebinding_controller.go index f41d16c21..f47babab5 100644 --- a/pkg/konnector/controllers/cluster/servicebinding/servicebinding_controller.go +++ b/pkg/konnector/controllers/cluster/servicebinding/servicebinding_controller.go @@ -103,11 +103,8 @@ func NewController( getClusterBinding: func(ctx context.Context) (*kubebindv1alpha2.ClusterBinding, error) { return providerBindClient.KubeBindV1alpha2().ClusterBindings(providerNamespace).Get(ctx, "cluster", metav1.GetOptions{}) }, - getAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - return providerBindClient.KubeBindV1alpha2().APIResourceSchemas().Get(ctx, name, metav1.GetOptions{}) - }, - getBoundAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - return providerBindClient.KubeBindV1alpha2().BoundAPIResourceSchemas(providerNamespace).Get(ctx, name, metav1.GetOptions{}) + getBoundSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) { + return providerBindClient.KubeBindV1alpha2().BoundSchemas(providerNamespace).Get(ctx, name, metav1.GetOptions{}) }, updateServiceExportStatus: func(ctx context.Context, export *kubebindv1alpha2.APIServiceExport) (*kubebindv1alpha2.APIServiceExport, error) { return providerBindClient.KubeBindV1alpha2().APIServiceExports(providerNamespace).UpdateStatus(ctx, export, metav1.UpdateOptions{}) diff --git a/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile.go b/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile.go index 5281003d7..d05339a09 100644 --- a/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile.go +++ b/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile.go @@ -35,12 +35,11 @@ import ( type reconciler struct { consumerSecretRefKey, providerNamespace string - reconcileServiceBinding func(binding *kubebindv1alpha2.APIServiceBinding) bool - getServiceExport func(name string) (*kubebindv1alpha2.APIServiceExport, error) - getServiceBinding func(name string) (*kubebindv1alpha2.APIServiceBinding, error) - getClusterBinding func(ctx context.Context) (*kubebindv1alpha2.ClusterBinding, error) - getAPIResourceSchema func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) - getBoundAPIResourceSchema func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) + reconcileServiceBinding func(binding *kubebindv1alpha2.APIServiceBinding) bool + getServiceExport func(name string) (*kubebindv1alpha2.APIServiceExport, error) + getServiceBinding func(name string) (*kubebindv1alpha2.APIServiceBinding, error) + getClusterBinding func(ctx context.Context) (*kubebindv1alpha2.ClusterBinding, error) + getBoundSchema func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) updateServiceExportStatus func(ctx context.Context, export *kubebindv1alpha2.APIServiceExport) (*kubebindv1alpha2.APIServiceExport, error) @@ -117,14 +116,14 @@ func (r *reconciler) ensureCRDs(ctx context.Context, binding *kubebindv1alpha2.A } // Get all APIResourceSchema objects referenced by the export - schemas, err := r.getAPIResourceSchemasFromExport(ctx, export) + schemas, err := r.getSchemasFromExport(ctx, export) if err != nil { conditions.MarkFalse( binding, kubebindv1alpha2.APIServiceBindingConditionConnected, - "APIResourceSchemaFetchFailed", + "BoundSchemaFetchFailed", conditionsapi.ConditionSeverityError, - "Failed to fetch APIResourceSchema objects: %s", + "Failed to fetch BoundSchema objects: %s", err, ) // We dont have schema - try again. Might be a race on provider side. @@ -137,7 +136,7 @@ func (r *reconciler) ensureCRDs(ctx context.Context, binding *kubebindv1alpha2.A errs = append(errs, err) } - if err := r.ensureCRDsFromAPIResourceSchema(ctx, binding, schema); err != nil { + if err := r.ensureCRDsFromBoundSchema(ctx, binding, schema); err != nil { errs = append(errs, err) } } @@ -152,17 +151,17 @@ func (r *reconciler) ensureCRDs(ctx context.Context, binding *kubebindv1alpha2.A } func (r *reconciler) referenceBoundAPIResourceSchema(ctx context.Context, binding *kubebindv1alpha2.APIServiceBinding, name string) error { - boundSchema, err := r.getBoundAPIResourceSchema(ctx, name) + boundSchema, err := r.getBoundSchema(ctx, name) if err != nil && !errors.IsNotFound(err) { - return fmt.Errorf("failed to get BoundAPIResourceSchema %s: %w", name, err) + return fmt.Errorf("failed to get BoundSchema %s: %w", name, err) } if boundSchema == nil { return nil } - group := boundSchema.Spec.APIResourceSchemaCRDSpec.Group - resource := boundSchema.Spec.APIResourceSchemaCRDSpec.Names.Plural + group := boundSchema.Spec.Group + resource := boundSchema.Spec.Names.Plural if len(binding.Status.BoundSchemas) > 0 { for _, ref := range binding.Status.BoundSchemas { @@ -187,20 +186,9 @@ func (r *reconciler) referenceBoundAPIResourceSchema(ctx context.Context, bindin return nil } -func (r *reconciler) ensureCRDsFromAPIResourceSchema(ctx context.Context, binding *kubebindv1alpha2.APIServiceBinding, schema *kubebindv1alpha2.APIResourceSchema) error { +func (r *reconciler) ensureCRDsFromBoundSchema(ctx context.Context, binding *kubebindv1alpha2.APIServiceBinding, schema *kubebindv1alpha2.BoundSchema) error { var errs []error - crd, err := kubebindhelpers.APIResourceSchemaToCRD(schema) - if err != nil { - conditions.MarkFalse( - binding, - kubebindv1alpha2.APIServiceBindingConditionConnected, - "APIResourceSchemaInvalid", - conditionsapi.ConditionSeverityError, - "APIResourceSchema %s is invalid: %s", - binding.Name, err, - ) - return nil - } + crd := kubebindhelpers.BoundSchemaToCRD(schema) newReference := metav1.OwnerReference{ APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), @@ -275,19 +263,14 @@ func (r *reconciler) ensurePrettyName(ctx context.Context, binding *kubebindv1al return nil } -func (r *reconciler) getAPIResourceSchemasFromExport(ctx context.Context, export *kubebindv1alpha2.APIServiceExport) ([]*kubebindv1alpha2.APIResourceSchema, error) { - schemas := make([]*kubebindv1alpha2.APIResourceSchema, 0, len(export.Spec.Resources)) +// getSchemasFromExport will return all schemas, based on export we are dealing with. +func (r *reconciler) getSchemasFromExport(ctx context.Context, export *kubebindv1alpha2.APIServiceExport) ([]*kubebindv1alpha2.BoundSchema, error) { + schemas := make([]*kubebindv1alpha2.BoundSchema, 0, len(export.Spec.Resources)) for _, ref := range export.Spec.Resources { - if ref.Type != "APIResourceSchema" { - return nil, fmt.Errorf("unsupported resource type %q in APIServiceExport %s", - ref.Type, export.Name) - } - - schema, err := r.getAPIResourceSchema(ctx, ref.Name) + schema, err := r.getBoundSchema(ctx, ref.ResourceGroupName()) if err != nil { - return nil, fmt.Errorf("failed to get APIResourceSchema %s: %w", - ref.Name, err) + return nil, fmt.Errorf("failed to get Schema %s: %w", ref.ResourceGroupName(), err) } schemas = append(schemas, schema) diff --git a/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile_test.go b/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile_test.go index 23b6f30c8..d51cee75e 100644 --- a/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile_test.go +++ b/pkg/konnector/controllers/cluster/servicebinding/servicebinding_reconcile_test.go @@ -34,8 +34,7 @@ func TestEnsureCRDs(t *testing.T) { name string bindingName string getCRD func(name string) (*apiextensionsv1.CustomResourceDefinition, error) - boundSchema *kubebindv1alpha2.BoundAPIResourceSchema - schema *kubebindv1alpha2.APIResourceSchema + boundSchema *kubebindv1alpha2.BoundSchema serviceExport *kubebindv1alpha2.APIServiceExport expectConditions conditionsapi.Conditions wantErr bool @@ -46,10 +45,9 @@ func TestEnsureCRDs(t *testing.T) { getCRD: func(name string) (*apiextensionsv1.CustomResourceDefinition, error) { return nil, errors.NewNotFound(apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions").GroupResource(), name) }, - schema: newAPIResourceSchema("test-schema", "default", "example.com", "tests"), - boundSchema: newBoundAPIResourceSchema("test-schema", "default", "example.com", "tests"), - serviceExport: newServiceExportWithResources("test-binding", "default", []kubebindv1alpha2.APIResourceSchemaReference{ - {Name: "test-schema", Type: "APIResourceSchema"}, + boundSchema: newBoundSchema("tests.example.com", "default", "example.com", "tests"), + serviceExport: newServiceExportWithResources("test-binding", "default", []kubebindv1alpha2.APIServiceExportRequestResource{ + {GroupResource: kubebindv1alpha2.GroupResource{Group: "example.com", Resource: "tests"}}, }), expectConditions: conditionsapi.Conditions{ conditionsapi.Condition{Type: "Connected", Status: "True"}, @@ -60,10 +58,9 @@ func TestEnsureCRDs(t *testing.T) { name: "fail-when-external-crd-present", bindingName: "test-binding", getCRD: newGetCRD("tests.example.com", newCRD("tests.example.com")), - schema: newAPIResourceSchema("test-schema", "default", "example.com", "tests"), - boundSchema: newBoundAPIResourceSchema("test-schema", "default", "example.com", "tests"), - serviceExport: newServiceExportWithResources("test-binding", "default", []kubebindv1alpha2.APIResourceSchemaReference{ - {Name: "test-schema", Type: "APIResourceSchema"}, + boundSchema: newBoundSchema("tests.example.com", "default", "example.com", "tests"), + serviceExport: newServiceExportWithResources("test-binding", "default", []kubebindv1alpha2.APIServiceExportRequestResource{ + {GroupResource: kubebindv1alpha2.GroupResource{Group: "example.com", Resource: "tests"}}, }), expectConditions: conditionsapi.Conditions{ conditionsapi.Condition{ @@ -82,17 +79,11 @@ func TestEnsureCRDs(t *testing.T) { r := &reconciler{ getCRD: tt.getCRD, getServiceExport: newGetServiceExport(tt.serviceExport.Name, tt.serviceExport), - getAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - if name == tt.schema.Name { - return tt.schema, nil - } - return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("apiresourceschemas").GroupResource(), name) - }, - getBoundAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - if name == tt.schema.Name { + getBoundSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) { + if name == tt.boundSchema.Name { return tt.boundSchema, nil } - return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("boundapiresourceschemas").GroupResource(), name) + return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("boundschemas").GroupResource(), name) }, createCRD: func(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition) (*apiextensionsv1.CustomResourceDefinition, error) { return crd.DeepCopy(), nil @@ -116,48 +107,34 @@ func TestEnsureCRDs(t *testing.T) { }) } } -func newBoundAPIResourceSchema(name, namespace string, group, plural string) *kubebindv1alpha2.BoundAPIResourceSchema { - return &kubebindv1alpha2.BoundAPIResourceSchema{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: kubebindv1alpha2.BoundAPIResourceSchemaSpec{ - APIResourceSchemaCRDSpec: kubebindv1alpha2.APIResourceSchemaCRDSpec{ - Group: group, - Names: apiextensionsv1.CustomResourceDefinitionNames{ - Plural: plural, - }, - }, - }, - } -} - -func newAPIResourceSchema(name, namespace, group, plural string) *kubebindv1alpha2.APIResourceSchema { - return &kubebindv1alpha2.APIResourceSchema{ +func newBoundSchema(name, namespace string, group, plural string) *kubebindv1alpha2.BoundSchema { + return &kubebindv1alpha2.BoundSchema{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: kubebindv1alpha2.APIResourceSchemaSpec{ - APIResourceSchemaCRDSpec: kubebindv1alpha2.APIResourceSchemaCRDSpec{ + Spec: kubebindv1alpha2.BoundSchemaSpec{ + InformerScope: kubebindv1alpha2.NamespacedScope, + APICRDSpec: kubebindv1alpha2.APICRDSpec{ Group: group, Names: apiextensionsv1.CustomResourceDefinitionNames{ Plural: plural, }, + Scope: apiextensionsv1.NamespaceScoped, }, }, } } -func newServiceExportWithResources(name, namespace string, resources []kubebindv1alpha2.APIResourceSchemaReference) *kubebindv1alpha2.APIServiceExport { +func newServiceExportWithResources(name, namespace string, resources []kubebindv1alpha2.APIServiceExportRequestResource) *kubebindv1alpha2.APIServiceExport { return &kubebindv1alpha2.APIServiceExport{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Spec: kubebindv1alpha2.APIServiceExportSpec{ - Resources: resources, + Resources: resources, + InformerScope: kubebindv1alpha2.NamespacedScope, }, } } diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_controller.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_controller.go index 75cb35632..b3057eb30 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_controller.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_controller.go @@ -35,6 +35,7 @@ import ( "github.com/kube-bind/kube-bind/pkg/committer" "github.com/kube-bind/kube-bind/pkg/indexers" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/contextstore" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" bindclient "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" @@ -94,25 +95,20 @@ func NewController( consumerConfig: consumerConfig, providerConfig: providerConfig, - syncContext: map[string]syncContext{}, + syncStore: contextstore.New(), - getCRD: func(name string) (*apiextensionsv1.CustomResourceDefinition, error) { - return crdInformer.Lister().Get(name) - }, getServiceBinding: func(name string) (*kubebindv1alpha2.APIServiceBinding, error) { return serviceBindingInformer.Lister().Get(name) }, - getAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - return providerBindClient.KubeBindV1alpha2().APIResourceSchemas().Get(ctx, name, metav1.GetOptions{}) - }, - getBoundAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - return providerBindClient.KubeBindV1alpha2().BoundAPIResourceSchemas(providerNamespace).Get(ctx, name, metav1.GetOptions{}) + getCRD: func(name string) (*apiextensionsv1.CustomResourceDefinition, error) { + return crdInformer.Lister().Get(name) }, - createBoundAPIResourceSchema: func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundAPIResourceSchema) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - return providerBindClient.KubeBindV1alpha2().BoundAPIResourceSchemas(providerNamespace).Create(ctx, boundSchema, metav1.CreateOptions{}) + getRemoteBoundSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) { + return providerBindClient.KubeBindV1alpha2().BoundSchemas(providerNamespace).Get(ctx, name, metav1.GetOptions{}) }, - updateBoundAPIResourceSchema: func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundAPIResourceSchema) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - return providerBindClient.KubeBindV1alpha2().BoundAPIResourceSchemas(providerNamespace).Update(ctx, boundSchema, metav1.UpdateOptions{}) + updateRemoteBoundSchema: func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundSchema) error { + _, err := providerBindClient.KubeBindV1alpha2().BoundSchemas(providerNamespace).UpdateStatus(ctx, boundSchema, metav1.UpdateOptions{}) + return err }, }, @@ -127,6 +123,10 @@ func NewController( indexers.ServiceNamespaceByNamespace: indexers.IndexServiceNamespaceByNamespace, }) + indexers.AddIfNotPresentOrDie(serviceExportInformer.Informer().GetIndexer(), cache.Indexers{ + indexers.ServiceExportByCustomResourceDefinition: indexers.IndexServiceExportByCustomResourceDefinition, + }) + if _, err := serviceExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj any) { c.enqueueServiceExport(logger, obj) @@ -189,15 +189,30 @@ func (c *controller) enqueueServiceBinding(logger klog.Logger, obj any) { } func (c *controller) enqueueCRD(logger klog.Logger, obj any) { - crdKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + name, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) if err != nil { runtime.HandleError(err) return } - key := c.providerNamespace + "/" + crdKey - logger.V(2).Info("queueing APIServiceExport", "key", key, "reason", "APIServiceExport", "APIServiceExportKey", crdKey) - c.queue.Add(key) + exports, err := c.serviceExportIndexer.ByIndex(indexers.ServiceExportByCustomResourceDefinition, name) + if err != nil && !errors.IsNotFound(err) { + runtime.HandleError(err) + return + } else if errors.IsNotFound(err) { + return // skip this secret + } + + for _, obj := range exports { + export := obj.(*kubebindv1alpha2.APIServiceExport) + key, err := cache.MetaNamespaceKeyFunc(export) + if err != nil { + runtime.HandleError(err) + return + } + logger.V(2).Info("queueing APIServiceExport", "key", key, "reason", "CustomResourceDefinition", "name", name) + c.queue.Add(key) + } } // Start starts the controller, which stops when ctx.Done() is closed. @@ -273,7 +288,7 @@ func (c *controller) processNextWorkItem(ctx context.Context) bool { } func (c *controller) process(ctx context.Context, key string) error { - ns, name, err := cache.SplitMetaNamespaceKey(key) + namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { runtime.HandleError(err) return nil // we cannot do anything @@ -281,19 +296,19 @@ func (c *controller) process(ctx context.Context, key string) error { logger := klog.FromContext(ctx) - obj, err := c.serviceExportLister.APIServiceExports(ns).Get(name) + obj, err := c.serviceExportLister.APIServiceExports(namespace).Get(name) if err != nil && !errors.IsNotFound(err) { return err } else if errors.IsNotFound(err) { logger.Error(err, "APIServiceExport disappeared") - return c.reconcile(ctx, name, nil) + return c.reconcile(ctx, namespace, name, nil) } old := obj obj = obj.DeepCopy() var errs []error - if err := c.reconcile(ctx, name, obj); err != nil { + if err := c.reconcile(ctx, namespace, name, obj); err != nil { errs = append(errs, err) } diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go index a625490f7..43f30542c 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go @@ -19,9 +19,9 @@ package serviceexport import ( "context" "fmt" - "reflect" + "maps" + "slices" "strings" - "sync" "time" corev1 "k8s.io/api/core/v1" @@ -36,9 +36,11 @@ import ( "k8s.io/client-go/rest" "k8s.io/klog/v2" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/claimedresources" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/multinsinformer" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/spec" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/status" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/contextstore" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" @@ -54,26 +56,19 @@ type reconciler struct { consumerConfig, providerConfig *rest.Config - lock sync.Mutex - syncContext map[string]syncContext // by CRD name + syncStore contextstore.Store // by APIServiceExport name. This includes same ctx for resourc claims and crds. - getCRD func(name string) (*apiextensionsv1.CustomResourceDefinition, error) - getServiceBinding func(name string) (*kubebindv1alpha2.APIServiceBinding, error) - getAPIResourceSchema func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) - getBoundAPIResourceSchema func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - createBoundAPIResourceSchema func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundAPIResourceSchema) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - updateBoundAPIResourceSchema func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundAPIResourceSchema) (*kubebindv1alpha2.BoundAPIResourceSchema, error) -} + getCRD func(name string) (*apiextensionsv1.CustomResourceDefinition, error) + getServiceBinding func(name string) (*kubebindv1alpha2.APIServiceBinding, error) + getRemoteBoundSchema func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) -type syncContext struct { - generation int64 - cancel func() + updateRemoteBoundSchema func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundSchema) error } -func (r *reconciler) reconcile(ctx context.Context, name string, export *kubebindv1alpha2.APIServiceExport) error { +func (r *reconciler) reconcile(ctx context.Context, namespace, name string, export *kubebindv1alpha2.APIServiceExport) error { errs := []error{} - if err := r.ensureControllers(ctx, name, export); err != nil { + if err := r.ensureControllers(ctx, namespace, name, export); err != nil { errs = append(errs, err) } @@ -89,22 +84,17 @@ func (r *reconciler) reconcile(ctx context.Context, name string, export *kubebin return utilerrors.NewAggregate(errs) } -func (r *reconciler) ensureControllers(ctx context.Context, name string, export *kubebindv1alpha2.APIServiceExport) error { +func (r *reconciler) ensureControllers(ctx context.Context, namespace, name string, export *kubebindv1alpha2.APIServiceExport) error { logger := klog.FromContext(ctx) + exportKey := contextstore.Key(namespace + "." + name) // Key for the export if export == nil { - // stop dangling syncers on delete - r.lock.Lock() - defer r.lock.Unlock() - // Clean up any controllers associated with this export - for key, c := range r.syncContext { - if strings.HasSuffix(key, "."+name) { - logger.V(1).Info("Stopping APIServiceExport sync", "key", key, "reason", "APIServiceExport deleted") - c.cancel() - delete(r.syncContext, key) - } + deleted := r.syncStore.BulkDeletePrefixed(exportKey) + for _, k := range deleted { + logger.V(1).Info("Stopping APIServiceExport sync", "key", k.Key(), "reason", "NoAPIServiceExport") } + return nil } @@ -115,14 +105,9 @@ func (r *reconciler) ensureControllers(ctx context.Context, name string, export } if binding == nil { // Stop all controllers for this export - r.lock.Lock() - defer r.lock.Unlock() - for key, c := range r.syncContext { - if strings.HasSuffix(key, "."+export.Name) { - logger.V(1).Info("Stopping APIServiceExport sync", "key", key, "reason", "NoAPIServiceBinding") - c.cancel() - delete(r.syncContext, key) - } + deleted := r.syncStore.BulkDeletePrefixed(exportKey) + for _, k := range deleted { + logger.V(1).Info("Stopping APIServiceExport sync", "key", k.Key(), "reason", "NoAPIServiceBinding") } return nil } @@ -130,140 +115,74 @@ func (r *reconciler) ensureControllers(ctx context.Context, name string, export // Process each resource referenced by the export var errs []error processedSchemas := make(map[string]bool) + var isClusterScoped bool - for _, resourceRef := range export.Spec.Resources { - if resourceRef.Type != "APIResourceSchema" { - logger.V(1).Info("Skipping unsupported resource type", "type", resourceRef.Type) - continue - } - + for _, res := range export.Spec.Resources { + name := res.Resource + "." + res.Group // Fetch the APIResourceSchema - schema, err := r.getAPIResourceSchema(ctx, resourceRef.Name) + schema, err := r.getRemoteBoundSchema(ctx, name) if err != nil { if errors.IsNotFound(err) { - // Stop the controller for this schema if it exists - r.lock.Lock() - key := resourceRef.Name + "." + export.Name - if c, found := r.syncContext[key]; found { - logger.V(1).Info("Stopping APIServiceExport resource sync", "key", key, "reason", "APIResourceSchema not found") - c.cancel() - delete(r.syncContext, key) + // Stop the controller for this schema if it does not exists. + key := contextstore.NewKey(namespace, name, name) + deleted := r.syncStore.BulkDeletePrefixed(key) + for _, k := range deleted { + logger.V(1).Info("Stopping APIServiceExport sync", "key", k.Key(), "reason", "BoundSchema not found") } - r.lock.Unlock() continue } errs = append(errs, err) continue } - // Ensure BoundAPIResourceSchema exists for tracking status - if err := r.ensureBoundAPIResourceSchema(ctx, export, schema); err != nil { - errs = append(errs, err) - continue - } - // Start/update controller for this schema if err := r.ensureControllerForSchema(ctx, export, schema); err != nil { errs = append(errs, err) } - processedSchemas[resourceRef.Name] = true + processedSchemas[name] = true // This is only schemas names (suffix) + isClusterScoped = schema.Spec.Scope == apiextensionsv1.ClusterScoped || schema.Spec.InformerScope == kubebindv1alpha2.ClusterScope } - // Stop controllers for schemas that are no longer referenced - r.lock.Lock() - for key, c := range r.syncContext { - parts := strings.Split(key, ".") - if len(parts) == 2 && parts[1] == export.Name { - schemaName := parts[0] - if !processedSchemas[schemaName] { - logger.V(1).Info("Stopping APIServiceExport resource sync", "key", key, "reason", "Schema no longer referenced") - c.cancel() - delete(r.syncContext, key) - } - } + // Ensure controller for permission claims + if err := r.ensureControllersForPermissionClaims(ctx, export, binding, isClusterScoped); err != nil { + errs = append(errs, err) } - r.lock.Unlock() - return utilerrors.NewAggregate(errs) -} - -func (r *reconciler) ensureBoundAPIResourceSchema(ctx context.Context, export *kubebindv1alpha2.APIServiceExport, schema *kubebindv1alpha2.APIResourceSchema) error { - boundSchema, err := r.getBoundAPIResourceSchema(ctx, schema.Name) - if err != nil { - if errors.IsNotFound(err) { - // Create new BoundAPIResourceSchema - boundSchema = &kubebindv1alpha2.BoundAPIResourceSchema{ - ObjectMeta: metav1.ObjectMeta{ - Name: schema.Name, - Namespace: export.Namespace, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), - Kind: "APIServiceExport", - Name: export.Name, - UID: export.UID, - Controller: func() *bool { b := true; return &b }(), - }, - }, - }, - Spec: kubebindv1alpha2.BoundAPIResourceSchemaSpec{ - InformerScope: schema.Spec.InformerScope, - APIResourceSchemaCRDSpec: schema.Spec.APIResourceSchemaCRDSpec, - }, - } - - conditions.MarkFalse( - boundSchema, - kubebindv1alpha2.BoundAPIResourceSchemaValid, - string(kubebindv1alpha2.BoundAPIResourceSchemaPending), - conditionsapi.ConditionSeverityInfo, - "Waiting for CRD to be created in consumer cluster", - ) - - _, err := r.createBoundAPIResourceSchema(ctx, boundSchema) - if err != nil { - return fmt.Errorf("failed to create BoundAPIResourceSchema %s: %w", boundSchema.Name, err) - } - return nil + // Stop controllers for schemas that are no longer referenced. + // This will be `exportNamespace.exportName.` + contexts := r.syncStore.ListPrefixed(exportKey) + for _, c := range contexts { + schemaName := strings.TrimPrefix(string(c.Key()), string(exportKey)+".") // schemaName will be processed schema names. + // Skip permission claim controllers - they are handled separately in ensureControllersForPermissionClaims + if strings.HasPrefix(schemaName, "claim.") { + continue + } + if !processedSchemas[schemaName] { + logger.V(1).Info("Stopping APIServiceExport resource sync", "key", c.Key(), "reason", "Schema no longer referenced") + r.syncStore.Delete(c.Key()) } - return err - } - - // Check if InformerScope needs updating - if boundSchema.Spec.InformerScope != schema.Spec.InformerScope { - boundSchema.Spec.InformerScope = schema.Spec.InformerScope - } - - if !reflect.DeepEqual(boundSchema.Spec.APIResourceSchemaCRDSpec, schema.Spec.APIResourceSchemaCRDSpec) { - boundSchema.Spec.APIResourceSchemaCRDSpec = schema.Spec.APIResourceSchemaCRDSpec - } - _, err = r.updateBoundAPIResourceSchema(ctx, boundSchema) - if err != nil { - return fmt.Errorf("failed to update BoundAPIResourceSchema %s: %w", boundSchema.Name, err) } - return nil + return utilerrors.NewAggregate(errs) } -func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kubebindv1alpha2.APIServiceExport, schema *kubebindv1alpha2.APIResourceSchema) error { +func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kubebindv1alpha2.APIServiceExport, schema *kubebindv1alpha2.BoundSchema) error { logger := klog.FromContext(ctx) - key := schema.Name + "." + export.Name + key := contextstore.NewKey(export.Namespace, export.Name, schema.Name) - r.lock.Lock() - c, found := r.syncContext[key] + c, found := r.syncStore.Get(key) if found { - if c.generation == export.Generation { - r.lock.Unlock() + if c.Generation == export.Generation { return nil // all as expected } logger.V(1).Info("Stopping APIServiceExport resource sync", "key", key, "reason", "GenerationChanged", "generation", schema.Generation) - c.cancel() - delete(r.syncContext, key) + r.syncStore.Delete(key) } - r.lock.Unlock() + + // At this point we dont have a controller for this schema. // start a new syncer var syncVersion string @@ -274,7 +193,7 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube } } if syncVersion == "" { - return fmt.Errorf("no served version found for APIResourceSchema %s", schema.Name) + return fmt.Errorf("no served version found for BoundSchema %s", schema.Name) } gvr := runtimeschema.GroupVersionResource{ @@ -321,6 +240,7 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube } specCtrl, err := spec.NewController( + export, // pass the export to establish owner references on ServiceNamespace creation gvr, r.providerNamespace, providerNamespaceUID, @@ -359,25 +279,178 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube go func() { // to not block the main thread consumerSynced := consumerInf.WaitForCacheSync(ctxWithCancel.Done()) - logger.V(2).Info("Synced informers", "key", key, "consumer", consumerSynced) + logger.V(2).Info("Synced informers", "key", key, "consumer", slices.Collect(maps.Keys(consumerSynced))) providerSynced := providerInf.WaitForCacheSync(ctxWithCancel.Done()) - logger.V(2).Info("Synced informers", "key", key, "provider", providerSynced) + logger.V(2).Info("Synced informers", "key", key, "provider", slices.Collect(maps.Keys(providerSynced))) go specCtrl.Start(ctxWithCancel, 1) go statusCtrl.Start(ctxWithCancel, 1) }() - r.lock.Lock() - defer r.lock.Unlock() - if c, found := r.syncContext[key]; found { - c.cancel() + r.syncStore.Set(key, contextstore.SyncContext{ + Generation: schema.Generation, + Cancel: cancel, + }) + + return nil +} + +func (r *reconciler) ensureControllersForPermissionClaims( + ctx context.Context, + export *kubebindv1alpha2.APIServiceExport, + binding *kubebindv1alpha2.APIServiceBinding, + isClusterScoped bool, // schema.Spec.Scope == apiextensionsv1.ClusterScoped || schema.Spec.InformerScope == kubebindv1alpha2.ClusterScope +) error { + logger := klog.FromContext(ctx) + + // Track processed claims for cleanup + processedClaims := make(map[string]bool) + var errs []error + + // Process each permission claim + for _, claim := range binding.Spec.PermissionClaims { + claimGVR, err := kubebindv1alpha2.ResolveClaimableAPI(claim) + if err != nil { + logger.Info("skipping unsupported claim", "claim", claim, "error", err) + continue + } + + // Create unique key for this claim controller + claimKey := contextstore.NewKey(export.Namespace, export.Name, "claim", claimGVR.String()) + processedClaims[claimKey.String()] = true + + // Check if controller already exists with correct generation + c, found := r.syncStore.Get(claimKey) + if found { + if c.Generation == binding.Generation { + continue // controller is up to date + } + // Generation changed, stop old controller + logger.V(1).Info("Stopping permission claim controller", "key", claimKey, "reason", "GenerationChanged") + r.syncStore.Delete(claimKey) + } + + // Start new controller for this claim + if err := r.ensureControllerForPermissionClaim(ctx, binding, claim, claimGVR, isClusterScoped, claimKey); err != nil { + errs = append(errs, err) + } + } + + // Cleanup controllers for claims that are no longer present + claimPrefix := contextstore.NewKey(export.Namespace, export.Name, "claim") + contexts := r.syncStore.ListPrefixed(claimPrefix) + for _, c := range contexts { + if !processedClaims[c.Key().String()] { + logger.V(1).Info("Stopping permission claim controller", "key", c.Key(), "reason", "Claim no longer present") + r.syncStore.Delete(c.Key()) + } + } + + return utilerrors.NewAggregate(errs) +} + +func (r *reconciler) ensureControllerForPermissionClaim( + ctx context.Context, + binding *kubebindv1alpha2.APIServiceBinding, + claim kubebindv1alpha2.PermissionClaim, + claimGVR kubebindv1alpha2.GroupVersionResource, + isClusterScoped bool, + claimKey contextstore.Key, +) error { + logger := klog.FromContext(ctx) + + ctxWithCancel, cancel := context.WithCancel(ctx) + + dynamicProviderClient := dynamicclient.NewForConfigOrDie(r.providerConfig) + dynamicConsumerClient := dynamicclient.NewForConfigOrDie(r.consumerConfig) + + // Define a function to tweak list options based on the claim's LabelSelector. + tweakListOptions := func(options *metav1.ListOptions) { + if claim.Selector.LabelSelector != nil { + // Convert the LabelSelector struct to a string that the API server understands. + selector, err := metav1.LabelSelectorAsSelector(claim.Selector.LabelSelector) + if err == nil { + options.LabelSelector = selector.String() + logger.V(2).Info("Applying label selector to informer", "selector", options.LabelSelector, "resource", claim.Resource) + } else { + logger.Error(err, "failed to parse label selector", "selector", claim.Selector.LabelSelector) + } + } + } + + // Create consumer informer factory + var defaultConsumerInf dynamicinformer.DynamicSharedInformerFactory + if claim.Selector.LabelSelector != nil { + defaultConsumerInf = dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicConsumerClient, time.Minute*30, metav1.NamespaceAll, tweakListOptions) + } else { + defaultConsumerInf = dynamicinformer.NewDynamicSharedInformerFactory(dynamicConsumerClient, time.Minute*30) + } + + var providerInf multinsinformer.GetterInformer + if isClusterScoped { + factory := dynamicinformer.NewDynamicSharedInformerFactory(dynamicProviderClient, time.Minute*30) + factory.ForResource(claimGVR.GetSchemaGroupVersionResource()).Lister() // wire the GVR up in the informer factory + providerInf = multinsinformer.GetterInformerWrapper{ + GVR: claimGVR.GetSchemaGroupVersionResource(), + Delegate: factory, + } + } else { + var err error + providerInf, err = multinsinformer.NewDynamicMultiNamespaceInformer( + claimGVR.GetSchemaGroupVersionResource(), + r.providerNamespace, + r.providerConfig, + r.serviceNamespaceInformer, + ) + if err != nil { + logger.Info("aborting", "error", err) + cancel() + return err + } } - r.syncContext[key] = syncContext{ - generation: schema.Generation, - cancel: cancel, + + claimedCtrl, err := claimedresources.NewController( + claimGVR.GetSchemaGroupVersionResource(), + claim, + r.providerNamespace, + r.consumerConfig, + r.providerConfig, + defaultConsumerInf.ForResource(claimGVR.GetSchemaGroupVersionResource()), + providerInf, + r.serviceNamespaceInformer, + ) + + if err != nil { + cancel() + return err } + logger.Info("creating claim reconciler", "gvr", claimGVR, "key", claimKey) + + // Start the informers and controllers in a goroutine + go func() { + defaultConsumerInf.Start(ctxWithCancel.Done()) + + // Wait for consumer informers to sync + consumerSynced := defaultConsumerInf.WaitForCacheSync(ctxWithCancel.Done()) + logger.V(2).Info("Synced consumer informers", "consumer", slices.Collect(maps.Keys(consumerSynced)), "key", claimKey) + + // Start provider informer and wait for sync + providerInf.Start(ctxWithCancel) + providerSynced := providerInf.WaitForCacheSync(ctxWithCancel.Done()) + logger.V(2).Info("Synced provider informers", "provider", slices.Collect(maps.Keys(providerSynced)), "key", claimKey) + + // Start the claimed resources controller + claimedCtrl.Start(ctxWithCancel, 1) + }() + + // Store the controller context for tracking + r.syncStore.Set(claimKey, contextstore.SyncContext{ + Generation: binding.Generation, + Cancel: cancel, + }) + return nil } @@ -387,8 +460,9 @@ func (r *reconciler) ensureCRDConditionsCopiedToBoundSchema(ctx context.Context, } var errs []error allValid := true // assume all BoundAPIResourceSchemas are valid - for _, resourceRef := range export.Spec.Resources { - schema, err := r.getAPIResourceSchema(ctx, resourceRef.Name) + for _, res := range export.Spec.Resources { + name := res.Resource + "." + res.Group + boundSchema, err := r.getRemoteBoundSchema(ctx, name) if err != nil { if errors.IsNotFound(err) { continue @@ -397,7 +471,7 @@ func (r *reconciler) ensureCRDConditionsCopiedToBoundSchema(ctx context.Context, continue } - crd, err := r.getCRD(schema.Name) + crd, err := r.getCRD(boundSchema.Name) if err != nil { if errors.IsNotFound(err) { continue @@ -406,15 +480,6 @@ func (r *reconciler) ensureCRDConditionsCopiedToBoundSchema(ctx context.Context, continue } - boundSchema, err := r.getBoundAPIResourceSchema(ctx, schema.Name) - if err != nil { - if errors.IsNotFound(err) { - continue // BoundAPIResourceSchema not found, nothing to update - } - errs = append(errs, err) - continue - } - boundSchemaIndex := map[conditionsapi.ConditionType]int{} for i, c := range boundSchema.Status.Conditions { boundSchemaIndex[c.Type] = i @@ -449,13 +514,13 @@ func (r *reconciler) ensureCRDConditionsCopiedToBoundSchema(ctx context.Context, boundSchema.Status.AcceptedNames = crd.Status.AcceptedNames boundSchema.Status.StoredVersions = crd.Status.StoredVersions - if _, err := r.updateBoundAPIResourceSchema(ctx, boundSchema); err != nil { + if err := r.updateRemoteBoundSchema(ctx, boundSchema); err != nil { errs = append(errs, err) - allValid = false // at least one BoundAPIResourceSchema is not valid + allValid = false // at least one BoundSchemas is not valid } } - // Set APIServiceExport Ready condition based on all BoundAPIResourceSchemas + // Set APIServiceExport Ready condition based on all BoundSchemas if allValid { conditions.MarkTrue( export, @@ -465,9 +530,9 @@ func (r *reconciler) ensureCRDConditionsCopiedToBoundSchema(ctx context.Context, conditions.MarkFalse( export, kubebindv1alpha2.APIServiceExportConditionConnected, - "BoundAPIResourceSchemasNotValid", + "BoundSchemasNotValid", conditionsapi.ConditionSeverityWarning, - "One or more BoundAPIResourceSchemas are not valid", + "One or more BoundSchemas are not valid", ) } diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile_test.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile_test.go index 7aee3b3df..bbf1c2efb 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile_test.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile_test.go @@ -34,10 +34,9 @@ func TestEnsureCRDConditionsCopiedToBoundSchema(t *testing.T) { tests := []struct { name string getCRD func(name string) (*apiextensionsv1.CustomResourceDefinition, error) - schema *kubebindv1alpha2.APIResourceSchema - boundSchema *kubebindv1alpha2.BoundAPIResourceSchema + boundSchema *kubebindv1alpha2.BoundSchema export *kubebindv1alpha2.APIServiceExport - expected *kubebindv1alpha2.BoundAPIResourceSchema + expected *kubebindv1alpha2.BoundSchema wantErr bool }{ { @@ -46,16 +45,15 @@ func TestEnsureCRDConditionsCopiedToBoundSchema(t *testing.T) { {Type: "Something", Status: "True", Reason: "Reason", Message: "message"}, {Type: "Established", Status: "True", Reason: "Reason", Message: "message"}, })), - schema: newAPIResourceSchema("foo-schema", "default", "example.com", "foos"), - boundSchema: newBoundAPIResourceSchema("foo-schema", []conditionsapi.Condition{ + boundSchema: newBoundSchema("foo-schema", []conditionsapi.Condition{ {Type: "Ready", Status: "False", Severity: "Warning", Reason: "SomethingElseWrong", Message: "something else went wrong"}, {Type: "Established", Status: "True", Severity: "None", Reason: "Reason", Message: "message"}, {Type: "Structural", Status: "False", Severity: "Warning", Reason: "SomethingWrong", Message: "something went wrong"}, }), - export: newExportWithResources("test-export", "default", []kubebindv1alpha2.APIResourceSchemaReference{ - {Name: "foo-schema", Type: "APIResourceSchema"}, + export: newExportWithResources("test-export", "default", []kubebindv1alpha2.APIServiceExportRequestResource{ + {GroupResource: kubebindv1alpha2.GroupResource{Group: "example.com", Resource: "foos"}}, }), - expected: newBoundAPIResourceSchema("foo-schema", []conditionsapi.Condition{ + expected: newBoundSchema("foo-schema", []conditionsapi.Condition{ {Type: "Ready", Status: "False", Severity: "Warning", Reason: "SomethingWrong", Message: "something went wrong"}, {Type: "Established", Status: "True", Severity: "", Reason: "Reason", Message: "message"}, {Type: "Something", Status: "True", Severity: "", Reason: "Reason", Message: "message"}, @@ -67,13 +65,12 @@ func TestEnsureCRDConditionsCopiedToBoundSchema(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Track updated schema - var updatedSchema *kubebindv1alpha2.BoundAPIResourceSchema + var updatedSchema *kubebindv1alpha2.BoundSchema ctx := context.Background() r := &reconciler{ - getCRD: tt.getCRD, - getAPIResourceSchema: newGetAPIResourceSchema(ctx, tt.schema), - getBoundAPIResourceSchema: newGetBoundAPIResourceSchema(ctx, tt.boundSchema), - updateBoundAPIResourceSchema: newUpdateBoundAPIResourceSchema(&updatedSchema), + getCRD: tt.getCRD, + getRemoteBoundSchema: newGetBoundSchema(ctx, tt.boundSchema), + updateRemoteBoundSchema: newUpdateBoundSchema(&updatedSchema), } if err := r.ensureCRDConditionsCopiedToBoundSchema(context.Background(), tt.export); (err != nil) != tt.wantErr { @@ -113,66 +110,41 @@ func newCRD(name string, conditions []apiextensionsv1.CustomResourceDefinitionCo } } -func newAPIResourceSchema(name, namespace, group, plural string) *kubebindv1alpha2.APIResourceSchema { - return &kubebindv1alpha2.APIResourceSchema{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: kubebindv1alpha2.APIResourceSchemaSpec{ - APIResourceSchemaCRDSpec: kubebindv1alpha2.APIResourceSchemaCRDSpec{ - Group: group, - Names: apiextensionsv1.CustomResourceDefinitionNames{ - Plural: plural, - }, - }, - }, - } -} - -func newGetAPIResourceSchema(_ context.Context, schema *kubebindv1alpha2.APIResourceSchema) func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - return func(_ context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) { - if name == schema.Name { - return schema, nil - } - return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("apiresourceschemas").GroupResource(), name) - } -} - -func newGetBoundAPIResourceSchema(_ context.Context, boundSchema *kubebindv1alpha2.BoundAPIResourceSchema) func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - return func(ctx context.Context, name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { +func newGetBoundSchema(_ context.Context, boundSchema *kubebindv1alpha2.BoundSchema) func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) { + return func(ctx context.Context, name string) (*kubebindv1alpha2.BoundSchema, error) { if name == boundSchema.Name { return boundSchema, nil } - return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("boundapiresourceschemas").GroupResource(), name) + return nil, errors.NewNotFound(kubebindv1alpha2.SchemeGroupVersion.WithResource("boundschemas").GroupResource(), name) } } -func newUpdateBoundAPIResourceSchema(updatedSchemaPtr **kubebindv1alpha2.BoundAPIResourceSchema) func(context.Context, *kubebindv1alpha2.BoundAPIResourceSchema) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { - return func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundAPIResourceSchema) (*kubebindv1alpha2.BoundAPIResourceSchema, error) { +func newUpdateBoundSchema(updatedSchemaPtr **kubebindv1alpha2.BoundSchema) func(context.Context, *kubebindv1alpha2.BoundSchema) error { + return func(ctx context.Context, boundSchema *kubebindv1alpha2.BoundSchema) error { *updatedSchemaPtr = boundSchema.DeepCopy() - return boundSchema, nil + return nil } } -func newExportWithResources(name, namespace string, resources []kubebindv1alpha2.APIResourceSchemaReference) *kubebindv1alpha2.APIServiceExport { +func newExportWithResources(name, namespace string, resources []kubebindv1alpha2.APIServiceExportRequestResource) *kubebindv1alpha2.APIServiceExport { return &kubebindv1alpha2.APIServiceExport{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Spec: kubebindv1alpha2.APIServiceExportSpec{ - Resources: resources, + Resources: resources, + InformerScope: kubebindv1alpha2.NamespacedScope, }, } } -func newBoundAPIResourceSchema(name string, conditions []conditionsapi.Condition) *kubebindv1alpha2.BoundAPIResourceSchema { - return &kubebindv1alpha2.BoundAPIResourceSchema{ +func newBoundSchema(name string, conditions []conditionsapi.Condition) *kubebindv1alpha2.BoundSchema { + return &kubebindv1alpha2.BoundSchema{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, - Status: kubebindv1alpha2.BoundAPIResourceSchemaStatus{ + Status: kubebindv1alpha2.BoundSchemaStatus{ Conditions: conditions, }, } diff --git a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go index 884e1ec4b..db794909b 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go +++ b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go @@ -55,6 +55,7 @@ const ( // NewController returns a new controller reconciling downstream objects to upstream. func NewController( + apiServiceExport *kubebindv1alpha2.APIServiceExport, // used to establish owner references when create happens from the consumer side. gvr schema.GroupVersionResource, providerNamespace string, providerNamespaceUID string, @@ -99,6 +100,8 @@ func NewController( reconciler: reconciler{ providerNamespace: providerNamespace, + apiServiceExport: apiServiceExport, + getServiceNamespace: func(name string) (*kubebindv1alpha2.APIServiceNamespace, error) { return serviceNamespaceInformer.Lister().APIServiceNamespaces(providerNamespace).Get(name) }, diff --git a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go index 0eae960ad..19da4d205 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go @@ -33,6 +33,7 @@ import ( type reconciler struct { providerNamespace string + apiServiceExport *kubebindv1alpha2.APIServiceExport // used to establish owner references when create happens from the consumer side. getServiceNamespace func(name string) (*kubebindv1alpha2.APIServiceNamespace, error) createServiceNamespace func(ctx context.Context, sn *kubebindv1alpha2.APIServiceNamespace) (*kubebindv1alpha2.APIServiceNamespace, error) @@ -62,6 +63,9 @@ func (r *reconciler) reconcile(ctx context.Context, obj *unstructured.Unstructur ObjectMeta: metav1.ObjectMeta{ Name: ns, Namespace: r.providerNamespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(r.apiServiceExport, kubebindv1alpha2.SchemeGroupVersion.WithKind("APIServiceExport")), + }, }, }) if err != nil { diff --git a/pkg/konnector/controllers/contextstore/contextstore.go b/pkg/konnector/controllers/contextstore/contextstore.go new file mode 100644 index 000000000..c35141c6d --- /dev/null +++ b/pkg/konnector/controllers/contextstore/contextstore.go @@ -0,0 +1,125 @@ +/* +Copyright 2022 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package contextstore + +// contextstore allows to manage and track context per controllers, stored at the different levels of the controller hierarchy: +// APIServiceExport - schemas and permissionClaims - each schema runs its own gvr controller and each permissionClaim runs its own controller. +// Context are stored at the APIServiceExport level. + +import ( + "strings" + "sync" +) + +type Key string + +func (k Key) String() string { + return string(k) +} + +func NewKey(exportNamespace, exportName string, suffix ...string) Key { + return Key(exportNamespace + "." + exportName + "." + strings.Join(suffix, ".")) +} + +type Store interface { + Get(key Key) (SyncContext, bool) + ListPrefixed(prefix Key) []SyncContext + Set(key Key, value SyncContext) + Delete(key Key) + BulkDeletePrefixed(prefix Key) []SyncContext +} + +type contextStore struct { + lock sync.Mutex + store map[Key]SyncContext +} + +type SyncContext struct { + key Key // reference key for the context for logging + Generation int64 + Cancel func() +} + +func New() Store { + return &contextStore{ + store: make(map[Key]SyncContext), + } +} + +func (c *SyncContext) Key() Key { + return c.key +} + +func (c *contextStore) Get(key Key) (SyncContext, bool) { + c.lock.Lock() + defer c.lock.Unlock() + val, ok := c.store[key] + return val, ok +} + +func (c *contextStore) ListPrefixed(prefix Key) []SyncContext { + c.lock.Lock() + defer c.lock.Unlock() + + var results []SyncContext + for k, v := range c.store { + if strings.HasPrefix(k.String(), prefix.String()) { + results = append(results, v) + } + } + return results +} + +func (c *contextStore) Set(key Key, value SyncContext) { + c.lock.Lock() + defer c.lock.Unlock() + value.key = key + c.store[key] = value +} + +func (c *contextStore) Delete(key Key) { + c.lock.Lock() + defer c.lock.Unlock() + ctx, ok := c.store[key] + if ok { + ctx.Cancel() + } + delete(c.store, key) +} + +func (c *contextStore) BulkDeletePrefixed(prefix Key) []SyncContext { + c.lock.Lock() + defer c.lock.Unlock() + + var deleted []SyncContext + var keysToDelete []Key + + for k, v := range c.store { + if strings.HasPrefix(k.String(), prefix.String()) { + keysToDelete = append(keysToDelete, k) + deleted = append(deleted, v) + } + } + + for _, k := range keysToDelete { + ctx := c.store[k] + ctx.Cancel() + delete(c.store, k) + } + + return deleted +} diff --git a/scripts/run-frontend.sh b/scripts/run-frontend.sh new file mode 100755 index 000000000..71963bb50 --- /dev/null +++ b/scripts/run-frontend.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Build frontend script for kube-bind +# This script builds the Vue.js frontend and embeds it into the Go binary + +set -e + +echo "Building frontend..." + +# Navigate to web directory +cd web + +# Install dependencies if node_modules doesn't exist +if [ ! -d "node_modules" ]; then + echo "Installing frontend dependencies..." + npm install +fi + +# Build the frontend +echo "Running dev application..." +npm run dev diff --git a/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go b/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go index 97cfc0450..9d06976b0 100644 --- a/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go @@ -88,6 +88,10 @@ type APIServiceBindingSpec struct { // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="kubeconfigSecretRef is immutable" KubeconfigSecretRef ClusterSecretKeyRef `json:"kubeconfigSecretRef"` + + // PermissionClaims records decisions about permission claims requested by the service provider. + // Access is granted per GroupResource. + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` } type APIServiceBindingStatus struct { diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go index 2be34e89e..50dabf91a 100644 --- a/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha2 import ( - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" @@ -38,6 +37,9 @@ const ( // APIServiceExportConditionConsumerInSync is set to true when the APIServiceExport's // schema is applied to the consumer cluster. APIServiceExportConditionConsumerInSync conditionsapi.ConditionType = "ConsumerInSync" + + // APIServiceExportConditionPermissionClaim describes status of the permission claim, requested in the APIServiceExport and APIServiceExportRequest. + APIServiceExportConditionPermissionClaim conditionsapi.ConditionType = "PermissionClaim" ) // APIServiceExport specifies the resource to be exported. It is mostly a CRD: @@ -73,11 +75,23 @@ func (in *APIServiceExport) SetConditions(conditions conditionsapi.Conditions) { in.Status.Conditions = conditions } +// APIServiceExportResource is a resource that represents an APIServiceExport. +type APIServiceExportResource = APIServiceExportRequestResource + // APIServiceExportSpec defines the desired state of APIServiceExport. type APIServiceExportSpec struct { - // resources specifies the API resources to export + // resources is a list of resources that should be exported. + // // +required - Resources []APIResourceSchemaReference `json:"resources"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resources are immutable" + Resources []APIServiceExportResource `json:"resources"` + + // PermissionClaims records decisions about permission claims requested by the service provider. + // Access is granted per GroupResource. + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` + // informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. // // Cluster: The konnector has permission to watch all namespaces at once and cluster-scoped resources. @@ -95,20 +109,6 @@ type APIServiceExportSpec struct { ClusterScopedIsolation Isolation `json:"clusterScopedIsolation,omitempty"` } -// APIResourceSchemaReference is a list of references to APIResourceSchemas. -type APIResourceSchemaReference struct { - // Name is the name of the resource to export - // +required - // +kubebuilder:validation:Required - Name string `json:"name"` - - // Type of the resource to export - // Currently only APIResourceSchema is supported - // +kubebuilder:validation:Enum=APIResourceSchema - // +required - Type string `json:"type"` -} - // Isolation is an enum defining the different ways to isolate cluster scoped objects // // +kubebuilder:validation:Enum=Prefixed;Namespaced;None @@ -128,20 +128,6 @@ const ( // APIServiceExportStatus stores status information about a APIServiceExport. It // reflects the status of the CRD of the consumer cluster. type APIServiceExportStatus struct { - // acceptedNames are the names that are actually being used to serve discovery. - // They may be different than the names in spec. - // +optional - AcceptedNames apiextensionsv1.CustomResourceDefinitionNames `json:"acceptedNames"` - - // storedVersions lists all versions of CustomResources that were ever persisted. Tracking these - // versions allows a migration path for stored versions in etcd. The field is mutable - // so a migration controller can finish a migration to another version (ensuring - // no old objects are left in storage), and then remove the rest of the - // versions from this list. - // Versions may not be removed from `spec.versions` while they exist in this list. - // +optional - StoredVersions []string `json:"storedVersions"` - // conditions is a list of conditions that apply to the APIServiceExport. It is // updated by the konnector on the consumer cluster. Conditions conditionsapi.Conditions `json:"conditions,omitempty"` diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go index 302dff235..d5a658cef 100644 --- a/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go @@ -17,8 +17,11 @@ limitations under the License. package v1alpha2 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" ) @@ -104,6 +107,10 @@ type APIServiceExportRequestSpec struct { // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resources are immutable" Resources []APIServiceExportRequestResource `json:"resources"` + + // PermissionClaims records decisions about permission claims requested by the service provider. + // Access is granted per GroupResource. + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` } type APIServiceExportRequestResource struct { @@ -114,6 +121,56 @@ type APIServiceExportRequestResource struct { Versions []string `json:"versions,omitempty"` } +// ResourceGroupName returns the group name of the resource. +// +// Important: If you change this, change one for BoundSchema too. +func (r APIServiceExportRequestResource) ResourceGroupName() string { + return fmt.Sprintf("%s.%s", r.Resource, r.Group) +} + +// permissionClaim selects objects of a GVR that a service provider may +// request and that a consumer may accept and allow the service provider access to. +type PermissionClaim struct { + GroupResource `json:",inline"` + + // Selector is a resource selector that selects objects of a GVR. + Selector Selector `json:"selector,omitempty"` +} + +// Owner is the owner of the resource. +type Owner string + +const ( + // OwnerProvider indicates that the resource is owned by the provider. + OwnerProvider Owner = "provider" + // OwnerConsumer indicates that the resource is owned by the consumer. + OwnerConsumer Owner = "consumer" +) + +func (o Owner) String() string { + return string(o) +} + +// Selector is a resource selector that selects objects of a GVR. +type Selector struct { + // all claims all resources for the given group/resource. + // This is mutually exclusive with resourceSelector. + // +optional + All bool `json:"all,omitempty"` + + // LabelSelector is a label selector that selects objects of a GVR. + // +optional + LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` +} + +// SelectorResourceName identifies a specific resource by name. +// If backend operates at the namespace level isolation, namespace will be included. +type SelectorResourceName struct { + // Name is the name of the resource. + // +kubebuilder:validation:Required + Name string `json:"name,omitempty"` +} + // GroupResource identifies a resource. type GroupResource struct { // group is the name of an API group. @@ -133,6 +190,47 @@ type GroupResource struct { Resource string `json:"resource"` } +// String returns the string representation of the GR. +func (r GroupResource) String() string { + return fmt.Sprintf("%s.%s", r.Resource, r.Group) +} + +// GroupVersionResource unambiguously identifies a resource. +type GroupVersionResource struct { + // group is the name of an API group. + // For core groups this is the empty string '""'. + // + // +kubebuilder:validation:Pattern=`^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$` + // +kubebuilder:default="" + Group string `json:"group,omitempty"` + // version is the version of the resource. + // + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + // +required + // +kubebuilder:validation:Required + Version string `json:"version,omitempty"` + // resource is the name of the resource. + // + // +kubebuilder:validation:Pattern=`^[a-z][-a-z0-9]*[a-z0-9]$` + // +required + // +kubebuilder:validation:Required + Resource string `json:"resource,omitempty"` +} + +// String returns the string representation of the GVR. +func (r GroupVersionResource) String() string { + return fmt.Sprintf("%s.%s.%s", r.Resource, r.Version, r.Group) +} + +// GetSchemaGroupVersionResource returns the schema.GroupVersionResource representation of the GVR. +func (r GroupVersionResource) GetSchemaGroupVersionResource() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: r.Group, + Version: r.Version, + Resource: r.Resource, + } +} + // APIServiceExportRequestPhase describes the phase of a binding request. type APIServiceExportRequestPhase string diff --git a/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go b/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go index f99acaac4..f2a4ea15b 100644 --- a/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go +++ b/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go @@ -73,3 +73,87 @@ type BindingResponseAuthenticationOAuth2CodeGrant struct { // id is the ID of the authenticated user. It is for informational purposes only. ID string `json:"id"` } + +type BindableResourcesResponse struct { + metav1.TypeMeta `json:",inline"` + + // resources is a list of resources that the user can select from. + // + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Resources []BindableResource `json:"resources"` +} + +// BindableResource describes a resource that the user can select to bind to. +type BindableResource struct { + // name is the name of the resource. + // + // +required + // +kubebuilder:validation:Required + Name string `json:"name"` + + // description is a human friendly description of the resource. + // + // +optional + // +kubebuilder:validation:Optional + Description string `json:"description,omitempty"` + + // kind is the kind of the resource. + // +required + // +kubebuilder:validation:Required + Kind string `json:"kind"` + + // scope is the scope of the resource, either "Cluster" or "Namespaced". + // + // +required + // +kubebuilder:validation:Required + Scope string `json:"scope"` + + // apiVersion is the API version of the resource. + // + // +required + // +kubebuilder:validation:Required + APIVersion string `json:"apiVersion"` + + // group is the API group of the resource. + // + // +required + // +kubebuilder:validation:Required + Group string `json:"group"` + + // resource is the plural name of the resource. + // + // +required + // +kubebuilder:validation:Required + Resource string `json:"resource"` + + // sessionID is a session ID that the consumer must pass back to the service provider + // during the binding step. If multiple backends are aggregated, this can be used to + // to authenticate the user to the correct backend. + // + // +required + // +kubebuilder:validation:Required + SessionID string `json:"sessionID"` +} + +// BindableResourcesRequest is sent by the consumer to the service provider +// to indicate which resources the user wants to bind to. It is sent after +// authentication and resource selection on the service provider website. +type BindableResourcesRequest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // resources is a list of resources that the user can select from. + // + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Resources []BindableResource `json:"resources"` + + // PermissionClaims are additional permissions that the user wants to have. + // + // +optional + // +kubebuilder:validation:Optional + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` +} diff --git a/sdk/apis/kubebind/v1alpha2/boundapiresourceschema_types.go b/sdk/apis/kubebind/v1alpha2/boundapiresourceschema_types.go deleted file mode 100644 index 7c0e74df3..000000000 --- a/sdk/apis/kubebind/v1alpha2/boundapiresourceschema_types.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha2 - -import ( - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" -) - -// BoundAPIResourceSchema -// +crd -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:resource:scope=Namespaced,categories=kube-bindings,shortName=bas -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -type BoundAPIResourceSchema struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec BoundAPIResourceSchemaSpec `json:"spec"` - Status BoundAPIResourceSchemaStatus `json:"status,omitempty"` -} - -// BoundAPIResourceSchemaSpec defines the desired state of the BoundAPIResourceSchema. -type BoundAPIResourceSchemaSpec struct { - // InformerScope indicates whether the informer for defined custom resource is cluster- or namespace-scoped. - // Allowed values are `Cluster` and `Namespaced`. - // - // +required - // +kubebuilder:validation:Enum=Cluster;Namespaced - InformerScope InformerScope `json:"informerScope"` - - APIResourceSchemaCRDSpec `json:",inline"` -} - -const ( - // BoundAPIResourceSchemaReady indicates that the API resource schema is ready. - // It is set to true when the API resource schema is accepted and there are no drifts detected. - BoundAPIResourceSchemaValid conditionsapi.ConditionType = "Valid" - // BoundAPIResourceSchemaDriftDetected indicates that there is a drift between the consumer's API and the expected API. - // It is set to true when the API resource schema is not accepted or there are drifts detected. - BoundAPIResourceSchemaInvalid conditionsapi.ConditionType = "Invalid" -) - -// BoundAPIResourceSchemaConditionReason is the set of reasons for specific condition type. -// +kubebuilder:validation:Enum=Accepted;Rejected;Pending;DriftDetected -type BoundAPIResourceSchemaConditionReason string - -const ( - // BoundAPIResourceSchemaAccepted indicates that the API resource schema is accepted. - BoundAPIResourceSchemaAccepted BoundAPIResourceSchemaConditionReason = "Accepted" - // BoundAPIResourceSchemaRejected indicates that the API resource schema is rejected. - BoundAPIResourceSchemaRejected BoundAPIResourceSchemaConditionReason = "Rejected" - // BoundAPIResourceSchemaPending indicates that the API resource schema is pending. - BoundAPIResourceSchemaPending BoundAPIResourceSchemaConditionReason = "Pending" - // BoundAPIResourceSchemaDriftDetected indicates that there is a drift between the consumer's API and the expected API. - BoundAPIResourceSchemaDriftDetected BoundAPIResourceSchemaConditionReason = "DriftDetected" -) - -func (in *BoundAPIResourceSchema) GetConditions() conditionsapi.Conditions { - return in.Status.Conditions -} - -func (in *BoundAPIResourceSchema) SetConditions(conditions conditionsapi.Conditions) { - in.Status.Conditions = conditions -} - -// BoundAPIResourceSchemaStatus defines the observed state of the BoundAPIResourceSchema. -type BoundAPIResourceSchemaStatus struct { - // acceptedNames are the names that are actually being used to serve discovery. - // They may be different than the names in spec. - // +optional - AcceptedNames apiextensionsv1.CustomResourceDefinitionNames `json:"acceptedNames"` - - // storedVersions lists all versions of CustomResources that were ever persisted. Tracking these - // versions allows a migration path for stored versions in etcd. The field is mutable - // so a migration controller can finish a migration to another version (ensuring - // no old objects are left in storage), and then remove the rest of the - // versions from this list. - // Versions may not be removed from `spec.versions` while they exist in this list. - // +optional - StoredVersions []string `json:"storedVersions"` - // Conditions represent the latest available observations of the object's state. - // +optional - Conditions []conditionsapi.Condition `json:"conditions,omitempty"` - - // Instantiations tracks the number of instances of the resource on the consumer side. - // +optional - Instantiations int `json:"instantiations,omitempty"` -} - -// BoundAPIResourceSchemaList is a list of BoundAPIResourceSchemas. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type BoundAPIResourceSchemaList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata"` - - Items []BoundAPIResourceSchema `json:"items"` -} diff --git a/sdk/apis/kubebind/v1alpha2/apiresourceschema_types.go b/sdk/apis/kubebind/v1alpha2/boundchema_types.go similarity index 68% rename from sdk/apis/kubebind/v1alpha2/apiresourceschema_types.go rename to sdk/apis/kubebind/v1alpha2/boundchema_types.go index 589f3ede0..f4a2889d7 100644 --- a/sdk/apis/kubebind/v1alpha2/apiresourceschema_types.go +++ b/sdk/apis/kubebind/v1alpha2/boundchema_types.go @@ -18,37 +18,53 @@ package v1alpha2 import ( "encoding/json" + "fmt" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" -) - -// InformerScope is the scope of the Api. -// -// +kubebuilder:validation:Enum=Cluster;Namespaced -type InformerScope string -const ( - ClusterScope InformerScope = "Cluster" - NamespacedScope InformerScope = "Namespaced" + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" ) -// APIResourceSchema +// ExportedSchemas are the schemas exported by the current backend. +// Keys are resource.version.group string for quick resolve. +type ExportedSchemas map[string]*BoundSchema + +// BoundSchema // +crd // +genclient -// +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:resource:scope=Cluster,categories=kube-bindings,shortName=as +// +kubebuilder:resource:scope=Namespaced,categories=kube-bindings,shortName=bas +// +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -type APIResourceSchema struct { +type BoundSchema struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec APIResourceSchemaSpec `json:"spec"` + Spec BoundSchemaSpec `json:"spec"` + Status BoundSchemaStatus `json:"status,omitempty"` } -type APIResourceSchemaSpec struct { +// ResourceGroupName returns the group name of the resource. +// +// Important: If you change this, change one for APIServiceExportRequestResource too. +func (b *BoundSchema) ResourceGroupName() string { + return fmt.Sprintf("%s.%s", b.Spec.Names.Plural, b.Spec.Group) +} + +// InformerScope is the scope of the Api. +// +// +kubebuilder:validation:Enum=Cluster;Namespaced +type InformerScope string + +const ( + ClusterScope InformerScope = "Cluster" + NamespacedScope InformerScope = "Namespaced" +) + +// BoundSchemaSpec defines the desired state of the BoundSchema. +type BoundSchemaSpec struct { // InformerScope indicates whether the informer for defined custom resource is cluster- or namespace-scoped. // Allowed values are `Cluster` and `Namespaced`. // @@ -56,10 +72,11 @@ type APIResourceSchemaSpec struct { // +kubebuilder:validation:Enum=Cluster;Namespaced InformerScope InformerScope `json:"informerScope"` - APIResourceSchemaCRDSpec `json:",inline"` + // API CRD Spec is copy paste from apiextensionsv1.CustomResourceDefinitionSpec to allow deep copy + APICRDSpec `json:",inline"` } -type APIResourceSchemaCRDSpec struct { +type APICRDSpec struct { // group is the API group of the defined custom resource. Empty string means the // core API group. The resources are served under `/apis//...` or `/api` for the core group. // @@ -93,6 +110,70 @@ type APIResourceSchemaCRDSpec struct { Conversion *CustomResourceConversion `json:"conversion,omitempty"` } +// CustomResourceConversion describes how to convert different versions of a CR. +// +kubebuilder:validation:XValidation:message="Webhook must be specified if strategy=Webhook",rule="(self.strategy == 'None' && !has(self.webhook)) || (self.strategy == 'Webhook' && has(self.webhook))" +type CustomResourceConversion struct { + // strategy specifies how custom resources are converted between versions. Allowed values are: + // - `"None"`: The converter only change the apiVersion and would not touch any other field in the custom resource. + // - `"Webhook"`: API Server will call to an external webhook to do the conversion. Additional information + // is needed for this option. This requires spec.preserveUnknownFields to be false, and spec.conversion.webhook to be set. + // +kubebuilder:validation:Enum=None;Webhook + Strategy ConversionStrategyType `json:"strategy"` + + // webhook describes how to call the conversion webhook. Required when `strategy` is set to `"Webhook"`. + // +optional + Webhook *WebhookConversion `json:"webhook,omitempty"` +} + +// WebhookConversion describes how to call a conversion webhook +type WebhookConversion struct { + // clientConfig is the instructions for how to call the webhook if strategy is `Webhook`. + // +optional + ClientConfig *WebhookClientConfig `json:"clientConfig,omitempty"` + + // conversionReviewVersions is an ordered list of preferred `ConversionReview` + // versions the Webhook expects. The API server will use the first version in + // the list which it supports. If none of the versions specified in this list + // are supported by API server, conversion will fail for the custom resource. + // If a persisted Webhook configuration specifies allowed versions and does not + // include any versions known to the API Server, calls to the webhook will fail. + // +listType=atomic + ConversionReviewVersions []string `json:"conversionReviewVersions"` +} + +// WebhookClientConfig contains the information to make a TLS connection with the webhook. +type WebhookClientConfig struct { + // url gives the location of the webhook, in standard URL form + // (`scheme://host:port/path`). + // + // Please note that using `localhost` or `127.0.0.1` as a `host` is + // risky unless you take great care to run this webhook on all hosts + // which run an apiserver which might need to make calls to this + // webhook. Such installs are likely to be non-portable, i.e., not easy + // to turn up in a new cluster. + // + // The scheme must be "https"; the URL must begin with "https://". + // + // A path is optional, and if present may be any string permissible in + // a URL. You may use the path to pass an arbitrary string to the + // webhook, for example, a cluster identifier. + // + // Attempting to use a user or basic auth e.g. "user:password@" is not + // allowed. Fragments ("#...") and query parameters ("?...") are not + // allowed, either. + // + // +kubebuilder:validation:Format=uri + URL *string `json:"url,omitempty"` + + // caBundle is a PEM encoded CA bundle which will be used to validate the webhook's server certificate. + // If unspecified, system trust roots on the apiserver are used. + // +optional + CABundle []byte `json:"caBundle,omitempty"` +} + +// ConversionStrategyType describes different conversion types. +type ConversionStrategyType string + // APIResourceVersion describes one API version of a resource. type APIResourceVersion struct { // name is the version name, e.g. “v1”, “v2beta1”, etc. @@ -133,7 +214,7 @@ type APIResourceVersion struct { // +required // +kubebuilder:pruning:PreserveUnknownFields // +structType=atomic - Schema CRDVersionSchema `json:"schema"` + Schema runtime.RawExtension `json:"schema"` // subresources specify what subresources this version of the defined custom resource have. // // +optional @@ -148,96 +229,77 @@ type APIResourceVersion struct { AdditionalPrinterColumns []apiextensionsv1.CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty"` } -type CRDVersionSchema struct { - // openAPIV3Schema is the OpenAPI v3 schema to use for validation and pruning. - // - // +kubebuilder:pruning:PreserveUnknownFields - // +structType=atomic - // +required - // +kubebuilder:validation:Required - OpenAPIV3Schema runtime.RawExtension `json:"openAPIV3Schema"` -} +const ( + // BoundSchemaReady indicates that the API resource schema is ready. + // It is set to true when the API resource schema is accepted and there are no drifts detected. + BoundSchemaValid conditionsapi.ConditionType = "Valid" + // BoundSchemaDriftDetected indicates that there is a drift between the consumer's API and the expected API. + // It is set to true when the API resource schema is not accepted or there are drifts detected. + BoundSchemaInvalid conditionsapi.ConditionType = "Invalid" +) -// CustomResourceConversion describes how to convert different versions of a CR. -// +kubebuilder:validation:XValidation:message="Webhook must be specified if strategy=Webhook",rule="(self.strategy == 'None' && !has(self.webhook)) || (self.strategy == 'Webhook' && has(self.webhook))" -type CustomResourceConversion struct { - // strategy specifies how custom resources are converted between versions. Allowed values are: - // - `"None"`: The converter only change the apiVersion and would not touch any other field in the custom resource. - // - `"Webhook"`: API Server will call to an external webhook to do the conversion. Additional information - // is needed for this option. This requires spec.preserveUnknownFields to be false, and spec.conversion.webhook to be set. - // +kubebuilder:validation:Enum=None;Webhook - Strategy ConversionStrategyType `json:"strategy"` +// BoundSchemaConditionReason is the set of reasons for specific condition type. +// +kubebuilder:validation:Enum=Accepted;Rejected;Pending;DriftDetected +type BoundSchemaConditionReason string - // webhook describes how to call the conversion webhook. Required when `strategy` is set to `"Webhook"`. - // +optional - Webhook *WebhookConversion `json:"webhook,omitempty"` +const ( + // BoundSchemaAccepted indicates that the API resource schema is accepted. + BoundSchemaAccepted BoundSchemaConditionReason = "Accepted" + // BoundSchemaRejected indicates that the API resource schema is rejected. + BoundSchemaRejected BoundSchemaConditionReason = "Rejected" + // BoundSchemaPending indicates that the API resource schema is pending. + BoundSchemaPending BoundSchemaConditionReason = "Pending" + // BoundSchemaDriftDetected indicates that there is a drift between the consumer's API and the expected API. + BoundSchemaDriftDetected BoundSchemaConditionReason = "DriftDetected" +) + +func (in *BoundSchema) GetConditions() conditionsapi.Conditions { + return in.Status.Conditions } -// ConversionStrategyType describes different conversion types. -type ConversionStrategyType string +func (in *BoundSchema) SetConditions(conditions conditionsapi.Conditions) { + in.Status.Conditions = conditions +} -// WebhookConversion describes how to call a conversion webhook -type WebhookConversion struct { - // clientConfig is the instructions for how to call the webhook if strategy is `Webhook`. +// BoundSchemaStatus defines the observed state of the BoundSchema. +type BoundSchemaStatus struct { + // acceptedNames are the names that are actually being used to serve discovery. + // They may be different than the names in spec. // +optional - ClientConfig *WebhookClientConfig `json:"clientConfig,omitempty"` + AcceptedNames apiextensionsv1.CustomResourceDefinitionNames `json:"acceptedNames"` - // conversionReviewVersions is an ordered list of preferred `ConversionReview` - // versions the Webhook expects. The API server will use the first version in - // the list which it supports. If none of the versions specified in this list - // are supported by API server, conversion will fail for the custom resource. - // If a persisted Webhook configuration specifies allowed versions and does not - // include any versions known to the API Server, calls to the webhook will fail. - // +listType=atomic - ConversionReviewVersions []string `json:"conversionReviewVersions"` -} - -// WebhookClientConfig contains the information to make a TLS connection with the webhook. -type WebhookClientConfig struct { - // url gives the location of the webhook, in standard URL form - // (`scheme://host:port/path`). - // - // Please note that using `localhost` or `127.0.0.1` as a `host` is - // risky unless you take great care to run this webhook on all hosts - // which run an apiserver which might need to make calls to this - // webhook. Such installs are likely to be non-portable, i.e., not easy - // to turn up in a new cluster. - // - // The scheme must be "https"; the URL must begin with "https://". - // - // A path is optional, and if present may be any string permissible in - // a URL. You may use the path to pass an arbitrary string to the - // webhook, for example, a cluster identifier. - // - // Attempting to use a user or basic auth e.g. "user:password@" is not - // allowed. Fragments ("#...") and query parameters ("?...") are not - // allowed, either. - // - // +kubebuilder:validation:Format=uri - URL *string `json:"url,omitempty"` + // storedVersions lists all versions of CustomResources that were ever persisted. Tracking these + // versions allows a migration path for stored versions in etcd. The field is mutable + // so a migration controller can finish a migration to another version (ensuring + // no old objects are left in storage), and then remove the rest of the + // versions from this list. + // Versions may not be removed from `spec.versions` while they exist in this list. + // +optional + StoredVersions []string `json:"storedVersions"` + // Conditions represent the latest available observations of the object's state. + // +optional + Conditions []conditionsapi.Condition `json:"conditions,omitempty"` - // caBundle is a PEM encoded CA bundle which will be used to validate the webhook's server certificate. - // If unspecified, system trust roots on the apiserver are used. + // Instantiations tracks the number of instances of the resource on the consumer side. // +optional - CABundle []byte `json:"caBundle,omitempty"` + Instantiations int `json:"instantiations,omitempty"` } -// APIResourceSchemaList is a list of APIResourceSchemas. -// +// BoundSchemaList is a list of BoundSchemas. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type APIResourceSchemaList struct { +type BoundSchemaList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` - Items []APIResourceSchema `json:"items"` + Items []BoundSchema `json:"items"` } func (v *APIResourceVersion) GetSchema() (*apiextensionsv1.JSONSchemaProps, error) { - if v.Schema.OpenAPIV3Schema.Raw == nil { + if v.Schema.Raw == nil { return nil, nil } var schema apiextensionsv1.JSONSchemaProps - if err := json.Unmarshal(v.Schema.OpenAPIV3Schema.Raw, &schema); err != nil { + if err := json.Unmarshal(v.Schema.Raw, &schema); err != nil { return nil, err } return &schema, nil @@ -245,13 +307,13 @@ func (v *APIResourceVersion) GetSchema() (*apiextensionsv1.JSONSchemaProps, erro func (v *APIResourceVersion) SetSchema(schema *apiextensionsv1.JSONSchemaProps) error { if schema == nil { - v.Schema.OpenAPIV3Schema.Raw = nil + v.Schema.Raw = nil return nil } raw, err := json.Marshal(schema) if err != nil { return err } - v.Schema.OpenAPIV3Schema.Raw = raw + v.Schema.Raw = raw return nil } diff --git a/sdk/apis/kubebind/v1alpha2/claimable_apis.go b/sdk/apis/kubebind/v1alpha2/claimable_apis.go new file mode 100644 index 000000000..f99b9a8a0 --- /dev/null +++ b/sdk/apis/kubebind/v1alpha2/claimable_apis.go @@ -0,0 +1,92 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// InternalAPI describes an API to be imported from some schemes and generated OpenAPI V2 definitions. +type InternalAPI struct { + Names apiextensionsv1.CustomResourceDefinitionNames `json:"names"` + GroupVersionResource GroupVersionResource `json:"groupVersionResource"` + Instance runtime.Object `json:"instance"` + ResourceScope apiextensionsv1.ResourceScope `json:"resourceScope"` + HasStatus bool `json:"hasStatus"` +} + +// ClaimableAPIs is a list of APIs that can be claimed by a user. +type ClaimableAPIs []InternalAPI + +// ClaimableAPIsData is a list of APIs that can be claimed by a user. +var ClaimableAPIsData = ClaimableAPIs{ + { + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "configmaps", + Singular: "configmap", + Kind: "ConfigMap", + }, + GroupVersionResource: GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, + Instance: &corev1.ConfigMap{}, + ResourceScope: apiextensionsv1.NamespaceScoped, + }, + { + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "secrets", + Singular: "secret", + Kind: "Secret", + }, + GroupVersionResource: GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + }, + Instance: &corev1.Secret{}, + ResourceScope: apiextensionsv1.NamespaceScoped, + }, + { + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "serviceaccounts", + Singular: "serviceaccount", + Kind: "ServiceAccount", + }, + GroupVersionResource: GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "serviceaccounts", + }, + Instance: &corev1.ServiceAccount{}, + ResourceScope: apiextensionsv1.NamespaceScoped, + }, +} + +func ResolveClaimableAPI(claim PermissionClaim) (GroupVersionResource, error) { + for _, api := range ClaimableAPIsData { + if api.Names.Plural == claim.Resource && api.GroupVersionResource.Group == claim.Group { + return api.GroupVersionResource, nil + } + } + return GroupVersionResource{}, fmt.Errorf("no matching API found") +} diff --git a/sdk/apis/kubebind/v1alpha2/helpers/apiresourceschema.go b/sdk/apis/kubebind/v1alpha2/helpers/apiresourceschema.go deleted file mode 100644 index c66388cbc..000000000 --- a/sdk/apis/kubebind/v1alpha2/helpers/apiresourceschema.go +++ /dev/null @@ -1,188 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helpers - -import ( - "crypto/sha256" - "encoding/json" - "fmt" - "math/big" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" -) - -// CRDToAPIResourceSchema converts a CustomResourceDefinition to an APIResourceSchema. -func CRDToAPIResourceSchema(crd *apiextensionsv1.CustomResourceDefinition, prefix string) (*kubebindv1alpha2.APIResourceSchema, error) { - name := crd.Name - if prefix != "" { - name = prefix + "." + crd.Name - } - - informerScope := kubebindv1alpha2.NamespacedScope - apiResourceSchema := &kubebindv1alpha2.APIResourceSchema{ - TypeMeta: metav1.TypeMeta{ - APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), - Kind: "APIResourceSchema", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: kubebindv1alpha2.APIResourceSchemaSpec{ - InformerScope: informerScope, - APIResourceSchemaCRDSpec: kubebindv1alpha2.APIResourceSchemaCRDSpec{ - Group: crd.Spec.Group, - Names: crd.Spec.Names, - Scope: crd.Spec.Scope, - }, - }, - } - - if len(crd.Spec.Versions) > 1 && crd.Spec.Conversion == nil { - return nil, fmt.Errorf("multiple versions specified for CRD %q but no conversion strategy", crd.Name) - } - - if crd.Spec.Conversion != nil { - crConversion := &kubebindv1alpha2.CustomResourceConversion{ - Strategy: kubebindv1alpha2.ConversionStrategyType(crd.Spec.Conversion.Strategy), - } - - if crd.Spec.Conversion.Strategy == "Webhook" { - crConversion.Webhook = &kubebindv1alpha2.WebhookConversion{ - ConversionReviewVersions: crd.Spec.Conversion.Webhook.ConversionReviewVersions, - } - - if crd.Spec.Conversion.Webhook.ClientConfig != nil { - crConversion.Webhook.ClientConfig = &kubebindv1alpha2.WebhookClientConfig{ - URL: crd.Spec.Conversion.Webhook.ClientConfig.URL, - CABundle: crd.Spec.Conversion.Webhook.ClientConfig.CABundle, - } - } - } - - apiResourceSchema.Spec.Conversion = crConversion - } - - for _, crdVersion := range crd.Spec.Versions { - apiResourceVersion := kubebindv1alpha2.APIResourceVersion{ - Name: crdVersion.Name, - Served: crdVersion.Served, - Storage: crdVersion.Storage, - Deprecated: crdVersion.Deprecated, - DeprecationWarning: crdVersion.DeprecationWarning, - AdditionalPrinterColumns: crdVersion.AdditionalPrinterColumns, - } - if crdVersion.Schema != nil && crdVersion.Schema.OpenAPIV3Schema != nil { - rawSchema, err := json.Marshal(crdVersion.Schema.OpenAPIV3Schema) - if err != nil { - return nil, fmt.Errorf("error converting schema for version %q: %w", crdVersion.Name, err) - } - apiResourceVersion.Schema = kubebindv1alpha2.CRDVersionSchema{ - OpenAPIV3Schema: runtime.RawExtension{Raw: rawSchema}, - } - } - - if crdVersion.Subresources != nil { - apiResourceVersion.Subresources = *crdVersion.Subresources - } - - apiResourceSchema.Spec.Versions = append(apiResourceSchema.Spec.Versions, apiResourceVersion) - } - - return apiResourceSchema, nil -} - -// APIResourceSchemaToCRD converts an APIResourceSchema to a CustomResourceDefinition. -func APIResourceSchemaToCRD(schema *kubebindv1alpha2.APIResourceSchema) (*apiextensionsv1.CustomResourceDefinition, error) { - crd := &apiextensionsv1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Name: schema.Spec.Names.Plural + "." + schema.Spec.Group, - Annotations: map[string]string{ - "kube-bind.io/source-schema": schema.Name, - }, - }, - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Group: schema.Spec.Group, - Names: schema.Spec.Names, - Scope: schema.Spec.Scope, - }, - } - - for _, schemaVersion := range schema.Spec.Versions { - crdVersion := apiextensionsv1.CustomResourceDefinitionVersion{ - Name: schemaVersion.Name, - Served: schemaVersion.Served, - Storage: schemaVersion.Storage, - Deprecated: schemaVersion.Deprecated, - DeprecationWarning: schemaVersion.DeprecationWarning, - AdditionalPrinterColumns: schemaVersion.AdditionalPrinterColumns, - } - - if schemaVersion.Schema.OpenAPIV3Schema.Raw != nil { - var schemaObj apiextensionsv1.JSONSchemaProps - if err := json.Unmarshal(schemaVersion.Schema.OpenAPIV3Schema.Raw, &schemaObj); err != nil { - return nil, fmt.Errorf("failed to unmarshal schema for version %s: %v", schemaVersion.Name, err) - } - crdVersion.Schema = &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &schemaObj, - } - } - - if schemaVersion.Subresources.Status != nil || schemaVersion.Subresources.Scale != nil { - crdVersion.Subresources = &apiextensionsv1.CustomResourceSubresources{} - - if schemaVersion.Subresources.Status != nil { - crdVersion.Subresources.Status = &apiextensionsv1.CustomResourceSubresourceStatus{} - } - - if schemaVersion.Subresources.Scale != nil { - crdVersion.Subresources.Scale = &apiextensionsv1.CustomResourceSubresourceScale{ - SpecReplicasPath: schemaVersion.Subresources.Scale.SpecReplicasPath, - StatusReplicasPath: schemaVersion.Subresources.Scale.StatusReplicasPath, - LabelSelectorPath: schemaVersion.Subresources.Scale.LabelSelectorPath, - } - } - } - - crd.Spec.Versions = append(crd.Spec.Versions, crdVersion) - } - - return crd, nil -} -func APIResourceSchemaCRDSpecHash(obj *kubebindv1alpha2.APIResourceSchemaCRDSpec) string { - bs, err := json.Marshal(obj) - if err != nil { - utilruntime.HandleError(err) - return "" - } - - return toSha224Base62(string(bs)) -} - -func toSha224Base62(s string) string { - return toBase62(sha256.Sum224([]byte(s))) -} - -func toBase62(hash [28]byte) string { - var i big.Int - i.SetBytes(hash[:]) - return i.Text(62) -} diff --git a/sdk/apis/kubebind/v1alpha2/helpers/apiresourceschema_test.go b/sdk/apis/kubebind/v1alpha2/helpers/apiresourceschema_test.go deleted file mode 100644 index e5722623a..000000000 --- a/sdk/apis/kubebind/v1alpha2/helpers/apiresourceschema_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helpers - -import ( - "testing" - - "github.com/stretchr/testify/require" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/utils/ptr" -) - -func TestWebhookCRDStorageVersion(t *testing.T) { - input := v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Versions: []v1.CustomResourceDefinitionVersion{ - { - Served: true, - Name: "v1alpha2", - Storage: false, - }, - { - Served: true, - Name: "v1", - Storage: true, - }, - }, - Conversion: &v1.CustomResourceConversion{ - Strategy: v1.WebhookConverter, - Webhook: &v1.WebhookConversion{ - ClientConfig: &v1.WebhookClientConfig{ - URL: ptr.To("https://example.com/webhook"), - CABundle: []byte("1234")}, - }, - }, - }, - } - - output, err := CRDToAPIResourceSchema(&input, "test") - - require.NoError(t, err) - - atLeastOneStorageVersion := false - for _, v := range output.Spec.Versions { - atLeastOneStorageVersion = atLeastOneStorageVersion || v.Storage - } - if !atLeastOneStorageVersion { - t.Fatal("returned ResourceExport has no storage version", output) - } -} diff --git a/sdk/apis/kubebind/v1alpha2/helpers/boundapiresourceschema.go b/sdk/apis/kubebind/v1alpha2/helpers/boundapiresourceschema.go deleted file mode 100644 index 01d664722..000000000 --- a/sdk/apis/kubebind/v1alpha2/helpers/boundapiresourceschema.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright 2025 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helpers - -import ( - "encoding/json" - "fmt" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" -) - -// CRDToBoundAPIResourceSchema converts a CustomResourceDefinition to an BoundAPIResourceSchema. -func CRDToBoundAPIResourceSchema(crd *apiextensionsv1.CustomResourceDefinition, prefix string) (*kubebindv1alpha2.APIResourceSchema, error) { - name := prefix + "." + crd.Name - informerScope := kubebindv1alpha2.NamespacedScope - apiResourceSchema := &kubebindv1alpha2.APIResourceSchema{ - TypeMeta: metav1.TypeMeta{ - APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), - Kind: "APIResourceSchema", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: kubebindv1alpha2.APIResourceSchemaSpec{ - InformerScope: informerScope, - APIResourceSchemaCRDSpec: kubebindv1alpha2.APIResourceSchemaCRDSpec{ - Group: crd.Spec.Group, - Names: crd.Spec.Names, - Scope: crd.Spec.Scope, - }, - }, - } - - if len(crd.Spec.Versions) > 1 && crd.Spec.Conversion == nil { - return nil, fmt.Errorf("multiple versions specified for CRD %q but no conversion strategy", crd.Name) - } - - if crd.Spec.Conversion != nil { - crConversion := &kubebindv1alpha2.CustomResourceConversion{ - Strategy: kubebindv1alpha2.ConversionStrategyType(crd.Spec.Conversion.Strategy), - } - - if crd.Spec.Conversion.Strategy == "Webhook" { - crConversion.Webhook = &kubebindv1alpha2.WebhookConversion{ - ConversionReviewVersions: crd.Spec.Conversion.Webhook.ConversionReviewVersions, - } - - if crd.Spec.Conversion.Webhook.ClientConfig != nil { - crConversion.Webhook.ClientConfig = &kubebindv1alpha2.WebhookClientConfig{ - URL: crd.Spec.Conversion.Webhook.ClientConfig.URL, - CABundle: crd.Spec.Conversion.Webhook.ClientConfig.CABundle, - } - } - } - - apiResourceSchema.Spec.Conversion = crConversion - } - - for _, crdVersion := range crd.Spec.Versions { - apiResourceVersion := kubebindv1alpha2.APIResourceVersion{ - Name: crdVersion.Name, - Served: crdVersion.Served, - Storage: crdVersion.Storage, - Deprecated: crdVersion.Deprecated, - DeprecationWarning: crdVersion.DeprecationWarning, - AdditionalPrinterColumns: crdVersion.AdditionalPrinterColumns, - } - if crdVersion.Schema != nil && crdVersion.Schema.OpenAPIV3Schema != nil { - rawSchema, err := json.Marshal(crdVersion.Schema.OpenAPIV3Schema) - if err != nil { - return nil, fmt.Errorf("error converting schema for version %q: %w", crdVersion.Name, err) - } - apiResourceVersion.Schema = kubebindv1alpha2.CRDVersionSchema{ - OpenAPIV3Schema: runtime.RawExtension{Raw: rawSchema}, - } - } - - if crdVersion.Subresources != nil { - apiResourceVersion.Subresources = *crdVersion.Subresources - } - - apiResourceSchema.Spec.Versions = append(apiResourceSchema.Spec.Versions, apiResourceVersion) - } - - return apiResourceSchema, nil -} diff --git a/sdk/apis/kubebind/v1alpha2/helpers/boundschema.go b/sdk/apis/kubebind/v1alpha2/helpers/boundschema.go new file mode 100644 index 000000000..b6c11ee73 --- /dev/null +++ b/sdk/apis/kubebind/v1alpha2/helpers/boundschema.go @@ -0,0 +1,194 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helpers + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "strings" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +// CRDToBoundSchema converts a CustomResourceDefinition to an BoundSchema. +func CRDToBoundSchema(crd *apiextensionsv1.CustomResourceDefinition, prefix string) (*kubebindv1alpha2.BoundSchema, error) { + name := prefix + "." + crd.Name + informerScope := kubebindv1alpha2.NamespacedScope + boundSchema := &kubebindv1alpha2.BoundSchema{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), + Kind: "BoundSchema", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: kubebindv1alpha2.BoundSchemaSpec{ + InformerScope: informerScope, + APICRDSpec: kubebindv1alpha2.APICRDSpec{ + Group: crd.Spec.Group, + Names: crd.Spec.Names, + Scope: crd.Spec.Scope, + }, + }, + } + + if len(crd.Spec.Versions) > 1 && crd.Spec.Conversion == nil { + return nil, fmt.Errorf("multiple versions specified for CRD %q but no conversion strategy", crd.Name) + } + + if crd.Spec.Conversion != nil { + crConversion := &kubebindv1alpha2.CustomResourceConversion{ + Strategy: kubebindv1alpha2.ConversionStrategyType(crd.Spec.Conversion.Strategy), + } + + if crd.Spec.Conversion.Strategy == "Webhook" { + crConversion.Webhook = &kubebindv1alpha2.WebhookConversion{ + ConversionReviewVersions: crd.Spec.Conversion.Webhook.ConversionReviewVersions, + } + + if crd.Spec.Conversion.Webhook.ClientConfig != nil { + crConversion.Webhook.ClientConfig = &kubebindv1alpha2.WebhookClientConfig{ + URL: crd.Spec.Conversion.Webhook.ClientConfig.URL, + CABundle: crd.Spec.Conversion.Webhook.ClientConfig.CABundle, + } + } + } + + boundSchema.Spec.Conversion = crConversion + } + + for _, crdVersion := range crd.Spec.Versions { + apiResourceVersion := kubebindv1alpha2.APIResourceVersion{ + Name: crdVersion.Name, + Served: crdVersion.Served, + Storage: crdVersion.Storage, + Deprecated: crdVersion.Deprecated, + DeprecationWarning: crdVersion.DeprecationWarning, + AdditionalPrinterColumns: crdVersion.AdditionalPrinterColumns, + } + if crdVersion.Schema != nil && crdVersion.Schema.OpenAPIV3Schema != nil { + rawSchema, err := json.Marshal(crdVersion.Schema.OpenAPIV3Schema) + if err != nil { + return nil, fmt.Errorf("error converting schema for version %q: %w", crdVersion.Name, err) + } + apiResourceVersion.Schema = runtime.RawExtension{Raw: rawSchema} + } + + if crdVersion.Subresources != nil { + apiResourceVersion.Subresources = *crdVersion.Subresources + } + + boundSchema.Spec.Versions = append(boundSchema.Spec.Versions, apiResourceVersion) + } + + return boundSchema, nil +} + +func UnstructuredToBoundSchema(u unstructured.Unstructured) (*kubebindv1alpha2.BoundSchema, error) { + boundSchema := &kubebindv1alpha2.BoundSchema{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), boundSchema); err != nil { + return nil, err + } + return boundSchema, nil +} + +func BoundSchemasSpecHash(schemas []*kubebindv1alpha2.BoundSchema) string { + hash := sha256.New() + for _, schema := range schemas { + if err := json.NewEncoder(hash).Encode(schema); err != nil { + continue + } + } + return fmt.Sprintf("%x", hash.Sum(nil)) +} + +func BoundSchemaToCRD(schema *kubebindv1alpha2.BoundSchema) *apiextensionsv1.CustomResourceDefinition { + crd := &apiextensionsv1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apiextensions.k8s.io/v1", + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: schema.Name, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: schema.Spec.Group, + Names: schema.Spec.Names, + Scope: schema.Spec.Scope, + }, + } + + if schema.Spec.Conversion != nil { + crConversion := &apiextensionsv1.CustomResourceConversion{ + Strategy: apiextensionsv1.ConversionStrategyType(schema.Spec.Conversion.Strategy), + } + + if apiextensionsv1.ConversionStrategyType(schema.Spec.Conversion.Strategy) == apiextensionsv1.WebhookConverter && schema.Spec.Conversion.Webhook != nil { + crConversion.Webhook = &apiextensionsv1.WebhookConversion{ + ConversionReviewVersions: schema.Spec.Conversion.Webhook.ConversionReviewVersions, + } + + if schema.Spec.Conversion.Webhook.ClientConfig != nil { + crConversion.Webhook.ClientConfig = &apiextensionsv1.WebhookClientConfig{ + URL: schema.Spec.Conversion.Webhook.ClientConfig.URL, + CABundle: schema.Spec.Conversion.Webhook.ClientConfig.CABundle, + } + } + } + + crd.Spec.Conversion = crConversion + } + + for _, version := range schema.Spec.Versions { + crdVersion := apiextensionsv1.CustomResourceDefinitionVersion{ + Name: version.Name, + Served: version.Served, + Storage: version.Storage, + Deprecated: version.Deprecated, + DeprecationWarning: version.DeprecationWarning, + AdditionalPrinterColumns: version.AdditionalPrinterColumns, + Subresources: &version.Subresources, + } + // Now schema can be openapiv3 or v2. + // we do some poor man checking: + if len(version.Schema.Raw) > 0 { + if strings.Contains(string(version.Schema.Raw), "openAPIV3Schema") { + var jsonSchema apiextensionsv1.CustomResourceValidation // we need to unmarshal into the correct type + if err := json.Unmarshal(version.Schema.Raw, &jsonSchema); err == nil { + crdVersion.Schema = &jsonSchema + } + } else { + var jsonSchema apiextensionsv1.JSONSchemaProps + if err := json.Unmarshal(version.Schema.Raw, &jsonSchema); err == nil { + crdVersion.Schema = &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &jsonSchema, + } + } + } + } + + crd.Spec.Versions = append(crd.Spec.Versions, crdVersion) + } + + return crd +} diff --git a/sdk/apis/kubebind/v1alpha2/register.go b/sdk/apis/kubebind/v1alpha2/register.go index 0504c3814..990552c55 100644 --- a/sdk/apis/kubebind/v1alpha2/register.go +++ b/sdk/apis/kubebind/v1alpha2/register.go @@ -46,10 +46,8 @@ func Resource(resource string) schema.GroupResource { // Adds the list of known types to api.Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &APIResourceSchema{}, - &APIResourceSchemaList{}, - &BoundAPIResourceSchema{}, - &BoundAPIResourceSchemaList{}, + &BoundSchema{}, + &BoundSchemaList{}, &APIServiceBinding{}, &APIServiceBindingList{}, &APIServiceExport{}, diff --git a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go index 4e9a67533..eb813c896 100644 --- a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go +++ b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go @@ -23,40 +23,14 @@ package v1alpha2 import ( v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" v1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIResourceSchema) DeepCopyInto(out *APIResourceSchema) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIResourceSchema. -func (in *APIResourceSchema) DeepCopy() *APIResourceSchema { - if in == nil { - return nil - } - out := new(APIResourceSchema) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *APIResourceSchema) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIResourceSchemaCRDSpec) DeepCopyInto(out *APIResourceSchemaCRDSpec) { +func (in *APICRDSpec) DeepCopyInto(out *APICRDSpec) { *out = *in in.Names.DeepCopyInto(&out.Names) if in.Versions != nil { @@ -74,78 +48,12 @@ func (in *APIResourceSchemaCRDSpec) DeepCopyInto(out *APIResourceSchemaCRDSpec) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIResourceSchemaCRDSpec. -func (in *APIResourceSchemaCRDSpec) DeepCopy() *APIResourceSchemaCRDSpec { - if in == nil { - return nil - } - out := new(APIResourceSchemaCRDSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIResourceSchemaList) DeepCopyInto(out *APIResourceSchemaList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]APIResourceSchema, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIResourceSchemaList. -func (in *APIResourceSchemaList) DeepCopy() *APIResourceSchemaList { - if in == nil { - return nil - } - out := new(APIResourceSchemaList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *APIResourceSchemaList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIResourceSchemaReference) DeepCopyInto(out *APIResourceSchemaReference) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIResourceSchemaReference. -func (in *APIResourceSchemaReference) DeepCopy() *APIResourceSchemaReference { - if in == nil { - return nil - } - out := new(APIResourceSchemaReference) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *APIResourceSchemaSpec) DeepCopyInto(out *APIResourceSchemaSpec) { - *out = *in - in.APIResourceSchemaCRDSpec.DeepCopyInto(&out.APIResourceSchemaCRDSpec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIResourceSchemaSpec. -func (in *APIResourceSchemaSpec) DeepCopy() *APIResourceSchemaSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APICRDSpec. +func (in *APICRDSpec) DeepCopy() *APICRDSpec { if in == nil { return nil } - out := new(APIResourceSchemaSpec) + out := new(APICRDSpec) in.DeepCopyInto(out) return out } @@ -183,7 +91,7 @@ func (in *APIServiceBinding) DeepCopyInto(out *APIServiceBinding) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -243,6 +151,13 @@ func (in *APIServiceBindingList) DeepCopyObject() runtime.Object { func (in *APIServiceBindingSpec) DeepCopyInto(out *APIServiceBindingSpec) { *out = *in out.KubeconfigSecretRef = in.KubeconfigSecretRef + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -471,6 +386,13 @@ func (in *APIServiceExportRequestSpec) DeepCopyInto(out *APIServiceExportRequest (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -512,8 +434,17 @@ func (in *APIServiceExportSpec) DeepCopyInto(out *APIServiceExportSpec) { *out = *in if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = make([]APIResourceSchemaReference, len(*in)) - copy(*out, *in) + *out = make([]APIServiceExportRequestResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } @@ -531,12 +462,6 @@ func (in *APIServiceExportSpec) DeepCopy() *APIServiceExportSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *APIServiceExportStatus) DeepCopyInto(out *APIServiceExportStatus) { *out = *in - in.AcceptedNames.DeepCopyInto(&out.AcceptedNames) - if in.StoredVersions != nil { - in, out := &in.StoredVersions, &out.StoredVersions - *out = make([]string, len(*in)) - copy(*out, *in) - } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make(v1alpha1.Conditions, len(*in)) @@ -671,6 +596,74 @@ func (in *AuthenticationMethod) DeepCopy() *AuthenticationMethod { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BindableResource) DeepCopyInto(out *BindableResource) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindableResource. +func (in *BindableResource) DeepCopy() *BindableResource { + if in == nil { + return nil + } + out := new(BindableResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BindableResourcesRequest) DeepCopyInto(out *BindableResourcesRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]BindableResource, len(*in)) + copy(*out, *in) + } + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindableResourcesRequest. +func (in *BindableResourcesRequest) DeepCopy() *BindableResourcesRequest { + if in == nil { + return nil + } + out := new(BindableResourcesRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BindableResourcesResponse) DeepCopyInto(out *BindableResourcesResponse) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]BindableResource, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindableResourcesResponse. +func (in *BindableResourcesResponse) DeepCopy() *BindableResourcesResponse { + if in == nil { + return nil + } + out := new(BindableResourcesResponse) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BindingProvider) DeepCopyInto(out *BindingProvider) { *out = *in @@ -779,7 +772,7 @@ func (in *BindingResponseAuthenticationOAuth2CodeGrant) DeepCopy() *BindingRespo } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BoundAPIResourceSchema) DeepCopyInto(out *BoundAPIResourceSchema) { +func (in *BoundSchema) DeepCopyInto(out *BoundSchema) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -788,18 +781,18 @@ func (in *BoundAPIResourceSchema) DeepCopyInto(out *BoundAPIResourceSchema) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundAPIResourceSchema. -func (in *BoundAPIResourceSchema) DeepCopy() *BoundAPIResourceSchema { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundSchema. +func (in *BoundSchema) DeepCopy() *BoundSchema { if in == nil { return nil } - out := new(BoundAPIResourceSchema) + out := new(BoundSchema) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BoundAPIResourceSchema) DeepCopyObject() runtime.Object { +func (in *BoundSchema) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -807,13 +800,13 @@ func (in *BoundAPIResourceSchema) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BoundAPIResourceSchemaList) DeepCopyInto(out *BoundAPIResourceSchemaList) { +func (in *BoundSchemaList) DeepCopyInto(out *BoundSchemaList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]BoundAPIResourceSchema, len(*in)) + *out = make([]BoundSchema, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -821,18 +814,18 @@ func (in *BoundAPIResourceSchemaList) DeepCopyInto(out *BoundAPIResourceSchemaLi return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundAPIResourceSchemaList. -func (in *BoundAPIResourceSchemaList) DeepCopy() *BoundAPIResourceSchemaList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundSchemaList. +func (in *BoundSchemaList) DeepCopy() *BoundSchemaList { if in == nil { return nil } - out := new(BoundAPIResourceSchemaList) + out := new(BoundSchemaList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BoundAPIResourceSchemaList) DeepCopyObject() runtime.Object { +func (in *BoundSchemaList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -840,24 +833,41 @@ func (in *BoundAPIResourceSchemaList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BoundAPIResourceSchemaSpec) DeepCopyInto(out *BoundAPIResourceSchemaSpec) { +func (in *BoundSchemaReference) DeepCopyInto(out *BoundSchemaReference) { + *out = *in + out.GroupResource = in.GroupResource + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundSchemaReference. +func (in *BoundSchemaReference) DeepCopy() *BoundSchemaReference { + if in == nil { + return nil + } + out := new(BoundSchemaReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BoundSchemaSpec) DeepCopyInto(out *BoundSchemaSpec) { *out = *in - in.APIResourceSchemaCRDSpec.DeepCopyInto(&out.APIResourceSchemaCRDSpec) + in.APICRDSpec.DeepCopyInto(&out.APICRDSpec) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundAPIResourceSchemaSpec. -func (in *BoundAPIResourceSchemaSpec) DeepCopy() *BoundAPIResourceSchemaSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundSchemaSpec. +func (in *BoundSchemaSpec) DeepCopy() *BoundSchemaSpec { if in == nil { return nil } - out := new(BoundAPIResourceSchemaSpec) + out := new(BoundSchemaSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BoundAPIResourceSchemaStatus) DeepCopyInto(out *BoundAPIResourceSchemaStatus) { +func (in *BoundSchemaStatus) DeepCopyInto(out *BoundSchemaStatus) { *out = *in in.AcceptedNames.DeepCopyInto(&out.AcceptedNames) if in.StoredVersions != nil { @@ -875,48 +885,36 @@ func (in *BoundAPIResourceSchemaStatus) DeepCopyInto(out *BoundAPIResourceSchema return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundAPIResourceSchemaStatus. -func (in *BoundAPIResourceSchemaStatus) DeepCopy() *BoundAPIResourceSchemaStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundSchemaStatus. +func (in *BoundSchemaStatus) DeepCopy() *BoundSchemaStatus { if in == nil { return nil } - out := new(BoundAPIResourceSchemaStatus) + out := new(BoundSchemaStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BoundSchemaReference) DeepCopyInto(out *BoundSchemaReference) { - *out = *in - out.GroupResource = in.GroupResource - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundSchemaReference. -func (in *BoundSchemaReference) DeepCopy() *BoundSchemaReference { - if in == nil { - return nil +func (in ClaimableAPIs) DeepCopyInto(out *ClaimableAPIs) { + { + in := &in + *out = make(ClaimableAPIs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return } - out := new(BoundSchemaReference) - in.DeepCopyInto(out) - return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CRDVersionSchema) DeepCopyInto(out *CRDVersionSchema) { - *out = *in - in.OpenAPIV3Schema.DeepCopyInto(&out.OpenAPIV3Schema) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDVersionSchema. -func (in *CRDVersionSchema) DeepCopy() *CRDVersionSchema { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimableAPIs. +func (in ClaimableAPIs) DeepCopy() ClaimableAPIs { if in == nil { return nil } - out := new(CRDVersionSchema) + out := new(ClaimableAPIs) in.DeepCopyInto(out) - return out + return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -1061,6 +1059,36 @@ func (in *CustomResourceConversion) DeepCopy() *CustomResourceConversion { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ExportedSchemas) DeepCopyInto(out *ExportedSchemas) { + { + in := &in + *out = make(ExportedSchemas, len(*in)) + for key, val := range *in { + var outVal *BoundSchema + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(BoundSchema) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExportedSchemas. +func (in ExportedSchemas) DeepCopy() ExportedSchemas { + if in == nil { + return nil + } + out := new(ExportedSchemas) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GroupResource) DeepCopyInto(out *GroupResource) { *out = *in @@ -1077,6 +1105,43 @@ func (in *GroupResource) DeepCopy() *GroupResource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupVersionResource) DeepCopyInto(out *GroupVersionResource) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupVersionResource. +func (in *GroupVersionResource) DeepCopy() *GroupVersionResource { + if in == nil { + return nil + } + out := new(GroupVersionResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalAPI) DeepCopyInto(out *InternalAPI) { + *out = *in + in.Names.DeepCopyInto(&out.Names) + out.GroupVersionResource = in.GroupVersionResource + if in.Instance != nil { + out.Instance = in.Instance.DeepCopyObject() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalAPI. +func (in *InternalAPI) DeepCopy() *InternalAPI { + if in == nil { + return nil + } + out := new(InternalAPI) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalSecretKeyRef) DeepCopyInto(out *LocalSecretKeyRef) { *out = *in @@ -1125,6 +1190,61 @@ func (in *OAuth2CodeGrant) DeepCopy() *OAuth2CodeGrant { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PermissionClaim) DeepCopyInto(out *PermissionClaim) { + *out = *in + out.GroupResource = in.GroupResource + in.Selector.DeepCopyInto(&out.Selector) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PermissionClaim. +func (in *PermissionClaim) DeepCopy() *PermissionClaim { + if in == nil { + return nil + } + out := new(PermissionClaim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Selector) DeepCopyInto(out *Selector) { + *out = *in + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector. +func (in *Selector) DeepCopy() *Selector { + if in == nil { + return nil + } + out := new(Selector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SelectorResourceName) DeepCopyInto(out *SelectorResourceName) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelectorResourceName. +func (in *SelectorResourceName) DeepCopy() *SelectorResourceName { + if in == nil { + return nil + } + out := new(SelectorResourceName) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WebhookClientConfig) DeepCopyInto(out *WebhookClientConfig) { *out = *in diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/boundapiresourceschema.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/boundapiresourceschema.go deleted file mode 100644 index 5781d3925..000000000 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/boundapiresourceschema.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha2 - -import ( - context "context" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" - - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" - scheme "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/scheme" -) - -// BoundAPIResourceSchemasGetter has a method to return a BoundAPIResourceSchemaInterface. -// A group's client should implement this interface. -type BoundAPIResourceSchemasGetter interface { - BoundAPIResourceSchemas(namespace string) BoundAPIResourceSchemaInterface -} - -// BoundAPIResourceSchemaInterface has methods to work with BoundAPIResourceSchema resources. -type BoundAPIResourceSchemaInterface interface { - Create(ctx context.Context, boundAPIResourceSchema *kubebindv1alpha2.BoundAPIResourceSchema, opts v1.CreateOptions) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - Update(ctx context.Context, boundAPIResourceSchema *kubebindv1alpha2.BoundAPIResourceSchema, opts v1.UpdateOptions) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, boundAPIResourceSchema *kubebindv1alpha2.BoundAPIResourceSchema, opts v1.UpdateOptions) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.BoundAPIResourceSchemaList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *kubebindv1alpha2.BoundAPIResourceSchema, err error) - BoundAPIResourceSchemaExpansion -} - -// boundAPIResourceSchemas implements BoundAPIResourceSchemaInterface -type boundAPIResourceSchemas struct { - *gentype.ClientWithList[*kubebindv1alpha2.BoundAPIResourceSchema, *kubebindv1alpha2.BoundAPIResourceSchemaList] -} - -// newBoundAPIResourceSchemas returns a BoundAPIResourceSchemas -func newBoundAPIResourceSchemas(c *KubeBindV1alpha2Client, namespace string) *boundAPIResourceSchemas { - return &boundAPIResourceSchemas{ - gentype.NewClientWithList[*kubebindv1alpha2.BoundAPIResourceSchema, *kubebindv1alpha2.BoundAPIResourceSchemaList]( - "boundapiresourceschemas", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *kubebindv1alpha2.BoundAPIResourceSchema { return &kubebindv1alpha2.BoundAPIResourceSchema{} }, - func() *kubebindv1alpha2.BoundAPIResourceSchemaList { - return &kubebindv1alpha2.BoundAPIResourceSchemaList{} - }, - ), - } -} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/apiresourceschema.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/boundschema.go similarity index 51% rename from sdk/client/clientset/versioned/typed/kubebind/v1alpha2/apiresourceschema.go rename to sdk/client/clientset/versioned/typed/kubebind/v1alpha2/boundschema.go index d7d6a01ab..2e73da6e4 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/apiresourceschema.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/boundschema.go @@ -30,40 +30,42 @@ import ( scheme "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/scheme" ) -// APIResourceSchemasGetter has a method to return a APIResourceSchemaInterface. +// BoundSchemasGetter has a method to return a BoundSchemaInterface. // A group's client should implement this interface. -type APIResourceSchemasGetter interface { - APIResourceSchemas() APIResourceSchemaInterface +type BoundSchemasGetter interface { + BoundSchemas(namespace string) BoundSchemaInterface } -// APIResourceSchemaInterface has methods to work with APIResourceSchema resources. -type APIResourceSchemaInterface interface { - Create(ctx context.Context, aPIResourceSchema *kubebindv1alpha2.APIResourceSchema, opts v1.CreateOptions) (*kubebindv1alpha2.APIResourceSchema, error) - Update(ctx context.Context, aPIResourceSchema *kubebindv1alpha2.APIResourceSchema, opts v1.UpdateOptions) (*kubebindv1alpha2.APIResourceSchema, error) +// BoundSchemaInterface has methods to work with BoundSchema resources. +type BoundSchemaInterface interface { + Create(ctx context.Context, boundSchema *kubebindv1alpha2.BoundSchema, opts v1.CreateOptions) (*kubebindv1alpha2.BoundSchema, error) + Update(ctx context.Context, boundSchema *kubebindv1alpha2.BoundSchema, opts v1.UpdateOptions) (*kubebindv1alpha2.BoundSchema, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, boundSchema *kubebindv1alpha2.BoundSchema, opts v1.UpdateOptions) (*kubebindv1alpha2.BoundSchema, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*kubebindv1alpha2.APIResourceSchema, error) - List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.APIResourceSchemaList, error) + Get(ctx context.Context, name string, opts v1.GetOptions) (*kubebindv1alpha2.BoundSchema, error) + List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.BoundSchemaList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *kubebindv1alpha2.APIResourceSchema, err error) - APIResourceSchemaExpansion + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *kubebindv1alpha2.BoundSchema, err error) + BoundSchemaExpansion } -// aPIResourceSchemas implements APIResourceSchemaInterface -type aPIResourceSchemas struct { - *gentype.ClientWithList[*kubebindv1alpha2.APIResourceSchema, *kubebindv1alpha2.APIResourceSchemaList] +// boundSchemas implements BoundSchemaInterface +type boundSchemas struct { + *gentype.ClientWithList[*kubebindv1alpha2.BoundSchema, *kubebindv1alpha2.BoundSchemaList] } -// newAPIResourceSchemas returns a APIResourceSchemas -func newAPIResourceSchemas(c *KubeBindV1alpha2Client) *aPIResourceSchemas { - return &aPIResourceSchemas{ - gentype.NewClientWithList[*kubebindv1alpha2.APIResourceSchema, *kubebindv1alpha2.APIResourceSchemaList]( - "apiresourceschemas", +// newBoundSchemas returns a BoundSchemas +func newBoundSchemas(c *KubeBindV1alpha2Client, namespace string) *boundSchemas { + return &boundSchemas{ + gentype.NewClientWithList[*kubebindv1alpha2.BoundSchema, *kubebindv1alpha2.BoundSchemaList]( + "boundschemas", c.RESTClient(), scheme.ParameterCodec, - "", - func() *kubebindv1alpha2.APIResourceSchema { return &kubebindv1alpha2.APIResourceSchema{} }, - func() *kubebindv1alpha2.APIResourceSchemaList { return &kubebindv1alpha2.APIResourceSchemaList{} }, + namespace, + func() *kubebindv1alpha2.BoundSchema { return &kubebindv1alpha2.BoundSchema{} }, + func() *kubebindv1alpha2.BoundSchemaList { return &kubebindv1alpha2.BoundSchemaList{} }, ), } } diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_boundapiresourceschema.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_boundapiresourceschema.go deleted file mode 100644 index d91d8c8a6..000000000 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_boundapiresourceschema.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - gentype "k8s.io/client-go/gentype" - - v1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/typed/kubebind/v1alpha2" -) - -// fakeBoundAPIResourceSchemas implements BoundAPIResourceSchemaInterface -type fakeBoundAPIResourceSchemas struct { - *gentype.FakeClientWithList[*v1alpha2.BoundAPIResourceSchema, *v1alpha2.BoundAPIResourceSchemaList] - Fake *FakeKubeBindV1alpha2 -} - -func newFakeBoundAPIResourceSchemas(fake *FakeKubeBindV1alpha2, namespace string) kubebindv1alpha2.BoundAPIResourceSchemaInterface { - return &fakeBoundAPIResourceSchemas{ - gentype.NewFakeClientWithList[*v1alpha2.BoundAPIResourceSchema, *v1alpha2.BoundAPIResourceSchemaList]( - fake.Fake, - namespace, - v1alpha2.SchemeGroupVersion.WithResource("boundapiresourceschemas"), - v1alpha2.SchemeGroupVersion.WithKind("BoundAPIResourceSchema"), - func() *v1alpha2.BoundAPIResourceSchema { return &v1alpha2.BoundAPIResourceSchema{} }, - func() *v1alpha2.BoundAPIResourceSchemaList { return &v1alpha2.BoundAPIResourceSchemaList{} }, - func(dst, src *v1alpha2.BoundAPIResourceSchemaList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha2.BoundAPIResourceSchemaList) []*v1alpha2.BoundAPIResourceSchema { - return gentype.ToPointerSlice(list.Items) - }, - func(list *v1alpha2.BoundAPIResourceSchemaList, items []*v1alpha2.BoundAPIResourceSchema) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_apiresourceschema.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_boundschema.go similarity index 51% rename from sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_apiresourceschema.go rename to sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_boundschema.go index 684c48354..19b73ab41 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_apiresourceschema.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_boundschema.go @@ -25,26 +25,26 @@ import ( kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/typed/kubebind/v1alpha2" ) -// fakeAPIResourceSchemas implements APIResourceSchemaInterface -type fakeAPIResourceSchemas struct { - *gentype.FakeClientWithList[*v1alpha2.APIResourceSchema, *v1alpha2.APIResourceSchemaList] +// fakeBoundSchemas implements BoundSchemaInterface +type fakeBoundSchemas struct { + *gentype.FakeClientWithList[*v1alpha2.BoundSchema, *v1alpha2.BoundSchemaList] Fake *FakeKubeBindV1alpha2 } -func newFakeAPIResourceSchemas(fake *FakeKubeBindV1alpha2) kubebindv1alpha2.APIResourceSchemaInterface { - return &fakeAPIResourceSchemas{ - gentype.NewFakeClientWithList[*v1alpha2.APIResourceSchema, *v1alpha2.APIResourceSchemaList]( +func newFakeBoundSchemas(fake *FakeKubeBindV1alpha2, namespace string) kubebindv1alpha2.BoundSchemaInterface { + return &fakeBoundSchemas{ + gentype.NewFakeClientWithList[*v1alpha2.BoundSchema, *v1alpha2.BoundSchemaList]( fake.Fake, - "", - v1alpha2.SchemeGroupVersion.WithResource("apiresourceschemas"), - v1alpha2.SchemeGroupVersion.WithKind("APIResourceSchema"), - func() *v1alpha2.APIResourceSchema { return &v1alpha2.APIResourceSchema{} }, - func() *v1alpha2.APIResourceSchemaList { return &v1alpha2.APIResourceSchemaList{} }, - func(dst, src *v1alpha2.APIResourceSchemaList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha2.APIResourceSchemaList) []*v1alpha2.APIResourceSchema { + namespace, + v1alpha2.SchemeGroupVersion.WithResource("boundschemas"), + v1alpha2.SchemeGroupVersion.WithKind("BoundSchema"), + func() *v1alpha2.BoundSchema { return &v1alpha2.BoundSchema{} }, + func() *v1alpha2.BoundSchemaList { return &v1alpha2.BoundSchemaList{} }, + func(dst, src *v1alpha2.BoundSchemaList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha2.BoundSchemaList) []*v1alpha2.BoundSchema { return gentype.ToPointerSlice(list.Items) }, - func(list *v1alpha2.APIResourceSchemaList, items []*v1alpha2.APIResourceSchema) { + func(list *v1alpha2.BoundSchemaList, items []*v1alpha2.BoundSchema) { list.Items = gentype.FromPointerSlice(items) }, ), diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go index 17853649a..3a48e9c60 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go @@ -29,10 +29,6 @@ type FakeKubeBindV1alpha2 struct { *testing.Fake } -func (c *FakeKubeBindV1alpha2) APIResourceSchemas() v1alpha2.APIResourceSchemaInterface { - return newFakeAPIResourceSchemas(c) -} - func (c *FakeKubeBindV1alpha2) APIServiceBindings() v1alpha2.APIServiceBindingInterface { return newFakeAPIServiceBindings(c) } @@ -49,8 +45,8 @@ func (c *FakeKubeBindV1alpha2) APIServiceNamespaces(namespace string) v1alpha2.A return newFakeAPIServiceNamespaces(c, namespace) } -func (c *FakeKubeBindV1alpha2) BoundAPIResourceSchemas(namespace string) v1alpha2.BoundAPIResourceSchemaInterface { - return newFakeBoundAPIResourceSchemas(c, namespace) +func (c *FakeKubeBindV1alpha2) BoundSchemas(namespace string) v1alpha2.BoundSchemaInterface { + return newFakeBoundSchemas(c, namespace) } func (c *FakeKubeBindV1alpha2) ClusterBindings(namespace string) v1alpha2.ClusterBindingInterface { diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go index 5c2fd1ed5..d0968607d 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go @@ -18,8 +18,6 @@ limitations under the License. package v1alpha2 -type APIResourceSchemaExpansion interface{} - type APIServiceBindingExpansion interface{} type APIServiceExportExpansion interface{} @@ -28,6 +26,6 @@ type APIServiceExportRequestExpansion interface{} type APIServiceNamespaceExpansion interface{} -type BoundAPIResourceSchemaExpansion interface{} +type BoundSchemaExpansion interface{} type ClusterBindingExpansion interface{} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go index 72842b593..f215f2faa 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go @@ -29,12 +29,11 @@ import ( type KubeBindV1alpha2Interface interface { RESTClient() rest.Interface - APIResourceSchemasGetter APIServiceBindingsGetter APIServiceExportsGetter APIServiceExportRequestsGetter APIServiceNamespacesGetter - BoundAPIResourceSchemasGetter + BoundSchemasGetter ClusterBindingsGetter } @@ -43,10 +42,6 @@ type KubeBindV1alpha2Client struct { restClient rest.Interface } -func (c *KubeBindV1alpha2Client) APIResourceSchemas() APIResourceSchemaInterface { - return newAPIResourceSchemas(c) -} - func (c *KubeBindV1alpha2Client) APIServiceBindings() APIServiceBindingInterface { return newAPIServiceBindings(c) } @@ -63,8 +58,8 @@ func (c *KubeBindV1alpha2Client) APIServiceNamespaces(namespace string) APIServi return newAPIServiceNamespaces(c, namespace) } -func (c *KubeBindV1alpha2Client) BoundAPIResourceSchemas(namespace string) BoundAPIResourceSchemaInterface { - return newBoundAPIResourceSchemas(c, namespace) +func (c *KubeBindV1alpha2Client) BoundSchemas(namespace string) BoundSchemaInterface { + return newBoundSchemas(c, namespace) } func (c *KubeBindV1alpha2Client) ClusterBindings(namespace string) ClusterBindingInterface { diff --git a/sdk/client/informers/externalversions/generic.go b/sdk/client/informers/externalversions/generic.go index ec0f17da2..eb77273d5 100644 --- a/sdk/client/informers/externalversions/generic.go +++ b/sdk/client/informers/externalversions/generic.go @@ -67,8 +67,6 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha1().ClusterBindings().Informer()}, nil // Group=kube-bind.io, Version=v1alpha2 - case v1alpha2.SchemeGroupVersion.WithResource("apiresourceschemas"): - return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIResourceSchemas().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("apiservicebindings"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceBindings().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("apiserviceexports"): @@ -77,8 +75,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceExportRequests().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("apiservicenamespaces"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceNamespaces().Informer()}, nil - case v1alpha2.SchemeGroupVersion.WithResource("boundapiresourceschemas"): - return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().BoundAPIResourceSchemas().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("boundschemas"): + return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().BoundSchemas().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusterbindings"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().ClusterBindings().Informer()}, nil diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/boundapiresourceschema.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/boundapiresourceschema.go deleted file mode 100644 index 4a0a451f6..000000000 --- a/sdk/client/informers/externalversions/kubebind/v1alpha2/boundapiresourceschema.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by informer-gen. DO NOT EDIT. - -package v1alpha2 - -import ( - context "context" - time "time" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - watch "k8s.io/apimachinery/pkg/watch" - cache "k8s.io/client-go/tools/cache" - - apiskubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" - versioned "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" - internalinterfaces "github.com/kube-bind/kube-bind/sdk/client/informers/externalversions/internalinterfaces" - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2" -) - -// BoundAPIResourceSchemaInformer provides access to a shared informer and lister for -// BoundAPIResourceSchemas. -type BoundAPIResourceSchemaInformer interface { - Informer() cache.SharedIndexInformer - Lister() kubebindv1alpha2.BoundAPIResourceSchemaLister -} - -type boundAPIResourceSchemaInformer struct { - factory internalinterfaces.SharedInformerFactory - tweakListOptions internalinterfaces.TweakListOptionsFunc - namespace string -} - -// NewBoundAPIResourceSchemaInformer constructs a new informer for BoundAPIResourceSchema type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewBoundAPIResourceSchemaInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredBoundAPIResourceSchemaInformer(client, namespace, resyncPeriod, indexers, nil) -} - -// NewFilteredBoundAPIResourceSchemaInformer constructs a new informer for BoundAPIResourceSchema type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewFilteredBoundAPIResourceSchemaInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.KubeBindV1alpha2().BoundAPIResourceSchemas(namespace).List(context.Background(), options) - }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.KubeBindV1alpha2().BoundAPIResourceSchemas(namespace).Watch(context.Background(), options) - }, - ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.KubeBindV1alpha2().BoundAPIResourceSchemas(namespace).List(ctx, options) - }, - WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.KubeBindV1alpha2().BoundAPIResourceSchemas(namespace).Watch(ctx, options) - }, - }, - &apiskubebindv1alpha2.BoundAPIResourceSchema{}, - resyncPeriod, - indexers, - ) -} - -func (f *boundAPIResourceSchemaInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredBoundAPIResourceSchemaInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) -} - -func (f *boundAPIResourceSchemaInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apiskubebindv1alpha2.BoundAPIResourceSchema{}, f.defaultInformer) -} - -func (f *boundAPIResourceSchemaInformer) Lister() kubebindv1alpha2.BoundAPIResourceSchemaLister { - return kubebindv1alpha2.NewBoundAPIResourceSchemaLister(f.Informer().GetIndexer()) -} diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/apiresourceschema.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/boundschema.go similarity index 57% rename from sdk/client/informers/externalversions/kubebind/v1alpha2/apiresourceschema.go rename to sdk/client/informers/externalversions/kubebind/v1alpha2/boundschema.go index 06ecc7db0..d56d2b0dd 100644 --- a/sdk/client/informers/externalversions/kubebind/v1alpha2/apiresourceschema.go +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/boundschema.go @@ -33,70 +33,71 @@ import ( kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2" ) -// APIResourceSchemaInformer provides access to a shared informer and lister for -// APIResourceSchemas. -type APIResourceSchemaInformer interface { +// BoundSchemaInformer provides access to a shared informer and lister for +// BoundSchemas. +type BoundSchemaInformer interface { Informer() cache.SharedIndexInformer - Lister() kubebindv1alpha2.APIResourceSchemaLister + Lister() kubebindv1alpha2.BoundSchemaLister } -type aPIResourceSchemaInformer struct { +type boundSchemaInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string } -// NewAPIResourceSchemaInformer constructs a new informer for APIResourceSchema type. +// NewBoundSchemaInformer constructs a new informer for BoundSchema type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewAPIResourceSchemaInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredAPIResourceSchemaInformer(client, resyncPeriod, indexers, nil) +func NewBoundSchemaInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredBoundSchemaInformer(client, namespace, resyncPeriod, indexers, nil) } -// NewFilteredAPIResourceSchemaInformer constructs a new informer for APIResourceSchema type. +// NewFilteredBoundSchemaInformer constructs a new informer for BoundSchema type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredAPIResourceSchemaInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredBoundSchemaInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.KubeBindV1alpha2().APIResourceSchemas().List(context.Background(), options) + return client.KubeBindV1alpha2().BoundSchemas(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.KubeBindV1alpha2().APIResourceSchemas().Watch(context.Background(), options) + return client.KubeBindV1alpha2().BoundSchemas(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.KubeBindV1alpha2().APIResourceSchemas().List(ctx, options) + return client.KubeBindV1alpha2().BoundSchemas(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.KubeBindV1alpha2().APIResourceSchemas().Watch(ctx, options) + return client.KubeBindV1alpha2().BoundSchemas(namespace).Watch(ctx, options) }, }, - &apiskubebindv1alpha2.APIResourceSchema{}, + &apiskubebindv1alpha2.BoundSchema{}, resyncPeriod, indexers, ) } -func (f *aPIResourceSchemaInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredAPIResourceSchemaInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *boundSchemaInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredBoundSchemaInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *aPIResourceSchemaInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apiskubebindv1alpha2.APIResourceSchema{}, f.defaultInformer) +func (f *boundSchemaInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiskubebindv1alpha2.BoundSchema{}, f.defaultInformer) } -func (f *aPIResourceSchemaInformer) Lister() kubebindv1alpha2.APIResourceSchemaLister { - return kubebindv1alpha2.NewAPIResourceSchemaLister(f.Informer().GetIndexer()) +func (f *boundSchemaInformer) Lister() kubebindv1alpha2.BoundSchemaLister { + return kubebindv1alpha2.NewBoundSchemaLister(f.Informer().GetIndexer()) } diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go index 68bf0f176..e4abc8bb5 100644 --- a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go @@ -24,8 +24,6 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { - // APIResourceSchemas returns a APIResourceSchemaInformer. - APIResourceSchemas() APIResourceSchemaInformer // APIServiceBindings returns a APIServiceBindingInformer. APIServiceBindings() APIServiceBindingInformer // APIServiceExports returns a APIServiceExportInformer. @@ -34,8 +32,8 @@ type Interface interface { APIServiceExportRequests() APIServiceExportRequestInformer // APIServiceNamespaces returns a APIServiceNamespaceInformer. APIServiceNamespaces() APIServiceNamespaceInformer - // BoundAPIResourceSchemas returns a BoundAPIResourceSchemaInformer. - BoundAPIResourceSchemas() BoundAPIResourceSchemaInformer + // BoundSchemas returns a BoundSchemaInformer. + BoundSchemas() BoundSchemaInformer // ClusterBindings returns a ClusterBindingInformer. ClusterBindings() ClusterBindingInformer } @@ -51,11 +49,6 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } -// APIResourceSchemas returns a APIResourceSchemaInformer. -func (v *version) APIResourceSchemas() APIResourceSchemaInformer { - return &aPIResourceSchemaInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} -} - // APIServiceBindings returns a APIServiceBindingInformer. func (v *version) APIServiceBindings() APIServiceBindingInformer { return &aPIServiceBindingInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} @@ -76,9 +69,9 @@ func (v *version) APIServiceNamespaces() APIServiceNamespaceInformer { return &aPIServiceNamespaceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } -// BoundAPIResourceSchemas returns a BoundAPIResourceSchemaInformer. -func (v *version) BoundAPIResourceSchemas() BoundAPIResourceSchemaInformer { - return &boundAPIResourceSchemaInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +// BoundSchemas returns a BoundSchemaInformer. +func (v *version) BoundSchemas() BoundSchemaInformer { + return &boundSchemaInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // ClusterBindings returns a ClusterBindingInformer. diff --git a/sdk/client/listers/kubebind/v1alpha2/apiresourceschema.go b/sdk/client/listers/kubebind/v1alpha2/apiresourceschema.go deleted file mode 100644 index 06d40f0d4..000000000 --- a/sdk/client/listers/kubebind/v1alpha2/apiresourceschema.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha2 - -import ( - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" - - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" -) - -// APIResourceSchemaLister helps list APIResourceSchemas. -// All objects returned here must be treated as read-only. -type APIResourceSchemaLister interface { - // List lists all APIResourceSchemas in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*kubebindv1alpha2.APIResourceSchema, err error) - // Get retrieves the APIResourceSchema from the index for a given name. - // Objects returned here must be treated as read-only. - Get(name string) (*kubebindv1alpha2.APIResourceSchema, error) - APIResourceSchemaListerExpansion -} - -// aPIResourceSchemaLister implements the APIResourceSchemaLister interface. -type aPIResourceSchemaLister struct { - listers.ResourceIndexer[*kubebindv1alpha2.APIResourceSchema] -} - -// NewAPIResourceSchemaLister returns a new APIResourceSchemaLister. -func NewAPIResourceSchemaLister(indexer cache.Indexer) APIResourceSchemaLister { - return &aPIResourceSchemaLister{listers.New[*kubebindv1alpha2.APIResourceSchema](indexer, kubebindv1alpha2.Resource("apiresourceschema"))} -} diff --git a/sdk/client/listers/kubebind/v1alpha2/boundapiresourceschema.go b/sdk/client/listers/kubebind/v1alpha2/boundapiresourceschema.go deleted file mode 100644 index 595e1536f..000000000 --- a/sdk/client/listers/kubebind/v1alpha2/boundapiresourceschema.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha2 - -import ( - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" - - kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" -) - -// BoundAPIResourceSchemaLister helps list BoundAPIResourceSchemas. -// All objects returned here must be treated as read-only. -type BoundAPIResourceSchemaLister interface { - // List lists all BoundAPIResourceSchemas in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*kubebindv1alpha2.BoundAPIResourceSchema, err error) - // BoundAPIResourceSchemas returns an object that can list and get BoundAPIResourceSchemas. - BoundAPIResourceSchemas(namespace string) BoundAPIResourceSchemaNamespaceLister - BoundAPIResourceSchemaListerExpansion -} - -// boundAPIResourceSchemaLister implements the BoundAPIResourceSchemaLister interface. -type boundAPIResourceSchemaLister struct { - listers.ResourceIndexer[*kubebindv1alpha2.BoundAPIResourceSchema] -} - -// NewBoundAPIResourceSchemaLister returns a new BoundAPIResourceSchemaLister. -func NewBoundAPIResourceSchemaLister(indexer cache.Indexer) BoundAPIResourceSchemaLister { - return &boundAPIResourceSchemaLister{listers.New[*kubebindv1alpha2.BoundAPIResourceSchema](indexer, kubebindv1alpha2.Resource("boundapiresourceschema"))} -} - -// BoundAPIResourceSchemas returns an object that can list and get BoundAPIResourceSchemas. -func (s *boundAPIResourceSchemaLister) BoundAPIResourceSchemas(namespace string) BoundAPIResourceSchemaNamespaceLister { - return boundAPIResourceSchemaNamespaceLister{listers.NewNamespaced[*kubebindv1alpha2.BoundAPIResourceSchema](s.ResourceIndexer, namespace)} -} - -// BoundAPIResourceSchemaNamespaceLister helps list and get BoundAPIResourceSchemas. -// All objects returned here must be treated as read-only. -type BoundAPIResourceSchemaNamespaceLister interface { - // List lists all BoundAPIResourceSchemas in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*kubebindv1alpha2.BoundAPIResourceSchema, err error) - // Get retrieves the BoundAPIResourceSchema from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*kubebindv1alpha2.BoundAPIResourceSchema, error) - BoundAPIResourceSchemaNamespaceListerExpansion -} - -// boundAPIResourceSchemaNamespaceLister implements the BoundAPIResourceSchemaNamespaceLister -// interface. -type boundAPIResourceSchemaNamespaceLister struct { - listers.ResourceIndexer[*kubebindv1alpha2.BoundAPIResourceSchema] -} diff --git a/sdk/client/listers/kubebind/v1alpha2/boundschema.go b/sdk/client/listers/kubebind/v1alpha2/boundschema.go new file mode 100644 index 000000000..819029d4d --- /dev/null +++ b/sdk/client/listers/kubebind/v1alpha2/boundschema.go @@ -0,0 +1,71 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +// BoundSchemaLister helps list BoundSchemas. +// All objects returned here must be treated as read-only. +type BoundSchemaLister interface { + // List lists all BoundSchemas in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.BoundSchema, err error) + // BoundSchemas returns an object that can list and get BoundSchemas. + BoundSchemas(namespace string) BoundSchemaNamespaceLister + BoundSchemaListerExpansion +} + +// boundSchemaLister implements the BoundSchemaLister interface. +type boundSchemaLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.BoundSchema] +} + +// NewBoundSchemaLister returns a new BoundSchemaLister. +func NewBoundSchemaLister(indexer cache.Indexer) BoundSchemaLister { + return &boundSchemaLister{listers.New[*kubebindv1alpha2.BoundSchema](indexer, kubebindv1alpha2.Resource("boundschema"))} +} + +// BoundSchemas returns an object that can list and get BoundSchemas. +func (s *boundSchemaLister) BoundSchemas(namespace string) BoundSchemaNamespaceLister { + return boundSchemaNamespaceLister{listers.NewNamespaced[*kubebindv1alpha2.BoundSchema](s.ResourceIndexer, namespace)} +} + +// BoundSchemaNamespaceLister helps list and get BoundSchemas. +// All objects returned here must be treated as read-only. +type BoundSchemaNamespaceLister interface { + // List lists all BoundSchemas in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.BoundSchema, err error) + // Get retrieves the BoundSchema from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*kubebindv1alpha2.BoundSchema, error) + BoundSchemaNamespaceListerExpansion +} + +// boundSchemaNamespaceLister implements the BoundSchemaNamespaceLister +// interface. +type boundSchemaNamespaceLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.BoundSchema] +} diff --git a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go index a8959ba58..3aaa58aa1 100644 --- a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go +++ b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go @@ -18,10 +18,6 @@ limitations under the License. package v1alpha2 -// APIResourceSchemaListerExpansion allows custom methods to be added to -// APIResourceSchemaLister. -type APIResourceSchemaListerExpansion interface{} - // APIServiceBindingListerExpansion allows custom methods to be added to // APIServiceBindingLister. type APIServiceBindingListerExpansion interface{} @@ -50,13 +46,13 @@ type APIServiceNamespaceListerExpansion interface{} // APIServiceNamespaceNamespaceLister. type APIServiceNamespaceNamespaceListerExpansion interface{} -// BoundAPIResourceSchemaListerExpansion allows custom methods to be added to -// BoundAPIResourceSchemaLister. -type BoundAPIResourceSchemaListerExpansion interface{} +// BoundSchemaListerExpansion allows custom methods to be added to +// BoundSchemaLister. +type BoundSchemaListerExpansion interface{} -// BoundAPIResourceSchemaNamespaceListerExpansion allows custom methods to be added to -// BoundAPIResourceSchemaNamespaceLister. -type BoundAPIResourceSchemaNamespaceListerExpansion interface{} +// BoundSchemaNamespaceListerExpansion allows custom methods to be added to +// BoundSchemaNamespaceLister. +type BoundSchemaNamespaceListerExpansion interface{} // ClusterBindingListerExpansion allows custom methods to be added to // ClusterBindingLister. diff --git a/test/e2e/bind/fixtures/provider/apiresourceschema-foo.yaml b/test/e2e/bind/fixtures/provider/apiresourceschema-foo.yaml deleted file mode 100644 index 14f9cb50a..000000000 --- a/test/e2e/bind/fixtures/provider/apiresourceschema-foo.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: kube-bind.io/v1alpha2 -kind: APIResourceSchema -metadata: - name: foos.bar.io -spec: - group: bar.io - informerScope: Namespaced - names: - kind: Foo - plural: foos - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - spec: - properties: - deploymentName: - type: string - replicas: - maximum: 10 - minimum: 1 - type: integer - type: object - status: - properties: - availableReplicas: - type: integer - phase: - enum: - - Pending - - Running - - Succeeded - - Failed - - Unknown - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/test/e2e/bind/fixtures/provider/apiresourceschema-mangodb.yaml b/test/e2e/bind/fixtures/provider/apiresourceschema-mangodb.yaml deleted file mode 100644 index c16eb33a9..000000000 --- a/test/e2e/bind/fixtures/provider/apiresourceschema-mangodb.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: kube-bind.io/v1alpha2 -kind: APIResourceSchema -metadata: - name: mangodbs.mangodb.com -spec: - group: mangodb.com - informerScope: Namespaced - names: - kind: MangoDB - listKind: MangoDBList - plural: mangodbs - singular: mangodb - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - spec: - properties: - backup: - default: false - type: boolean - region: - default: us-east-1 - minLength: 1 - type: string - tier: - default: Shared - enum: - - Dedicated - - Shared - type: string - tokenSecret: - minLength: 1 - type: string - required: - - tokenSecret - type: object - status: - properties: - phase: - enum: - - Pending - - Running - - Succeeded - - Failed - - Unknown - type: string - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index 3ee266312..c01535fce 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -18,6 +18,7 @@ package bind import ( "context" + "encoding/json" "fmt" "strings" "testing" @@ -25,6 +26,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/headzoo/surf.v1" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,19 +45,25 @@ import ( func TestClusterScoped(t *testing.T) { t.Parallel() - // cluster scoped resource, with cluster scoped informers - testHappyCase(t, apiextensionsv1.ClusterScoped, kubebindv1alpha2.ClusterScope) + testHappyCase(t, apiextensionsv1.ClusterScoped, kubebindv1alpha2.ClusterScope, false) + testHappyCase(t, apiextensionsv1.ClusterScoped, kubebindv1alpha2.ClusterScope, true) } func TestNamespacedScoped(t *testing.T) { t.Parallel() - // namespaced resource, with namespace scoped informers - testHappyCase(t, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.NamespacedScope) - // namespaced resource, but with cluster scoped informers - testHappyCase(t, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.ClusterScope) + + testHappyCase(t, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.NamespacedScope, false) + testHappyCase(t, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.NamespacedScope, true) + testHappyCase(t, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.ClusterScope, false) + testHappyCase(t, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.ClusterScope, true) } -func testHappyCase(t *testing.T, resourceScope apiextensionsv1.ResourceScope, informerScope kubebindv1alpha2.InformerScope) { +func testHappyCase( + t *testing.T, + resourceScope apiextensionsv1.ResourceScope, + informerScope kubebindv1alpha2.InformerScope, + withPermissionClaims bool, +) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -65,7 +73,7 @@ func testHappyCase(t *testing.T, resourceScope apiextensionsv1.ResourceScope, in t.Logf("Starting backend with random port") addr, _ := framework.StartBackend(t, providerConfig, "--kubeconfig="+providerKubeconfig, "--listen-address=:0", "--consumer-scope="+string(informerScope)) - t.Logf("Creating APIResourceSchemas on provider side") + t.Logf("Creating CRD on provider side") providerfixtures.Bootstrap(t, framework.DiscoveryClient(t, providerConfig), framework.DynamicClient(t, providerConfig), nil) t.Logf("Creating consumer workspace and starting konnector") @@ -80,6 +88,9 @@ func testHappyCase(t *testing.T, resourceScope apiextensionsv1.ResourceScope, in consumerClient := framework.DynamicClient(t, consumerConfig).Resource(serviceGVR) providerClient := framework.DynamicClient(t, providerConfig).Resource(serviceGVR) + consumerCoreClient := framework.KubeClient(t, consumerConfig).CoreV1() + providerCoreClient := framework.KubeClient(t, providerConfig).CoreV1() + mangodbInstance := ` apiVersion: mangodb.com/v1alpha1 kind: MangoDB @@ -125,6 +136,45 @@ spec: framework.Bind(t, iostreams, authURLCh, invocations, fmt.Sprintf("http://%s/exports", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector") inv := <-invocations requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "-", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) + + // If we are in permissions claims mode - add configmaps & secrets + if withPermissionClaims { + var request kubebindv1alpha2.APIServiceExportRequest + err := json.Unmarshal(inv.Stdin, &request) + require.NoError(t, err) + request.Spec.PermissionClaims = []kubebindv1alpha2.PermissionClaim{ + { + GroupResource: kubebindv1alpha2.GroupResource{ + Group: "", + Resource: "configmaps", + }, + Selector: kubebindv1alpha2.Selector{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "configmaps", + }, + }, + }, + }, + { + GroupResource: kubebindv1alpha2.GroupResource{ + Group: "", + Resource: "secrets", + }, + Selector: kubebindv1alpha2.Selector{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "secrets", + }, + }, + }, + }, + } + payload, err := json.Marshal(request) + require.NoError(t, err) + inv.Stdin = payload + } + framework.BindAPIService(t, inv.Stdin, "", inv.Args...) t.Logf("Waiting for %s CRD to be created on consumer side", serviceGVR.Resource) @@ -166,6 +216,109 @@ spec: } }, }, + { + name: "create secrets and configmaps if permission claims enabled", + step: func(t *testing.T) { + if !withPermissionClaims { + t.Skip("Skipping permission claims test when permission claims are disabled") + return + } + + t.Logf("Creating configmap on consumer side") + configMapData := map[string]string{ + "config.yaml": "test: value", + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: downstreamNs, + Labels: map[string]string{ + "app": "configmaps", + }, + }, + Data: configMapData, + } + _, err := consumerCoreClient.ConfigMaps(downstreamNs).Create(ctx, configMap, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Logf("Creating secret on consumer side") + secretData := map[string][]byte{ + "password": []byte("secret-password"), + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: downstreamNs, + Labels: map[string]string{ + "app": "secrets", + }, + }, + Data: secretData, + } + _, err = consumerCoreClient.Secrets(downstreamNs).Create(ctx, secret, metav1.CreateOptions{}) + require.NoError(t, err) + }, + }, + { + name: "verify secrets and configmaps are synced to provider", + step: func(t *testing.T) { + if !withPermissionClaims { + t.Skip("Skipping permission claims test when permission claims are disabled") + return + } + + t.Logf("Waiting for configmap to be synced to provider side") + require.Eventually(t, func() bool { + _, err := providerCoreClient.ConfigMaps(upstreamNS).Get(ctx, "test-configmap", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for configmap to be synced to provider side") + + t.Logf("Waiting for secret to be synced to provider side") + require.Eventually(t, func() bool { + _, err := providerCoreClient.Secrets(upstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be synced to provider side") + + t.Logf("Verifying configmap data is correct") + providerConfigMap, err := providerCoreClient.ConfigMaps(upstreamNS).Get(ctx, "test-configmap", metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "test: value", providerConfigMap.Data["config.yaml"]) + + t.Logf("Verifying secret data is correct") + providerSecret, err := providerCoreClient.Secrets(upstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, []byte("secret-password"), providerSecret.Data["password"]) + }, + }, + { + name: "verify secrets and configmaps are deleted when removed from consumer", + step: func(t *testing.T) { + if !withPermissionClaims { + t.Skip("Skipping permission claims test when permission claims are disabled") + return + } + + t.Logf("Deleting configmap from consumer side") + err := consumerCoreClient.ConfigMaps(downstreamNs).Delete(ctx, "test-configmap", metav1.DeleteOptions{}) + require.NoError(t, err) + + t.Logf("Deleting secret from consumer side") + err = consumerCoreClient.Secrets(downstreamNs).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + require.NoError(t, err) + + t.Logf("Waiting for configmap to be deleted from provider side") + require.Eventually(t, func() bool { + _, err := providerCoreClient.ConfigMaps(upstreamNS).Get(ctx, "test-configmap", metav1.GetOptions{}) + return errors.IsNotFound(err) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for configmap to be deleted from provider side") + + t.Logf("Waiting for secret to be deleted from provider side") + require.Eventually(t, func() bool { + _, err := providerCoreClient.Secrets(upstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return errors.IsNotFound(err) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted from provider side") + }, + }, { name: "instance deleted upstream is recreated", step: func(t *testing.T) { diff --git a/test/e2e/framework/backend.go b/test/e2e/framework/backend.go index 0903c65ac..84b6e8c24 100644 --- a/test/e2e/framework/backend.go +++ b/test/e2e/framework/backend.go @@ -62,12 +62,11 @@ func StartBackendWithoutDefaultArgs(t *testing.T, clientConfig *rest.Config, arg require.NoError(t, err) err = crd.Create(ctx, crdClient.ApiextensionsV1().CustomResourceDefinitions(), - metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiresourceschemas"}, - metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "boundapiresourceschemas"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "clusterbindings"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiserviceexports"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiservicenamespaces"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiserviceexportrequests"}, + metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "boundschemas"}, ) require.NoError(t, err) diff --git a/test/e2e/framework/bind.go b/test/e2e/framework/bind.go index 28869467d..86c126f6f 100644 --- a/test/e2e/framework/bind.go +++ b/test/e2e/framework/bind.go @@ -53,7 +53,6 @@ func Bind(t *testing.T, iostreams genericclioptions.IOStreams, authURLCh chan<- require.NoError(t, err) err = opts.Validate() require.NoError(t, err) - opts.Runner = func(cmd *exec.Cmd) error { bs, err := io.ReadAll(cmd.Stdin) if err != nil { diff --git a/test/e2e/framework/browser.go b/test/e2e/framework/browser.go index 80d4c96ae..6772b22ab 100644 --- a/test/e2e/framework/browser.go +++ b/test/e2e/framework/browser.go @@ -17,6 +17,7 @@ limitations under the License. package framework import ( + "strings" "testing" "time" @@ -27,7 +28,7 @@ import ( func BrowserEventuallyAtPath(t *testing.T, browser *browser.Browser, path string) { require.Eventuallyf(t, func() bool { - if browser.Url().Path == path { + if strings.Contains(browser.Url().Path, path) { t.Logf("Browser is at %s, waiting for path %s", browser.Url(), path) return true } diff --git a/test/e2e/framework/clients.go b/test/e2e/framework/clients.go index d5cf9d9e1..ad8d18df9 100644 --- a/test/e2e/framework/clients.go +++ b/test/e2e/framework/clients.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) func DynamicClient(t *testing.T, config *rest.Config) dynamic.Interface { @@ -50,3 +51,9 @@ func DiscoveryClient(t *testing.T, config *rest.Config) discovery.DiscoveryInter require.NoError(t, err) return c } + +func NewRESTConfig(t *testing.T, kubeconfig string) *rest.Config { + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + require.NoError(t, err, "Failed to build config from kubeconfig file") + return config +} diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 000000000..55117e7cc --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,17 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + 'extends': [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript' + ], + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + 'vue/multi-word-component-names': 'off' + } +} \ No newline at end of file diff --git a/web/.npmrc b/web/.npmrc new file mode 100644 index 000000000..1a7f3efcd --- /dev/null +++ b/web/.npmrc @@ -0,0 +1,3 @@ +optional=false +fund=false +audit=false \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..1f9ba0545 --- /dev/null +++ b/web/README.md @@ -0,0 +1,117 @@ +# Kube Bind Frontend + +A Vue.js + TypeScript frontend application for the Kube Bind project that provides a web interface for binding Kubernetes resources across clusters with SSO authentication. + +## Features + +- 🔐 **SSO Authentication**: OAuth2/OIDC-based authentication via `/api/authorize` endpoint +- 🔗 **Resource Management**: Browse and bind available Kubernetes resources +- ⚡ **Modern Stack**: Built with Vue.js 3, TypeScript, and Vite +- 📱 **Responsive Design**: Works on desktop and mobile devices + +## Architecture + +The frontend integrates with the existing Go backend through the following endpoints: + +- `/api/authorize` - SSO authentication endpoint +- `/api/callback` - OAuth2 callback handler +- `/api/resources` - Fetch available resources +- `/api/bind` - Bind resources to cluster + +## Development Setup + +### Prerequisites + +- Node.js 18+ and npm +- Go 1.19+ for running the backend server + +### Development Workflow + +#### Option 1: Integrated Development (Recommended) +```bash +# Build the frontend first +cd web && npm install && npm run build && cd .. + +# Run the Go backend server (serves both API and frontend) +go run ./cmd/backend --listen-port=8080 + +# Visit http://localhost:8080 for the complete application +``` + +#### Option 2: Separate Development Servers +```bash +# Terminal 1: Start Go backend +go run ./cmd/backend --listen-port=8080 + +# Terminal 2: Start frontend dev server with hot reload +cd web +npm install +npm run dev + +# Visit http://localhost:3000 for frontend (proxies API to :8080) +``` + +### Available Scripts + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run lint` - Lint code +- `npm run type-check` - Run TypeScript type checking + +## Authentication Flow + +1. User clicks "Login" or accesses protected resource +2. Frontend redirects to `/api/authorize` with session parameters +3. Backend handles OAuth2 flow with configured OIDC provider +4. User is redirected back to frontend with authentication cookie +5. Frontend can now access protected endpoints + +## Project Structure + +``` +src/ +├── main.ts # Application entry point +├── App.vue # Root component +├── services/ +│ └── auth.ts # Authentication service +└── views/ + ├── Home.vue # Landing page + ├── Login.vue # Login form + └── Resources.vue # Resource management +``` + +## Configuration + +The frontend automatically detects the backend API through Vite proxy configuration. For production deployments, ensure the frontend is served from the same domain as the backend or configure CORS appropriately. + +## Building for Production + +### Integrated Build +```bash +# Use the build script (builds frontend + Go binary) +./scripts/build-frontend.sh + +# Or build manually: +cd web && npm run build && cd .. +go build -o bin/kube-bind-server ./cmd/backend + +# The frontend is automatically embedded in the Go binary +``` + +### Frontend Only +```bash +cd web +npm run build +``` + +The built files will be in the `web/dist/` directory and are automatically embedded into the Go binary via the `//go:embed` directive. + +## Architecture Integration + +The frontend is now fully integrated with the Go backend: + +- **Production**: Frontend assets are embedded in the Go binary using `//go:embed` +- **Development**: Fallback to local filesystem for live development +- **Routing**: SPA routes are handled by the Go server with proper fallback +- **API**: All `/api/*` routes are handled by Go, everything else serves the Vue.js app \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 000000000..a7770d9e7 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Kube Bind + + +
    + + + \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 000000000..cca3390cb --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,3152 @@ +{ + "name": "kube-bind-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kube-bind-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.5.0", + "vue": "^3.3.4", + "vue-router": "^4.2.4" + }, + "devDependencies": { + "@types/node": "^20.8.0", + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "@vitejs/plugin-vue": "^4.4.0", + "@vue/eslint-config-typescript": "^12.0.0", + "eslint": "^8.50.0", + "eslint-plugin-vue": "^9.17.0", + "typescript": "^5.2.0", + "vite": "^4.5.0", + "vue-tsc": "^1.8.15" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.15.tgz", + "integrity": "sha512-W3bqcbLsRdFDVcmAM5l6oLlcl67vjevn8j1FPZ4nx+K5jNoWCh+FC/btxFoBPnvQlrHHDwfjp1kjIEDfwJ0Mog==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", + "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", + "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", + "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.21", + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.18", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", + "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz", + "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz", + "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", + "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/runtime-core": "3.5.21", + "@vue/shared": "3.5.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz", + "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "vue": "3.5.21" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz", + "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", + "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-sfc": "3.5.21", + "@vue/runtime-dom": "3.5.21", + "@vue/server-renderer": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 000000000..41d04f598 --- /dev/null +++ b/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "kube-bind-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --config vite.config.docker.ts", + "build:local": "vite build", + "build:check": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "type-check": "vue-tsc --noEmit" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "axios": "^1.5.0" + }, + "devDependencies": { + "@types/node": "^20.8.0", + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "@vitejs/plugin-vue": "^4.4.0", + "@vue/eslint-config-typescript": "^12.0.0", + "eslint": "^8.50.0", + "eslint-plugin-vue": "^9.17.0", + "typescript": "^5.2.0", + "vite": "^4.5.0", + "vue-tsc": "^1.8.15" + } +} \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 000000000..2e1680a74 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 000000000..fb73d7e18 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,31 @@ +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import App from './App.vue' +import Resources from './views/Resources.vue' + +const routes = [ + // Default route redirects to resources + { path: '/', redirect: '/resources' }, + + // Main resources route + { path: '/resources', component: Resources }, + + // API routes that match backend endpoints - all serve the same resources view + { path: '/api/resources', component: Resources }, + { path: '/api/clusters/:cluster/resources', component: Resources, props: true }, + + // Web-friendly cluster-aware routes + { path: '/clusters/:cluster/resources', component: Resources, props: true }, + + // Catch-all route redirects to resources + { path: '/:pathMatch(.*)*', redirect: '/resources' } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +const app = createApp(App) +app.use(router) +app.mount('#app') \ No newline at end of file diff --git a/web/src/services/auth.ts b/web/src/services/auth.ts new file mode 100644 index 000000000..0385888a4 --- /dev/null +++ b/web/src/services/auth.ts @@ -0,0 +1,274 @@ +import axios from 'axios' + +export interface SessionInfo { + sessionId: string + clusterId: string + isAuthenticated: boolean +} + +export interface BindableResource { + name: string + kind: string + scope: string + apiVersion: string + group: string + resource: string + sessionID: string +} + +export interface ClaimableResource { + names: { + plural: string + singular: string + kind: string + } + groupVersionResource: { + group?: string + version: string + resource: string + } + resourceScope: string + hasStatus?: boolean +} + +export interface PermissionClaimSelector { + matchLabels?: Record + matchExpressions?: Array<{ + key: string + operator: string + values?: string[] + }> +} + +export interface PermissionClaim { + group: string + resource: string + selector?: { + all?: boolean + labelSelector?: PermissionClaimSelector + } +} + +export interface BindableResourcesRequest { + apiVersion: string + kind: string + metadata?: { + name?: string + } + resources: BindableResource[] + permissionClaims?: PermissionClaim[] +} + +class AuthService { + private sessionInfo: SessionInfo | null = null + + async checkAuthentication(): Promise { + try { + const sessionCookie = this.getSessionCookie() + if (!sessionCookie) { + return false + } + + return true + } catch (error) { + console.error('Auth check failed:', error) + return false + } + } + + login(clusterId: string = '', redirectPort: string = '3000'): void { + const sessionId = this.generateSessionId() + const redirectUrl = `${window.location.origin}/api/callback` + + // Use cluster-aware endpoint if clusterId is provided + const authPath = clusterId ? `/api/clusters/${clusterId}/authorize` : '/api/authorize' + const authUrl = new URL(authPath, window.location.origin) + authUrl.searchParams.set('s', sessionId) + authUrl.searchParams.set('c', clusterId) + authUrl.searchParams.set('u', redirectUrl) + authUrl.searchParams.set('p', redirectPort) + + this.sessionInfo = { + sessionId, + clusterId: clusterId, + isAuthenticated: false + } + + window.location.href = authUrl.toString() + } + + logout(): void { + this.sessionInfo = null + this.clearSessionCookie() + } + + getSessionInfo(): SessionInfo | null { + return this.sessionInfo + } + + private generateSessionId(): string { + return Math.random().toString(36).substring(2) + Date.now().toString(36) + } + + private getSessionCookie(): string | null { + const cookies = document.cookie.split(';') + for (let cookie of cookies) { + const [name, value] = cookie.trim().split('=') + if (name.startsWith('kube-bind')) { + return value + } + } + return null + } + + private clearSessionCookie(): void { + const cookies = document.cookie.split(';') + for (let cookie of cookies) { + const [name] = cookie.trim().split('=') + if (name.startsWith('kube-bind')) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } + } + } + + async getResources(clusterId: string = ''): Promise { + try { + const sessionCookie = this.getSessionCookie() + if (!sessionCookie) { + throw new Error('No session found') + } + + // Use cluster-aware endpoint if clusterId is provided + const resourcesPath = clusterId ? `/api/clusters/${clusterId}/resources` : '/api/resources' + const response = await axios.get(`${resourcesPath}?s=${sessionCookie}`) + return response.data + } catch (error) { + console.error('Failed to fetch resources:', error) + throw error + } + } + + async bindResource(group: string, resource: string, version: string, clusterId: string = ''): Promise { + try { + const sessionCookie = this.getSessionCookie() + if (!sessionCookie) { + throw new Error('No session found') + } + + return this.bindResourceWithSession(group, resource, version, clusterId, sessionCookie) + } catch (error) { + console.error('Failed to bind resource:', error) + throw error + } + } + + async getResourcesWithSession(clusterId: string = '', sessionId: string): Promise { + try { + // Use cluster-aware endpoint if clusterId is provided + const resourcesPath = clusterId ? `/api/clusters/${clusterId}/resources` : '/api/resources' + const fullUrl = `${resourcesPath}?s=${sessionId}` + + console.log('🌐 Making API request to:', fullUrl) + console.log('🔑 Session ID:', sessionId) + console.log('🏷️ Cluster ID:', clusterId || 'none (single cluster)') + + const response = await axios.get(fullUrl) + + console.log('✅ API Response Status:', response.status) + console.log('📄 Response Headers:', response.headers) + console.log('📦 Response Data:', response.data) + + return response.data + } catch (error: any) { + console.error('❌ Failed to fetch resources with session:', error) + if (error.response) { + console.error('📄 Error Response Status:', error.response.status) + console.error('📄 Error Response Data:', error.response.data) + console.error('📄 Error Response Headers:', error.response.headers) + } + throw error + } + } + + async bindResourceWithSession(group: string, resource: string, apiVersion: string, clusterId: string = '', sessionId: string, scope: string = 'Namespaced', kind: string = '', name: string = '', permissionClaims: PermissionClaim[] = [], customRequestName?: string): Promise { + try { + console.log('🔗 Binding resource with POST request') + console.log('📋 Resource details:', { group, resource, apiVersion, clusterId, sessionId }) + + // Use cluster-aware endpoint if clusterId is provided + const bindPath = clusterId ? `/api/clusters/${clusterId}/bind` : '/api/bind' + const bindUrl = `${bindPath}?s=${sessionId}` + + console.log('🌐 POST request to:', bindUrl) + + // Create the BindableResourcesRequest payload + const requestPayload: BindableResourcesRequest = { + apiVersion: 'kubebind.io/v1alpha2', + kind: 'BindableResourcesRequest', + resources: [{ + name: name || `${resource}.${group || 'core'}`, + kind: kind || resource, + scope: scope, + apiVersion: apiVersion, + group: group || '', + resource: resource, + sessionID: sessionId + }], + permissionClaims: permissionClaims + } + + // Add custom request name if provided + if (customRequestName && customRequestName.trim()) { + requestPayload.metadata = { + name: customRequestName.trim() + } + } + + console.log('📦 Request payload:', requestPayload) + + const response = await axios.post(bindUrl, requestPayload, { + headers: { + 'Content-Type': 'application/json' + } + }) + + console.log('✅ Bind response status:', response.status) + console.log('📦 Bind response data:', response.data) + + return response.data + } catch (error: any) { + console.error('❌ Failed to bind resource with session:', error) + if (error.response) { + console.error('📄 Error Response Status:', error.response.status) + console.error('📄 Error Response Data:', error.response.data) + } + throw error + } + } + + async getExports(clusterId: string = ''): Promise { + try { + // Use cluster-aware endpoint if clusterId is provided + const exportsPath = clusterId ? `/api/clusters/${clusterId}/exports` : '/api/exports' + const response = await axios.get(exportsPath) + return response.data + } catch (error) { + console.error('Failed to fetch exports:', error) + throw error + } + } + + async getClaimableResources(): Promise { + try { + console.log('🔍 Fetching claimable resources from /api/bindable-resources') + const response = await axios.get('/api/bindable-resources') + console.log('📦 Claimable resources response:', response.data) + return response.data || [] + } catch (error) { + console.error('❌ Failed to fetch claimable resources:', error) + throw error + } + } +} + +export const authService = new AuthService() \ No newline at end of file diff --git a/web/src/views/Resources.vue b/web/src/views/Resources.vue new file mode 100644 index 000000000..96547114a --- /dev/null +++ b/web/src/views/Resources.vue @@ -0,0 +1,1005 @@ + + + + + \ No newline at end of file diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 000000000..b68745b90 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 000000000..4eb43d054 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/web/vite.config.docker.ts b/web/vite.config.docker.ts new file mode 100644 index 000000000..5e0ed179e --- /dev/null +++ b/web/vite.config.docker.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +// Docker-specific configuration that avoids native dependency issues +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + build: { + outDir: 'dist', + assetsDir: 'assets', + emptyOutDir: true, + // Target ES2015 for broader compatibility + target: 'es2015', + // Disable minification to avoid potential native dep issues + minify: false, + // Use Rollup options that avoid native dependencies + rollupOptions: { + output: { + // Simplified output configuration + assetFileNames: 'assets/[name].[hash][extname]', + chunkFileNames: 'assets/[name].[hash].js', + entryFileNames: 'assets/[name].[hash].js', + // Avoid code splitting to reduce complexity + manualChunks: undefined + } + } + }, + // Ensure proper base URL for production + base: './', + // TypeScript configuration for better compatibility + esbuild: { + target: 'es2015' + }, + // Optimize dependencies to avoid potential issues + optimizeDeps: { + exclude: ['@rollup/rollup-linux-arm64-gnu', '@rollup/rollup-linux-x64-gnu'] + } +}) \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 000000000..7d137fb94 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + }, + build: { + outDir: 'dist', + assetsDir: 'assets', + emptyOutDir: true, + // Target ES2015 for broader compatibility + target: 'es2015', + // Generate manifest for proper asset handling + manifest: false, + // Ensure assets are properly hashed for caching + rollupOptions: { + output: { + assetFileNames: 'assets/[name]-[hash][extname]', + chunkFileNames: 'assets/[name]-[hash].js', + entryFileNames: 'assets/[name]-[hash].js' + } + } + }, + // Ensure proper base URL for production + base: './', + // TypeScript configuration for better compatibility + esbuild: { + target: 'es2015' + } +}) \ No newline at end of file