Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions .github/workflows/image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,46 @@ 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
env:
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} \
Expand All @@ -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 }} \
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ coverage.*
/dex
/bin
docs/generators/cli-doc/cli-doc
dex/
dex/

# Frontend dependencies and build
web/node_modules/
web/dist/
web/.vite/
web/*.tsbuildinfo
59 changes: 58 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
27 changes: 27 additions & 0 deletions NOTES
Original file line number Diff line number Diff line change
@@ -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-<identity>-<consumer-namespace>` 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-<identity>-<consumer-namespace>` 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-<identity>-<original-name>`.

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.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions apiserviceexport.yaml
Original file line number Diff line number Diff line change
@@ -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: {}
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
29 changes: 15 additions & 14 deletions backend/controllers/clusterbinding/clusterbinding_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 11 additions & 11 deletions backend/controllers/serviceexport/serviceexport_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{}
}

Expand All @@ -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).
Expand Down
Loading
Loading