Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@
!api/
!internal/
!hack/

# Re-include bundle for OLM bundle image builds
!bundle/
33 changes: 33 additions & 0 deletions .github/workflows/test-e2e-bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: OLM Bundle E2E Tests

on:
pull_request:
branches: ['main']

permissions:
contents: read

jobs:
test-e2e-bundle:
name: Run on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod

- name: Install the latest version of kind
run: |
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

- name: Deploy operator bundle to Kind cluster
run: make deploy-test-e2e-bundle

- name: Run OLM bundle e2e tests
run: make test-e2e-bundle
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ go.work
__pycache__/
*.pyc

# OLM bundle (generated by `make bundle`)
bundle/

# editor and IDE paraphernalia
.idea
.vscode
Expand Down
91 changes: 74 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
# Generated from kubebuilder template:
# https://github.com/kubernetes-sigs/kubebuilder/blob/v4.11.1/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go

# VERSION defines the project version for the bundle.
VERSION ?= 0.0.1

# CHANNELS define the bundle channels used in the bundle.
CHANNELS ?= alpha
BUNDLE_CHANNELS := --channels=$(CHANNELS)

# DEFAULT_CHANNEL defines the default channel used in the bundle.
DEFAULT_CHANNEL ?= alpha
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)

BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)

# REGISTRY defines the container registry for image targets.
REGISTRY_PORT ?= 5001
REGISTRY ?= localhost:$(REGISTRY_PORT)

# IMAGE_TAG_BASE defines the namespace and part of the image name for remote images.
IMAGE_TAG_BASE ?= $(REGISTRY)/mcp-lifecycle-operator

# Image URL to use all building/pushing image targets
IMG ?= controller:latest
IMG ?= $(IMAGE_TAG_BASE):latest

# BUNDLE_IMG defines the image:tag used for the bundle.
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)

# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)


# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
Expand Down Expand Up @@ -94,31 +121,30 @@ cover-clean: ## Remove cover.out and out/coverage.{txt,html} from test-cover.
KIND_CLUSTER ?= mcp-lifecycle-operator-test-e2e

.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
@command -v $(KIND) >/dev/null 2>&1 || { \
echo "Kind is not installed. Please install Kind manually."; \
exit 1; \
}
@case "$$($(KIND) get clusters)" in \
*"$(KIND_CLUSTER)"*) \
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation."; \
$(KIND) export kubeconfig --name $(KIND_CLUSTER) ;; \
*) \
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
esac
setup-test-e2e: ## Set up a Kind cluster with local registry for e2e tests
./hack/create-kind-cluster.sh --name $(KIND_CLUSTER) --container-tool $(CONTAINER_TOOL) --registry-port $(REGISTRY_PORT)

.PHONY: deploy-test-e2e
deploy-test-e2e: setup-test-e2e manifests generate ## Build and deploy the operator to the Kind cluster for e2e tests.
$(MAKE) docker-build IMG=example.com/mcp-lifecycle-operator:e2e
$(KIND) load docker-image example.com/mcp-lifecycle-operator:e2e --name $(KIND_CLUSTER)
$(MAKE) install deploy IMG=example.com/mcp-lifecycle-operator:e2e
$(MAKE) docker-build docker-push
$(MAKE) install deploy
$(KUBECTL) rollout status deployment/mcp-lifecycle-operator-controller-manager -n mcp-lifecycle-operator-system --timeout=120s

.PHONY: test-e2e
test-e2e: ## Run the e2e tests (requires operator already deployed, see deploy-test-e2e).
go test -tags=e2e ./test/e2e/ -v -count=1 -timeout 1h

.PHONY: deploy-test-e2e-bundle
deploy-test-e2e-bundle: setup-test-e2e manifests generate operator-sdk ## Build images, install OLM, and push bundle to local registry for OLM e2e tests.
$(MAKE) docker-build docker-push
$(MAKE) bundle bundle-build bundle-push
@"$(OPERATOR_SDK)" olm status > /dev/null 2>&1 || "$(OPERATOR_SDK)" olm install

.PHONY: test-e2e-bundle
test-e2e-bundle: ## Run OLM bundle e2e tests (requires deploy-test-e2e-bundle first).
OPERATOR_SDK="$(OPERATOR_SDK)" BUNDLE_IMG=$(BUNDLE_IMG) \
go test -tags=e2e ./test/olm/ -v -count=1 -timeout 30m

.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
@$(KIND) delete cluster --name $(KIND_CLUSTER)
Expand Down Expand Up @@ -242,6 +268,24 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -

##@ OLM

.PHONY: bundle
bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files.
"$(OPERATOR_SDK)" generate kustomize manifests -q
cd config/manager && "$(KUSTOMIZE)" edit set image controller=$(IMG)
"$(KUSTOMIZE)" build config/manifests | "$(OPERATOR_SDK)" generate bundle $(BUNDLE_GEN_FLAGS)
"$(OPERATOR_SDK)" bundle validate ./bundle

.PHONY: bundle-build
bundle-build: ## Build the bundle image.
$(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .

.PHONY: bundle-push
bundle-push: ## Push the bundle image.
$(CONTAINER_TOOL) push $(BUNDLE_IMG)


##@ Dependencies

## Location to install dependencies to
Expand All @@ -256,10 +300,12 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk

## Tool Versions
KUSTOMIZE_VERSION ?= v5.7.1
CONTROLLER_TOOLS_VERSION ?= v0.20.0
OPERATOR_SDK_VERSION ?= v1.41.1

#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
Expand Down Expand Up @@ -300,6 +346,17 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))

.PHONY: operator-sdk
operator-sdk: $(OPERATOR_SDK) ## Download operator-sdk locally if necessary.
$(OPERATOR_SDK): $(LOCALBIN)
@{ \
set -e ;\
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\
chmod +x $(OPERATOR_SDK) ;\
}


# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
Expand Down
21 changes: 21 additions & 0 deletions bundle.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM scratch

# Core bundle labels.
LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1
LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/
LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/
LABEL operators.operatorframework.io.bundle.package.v1=mcp-lifecycle-operator
LABEL operators.operatorframework.io.bundle.channels.v1=alpha
LABEL operators.operatorframework.io.bundle.channel.default.v1=alpha
LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.41.1
LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1
LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4

# Labels for testing.
LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1
LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/

# Copy files to locations specified by labels.
COPY bundle/manifests /manifests/
COPY bundle/metadata /metadata/
COPY bundle/tests/scorecard /tests/scorecard/
30 changes: 30 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/tls"
"flag"
"os"
"strings"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
Expand All @@ -32,6 +33,7 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
Expand Down Expand Up @@ -155,13 +157,23 @@ func main() {
metricsServerOptions.KeyName = metricsCertKey
}

var cacheOpts cache.Options
if watchNamespaces := getWatchNamespaces(); len(watchNamespaces) > 0 {
setupLog.Info("watching specific namespaces", "namespaces", watchNamespaces)
cacheOpts.DefaultNamespaces = make(map[string]cache.Config, len(watchNamespaces))
for _, ns := range watchNamespaces {
cacheOpts.DefaultNamespaces[ns] = cache.Config{}
}
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "bed7462b.x-k8s.io",
Cache: cacheOpts,
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
Expand Down Expand Up @@ -205,3 +217,21 @@ func main() {
os.Exit(1)
}
}

// getWatchNamespaces returns the namespaces the operator should watch.
// When WATCH_NAMESPACE is set (comma-separated), the operator restricts its
// cache to those namespaces. OLM populates this via the olm.targetNamespaces
// annotation. An empty value means watch all namespaces.
func getWatchNamespaces() []string {
ns, found := os.LookupEnv("WATCH_NAMESPACE")
if !found || ns == "" {
return nil
}
var namespaces []string
for n := range strings.SplitSeq(ns, ",") {
if trimmed := strings.TrimSpace(n); trimmed != "" {
namespaces = append(namespaces, trimmed)
}
}
return namespaces
}
5 changes: 5 additions & 0 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ spec:
# - --zap-devel
image: controller:latest
name: manager
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.annotations['olm.targetNamespaces']
ports: []
securityContext:
allowPrivilegeEscalation: false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
annotations:
alm-examples: '[]'
capabilities: Basic Install
name: mcp-lifecycle-operator.v0.0.0
namespace: placeholder
spec:
apiservicedefinitions: {}
customresourcedefinitions:
owned:
- description: "MCPServer runs a Model Context Protocol (MCP) server in Kubernetes.\n\nMCPServer
creates and manages a Deployment and Service to run an MCP server from a\ncontainer
image. The MCP server exposes tools, resources, and prompts that AI applications\ncan
use via the Model Context Protocol.\n\nExample:\n\n\tapiVersion: mcp.x-k8s.io/v1alpha1\n\tkind:
MCPServer\n\tmetadata:\n\t name: example\n\tspec:\n\t source:\n\t type:
ContainerImage\n\t containerImage:\n\t ref: example-mcp-image\n\t
\ config:\n\t port: 8080\n\nThe controller manages Deployment and Service
resources with the same name as the MCPServer,\nusing ownerReferences to establish
ownership. The controller will reject updates to resources\nowned by other
controllers or resources with no controller owner (to prevent silent overwrites\nof
manually-created resources), but will adopt orphaned resources from a deleted
MCPServer\nwith the same name to enable seamless recreation."
displayName: MCPServer
kind: MCPServer
name: mcpservers.mcp.x-k8s.io
version: v1alpha1
description: Kubernetes operator for managing MCP (Model Context Protocol) server
lifecycles.
displayName: MCP Lifecycle Operator
icon:
- base64data: ""
mediatype: ""
install:
spec:
deployments: null
strategy: ""
installModes:
- supported: true
type: OwnNamespace
- supported: true
type: SingleNamespace
- supported: true
type: MultiNamespace
- supported: true
type: AllNamespaces
keywords:
- mcp
- model-context-protocol
- ai
links:
- name: GitHub
url: https://github.com/kubernetes-sigs/mcp-lifecycle-operator
maintainers:
- email: cncf-mcp-lifecycle-operator-maintainers@lists.cncf.io
name: MCP Lifecycle Operator Maintainers
Comment on lines +56 to +57
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is more a placeholder for now, but should be adjusted, before we build the bundle

maturity: alpha
minKubeVersion: 1.32.0
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure here, I only took an older version

provider:
name: kubernetes-sigs
version: 0.0.0
5 changes: 5 additions & 0 deletions config/manifests/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resources:
- bases/mcp-lifecycle-operator.clusterserviceversion.yaml
- ../default
- ../samples
- ../scorecard
7 changes: 7 additions & 0 deletions config/scorecard/bases/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: scorecard.operatorframework.io/v1alpha3
kind: Configuration
metadata:
name: config
stages:
- parallel: true
tests: []
17 changes: 17 additions & 0 deletions config/scorecard/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
resources:
- bases/config.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
- path: patches/basic.config.yaml
target:
group: scorecard.operatorframework.io
kind: Configuration
name: config
version: v1alpha3
- path: patches/olm.config.yaml
target:
group: scorecard.operatorframework.io
kind: Configuration
name: config
version: v1alpha3
10 changes: 10 additions & 0 deletions config/scorecard/patches/basic.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- basic-check-spec
image: quay.io/operator-framework/scorecard-test:v1.41.1
labels:
suite: basic
test: basic-check-spec-test
Loading