## Motivation and Context In staging (and prod, for now) we pull the `:latest` mcp-registry image. But K8s won't actually change anything unless we recreate the pods. This change ensures the workflow goes from docker -> staging -> prod, and changes the annotation so that the pods are recreated each deploy. This ensures they actually pull the latest version. ## How Has This Been Tested? Deployed locally to minikube ## Breaking Changes None ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed
244 lines
7.7 KiB
Go
244 lines
7.7 KiB
Go
package k8s
|
|
|
|
import (
|
|
"os/exec"
|
|
"strings"
|
|
|
|
v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
|
|
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
|
|
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
|
|
networkingv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/networking/v1"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
|
|
|
|
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
|
)
|
|
|
|
// getGitCommitHash returns the current git commit hash
|
|
func getGitCommitHash() string {
|
|
cmd := exec.Command("git", "rev-parse", "HEAD")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
// Fallback to a default value if git command fails
|
|
return "unknown"
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
|
|
// DeployMCPRegistry deploys the MCP Registry to the Kubernetes cluster
|
|
func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, environment string) (*corev1.Service, error) {
|
|
conf := config.New(ctx, "mcp-registry")
|
|
githubClientId := conf.Require("githubClientId")
|
|
|
|
// Create Secret with sensitive configuration
|
|
secret, err := corev1.NewSecret(ctx, "mcp-registry-secrets", &corev1.SecretArgs{
|
|
Metadata: &metav1.ObjectMetaArgs{
|
|
Name: pulumi.String("mcp-registry-secrets"),
|
|
Namespace: pulumi.String("default"),
|
|
Labels: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
"environment": pulumi.String(environment),
|
|
},
|
|
},
|
|
StringData: pulumi.StringMap{
|
|
"GITHUB_CLIENT_SECRET": conf.RequireSecret("githubClientSecret"),
|
|
"JWT_PRIVATE_KEY": conf.RequireSecret("jwtPrivateKey"),
|
|
},
|
|
Type: pulumi.String("Opaque"),
|
|
}, pulumi.Provider(cluster.Provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create Deployment
|
|
_, err = v1.NewDeployment(ctx, "mcp-registry", &v1.DeploymentArgs{
|
|
Metadata: &metav1.ObjectMetaArgs{
|
|
Name: pulumi.String("mcp-registry"),
|
|
Namespace: pulumi.String("default"),
|
|
Labels: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
"environment": pulumi.String(environment),
|
|
},
|
|
},
|
|
Spec: &v1.DeploymentSpecArgs{
|
|
Replicas: pulumi.Int(2),
|
|
Selector: &metav1.LabelSelectorArgs{
|
|
MatchLabels: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
},
|
|
},
|
|
Template: &corev1.PodTemplateSpecArgs{
|
|
Metadata: &metav1.ObjectMetaArgs{
|
|
Labels: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
},
|
|
Annotations: pulumi.StringMap{
|
|
// Use git commit hash to trigger pod restarts when deploying new infra versions
|
|
"registry.modelcontextprotocol.io/deployCommit": pulumi.String(getGitCommitHash()),
|
|
},
|
|
},
|
|
Spec: &corev1.PodSpecArgs{
|
|
Containers: corev1.ContainerArray{
|
|
&corev1.ContainerArgs{
|
|
Name: pulumi.String("mcp-registry"),
|
|
Image: pulumi.String("ghcr.io/modelcontextprotocol/registry:latest"),
|
|
ImagePullPolicy: pulumi.String("Always"),
|
|
Ports: corev1.ContainerPortArray{
|
|
&corev1.ContainerPortArgs{
|
|
ContainerPort: pulumi.Int(8080),
|
|
Name: pulumi.String("http"),
|
|
},
|
|
},
|
|
Env: corev1.EnvVarArray{
|
|
&corev1.EnvVarArgs{
|
|
Name: pulumi.String("MCP_REGISTRY_DATABASE_URL"),
|
|
Value: pulumi.String("mongodb://mongodb.default.svc.cluster.local:27017"),
|
|
},
|
|
&corev1.EnvVarArgs{
|
|
Name: pulumi.String("MCP_REGISTRY_GITHUB_CLIENT_ID"),
|
|
Value: pulumi.String(githubClientId),
|
|
},
|
|
&corev1.EnvVarArgs{
|
|
Name: pulumi.String("MCP_REGISTRY_GITHUB_CLIENT_SECRET"),
|
|
ValueFrom: &corev1.EnvVarSourceArgs{
|
|
SecretKeyRef: &corev1.SecretKeySelectorArgs{
|
|
Name: secret.Metadata.Name(),
|
|
Key: pulumi.String("GITHUB_CLIENT_SECRET"),
|
|
},
|
|
},
|
|
},
|
|
&corev1.EnvVarArgs{
|
|
Name: pulumi.String("MCP_REGISTRY_JWT_PRIVATE_KEY"),
|
|
ValueFrom: &corev1.EnvVarSourceArgs{
|
|
SecretKeyRef: &corev1.SecretKeySelectorArgs{
|
|
Name: secret.Metadata.Name(),
|
|
Key: pulumi.String("JWT_PRIVATE_KEY"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
LivenessProbe: &corev1.ProbeArgs{
|
|
HttpGet: &corev1.HTTPGetActionArgs{
|
|
Path: pulumi.String("/v0/health"),
|
|
Port: pulumi.Int(8080),
|
|
},
|
|
InitialDelaySeconds: pulumi.Int(30),
|
|
TimeoutSeconds: pulumi.Int(5),
|
|
},
|
|
ReadinessProbe: &corev1.ProbeArgs{
|
|
HttpGet: &corev1.HTTPGetActionArgs{
|
|
Path: pulumi.String("/v0/health"),
|
|
Port: pulumi.Int(8080),
|
|
},
|
|
InitialDelaySeconds: pulumi.Int(5),
|
|
TimeoutSeconds: pulumi.Int(3),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, pulumi.Provider(cluster.Provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create Service
|
|
service, err := corev1.NewService(ctx, "mcp-registry", &corev1.ServiceArgs{
|
|
Metadata: &metav1.ObjectMetaArgs{
|
|
Name: pulumi.String("mcp-registry"),
|
|
Namespace: pulumi.String("default"),
|
|
Labels: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
"environment": pulumi.String(environment),
|
|
},
|
|
},
|
|
Spec: &corev1.ServiceSpecArgs{
|
|
Selector: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
},
|
|
Ports: corev1.ServicePortArray{
|
|
&corev1.ServicePortArgs{
|
|
Port: pulumi.Int(80),
|
|
TargetPort: pulumi.Int(8080),
|
|
Name: pulumi.String("http"),
|
|
},
|
|
},
|
|
Type: pulumi.String("ClusterIP"),
|
|
},
|
|
}, pulumi.Provider(cluster.Provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create Ingress
|
|
hosts := []string{
|
|
environment + ".registry.modelcontextprotocol.io",
|
|
}
|
|
|
|
// Add root domain for prod environment
|
|
if environment == "prod" {
|
|
hosts = append(hosts, "registry.modelcontextprotocol.io")
|
|
}
|
|
|
|
ingress, err := networkingv1.NewIngress(ctx, "mcp-registry", &networkingv1.IngressArgs{
|
|
Metadata: &metav1.ObjectMetaArgs{
|
|
Name: pulumi.String("mcp-registry"),
|
|
Namespace: pulumi.String("default"),
|
|
Labels: pulumi.StringMap{
|
|
"app": pulumi.String("mcp-registry"),
|
|
"environment": pulumi.String(environment),
|
|
},
|
|
Annotations: pulumi.StringMap{
|
|
"cert-manager.io/cluster-issuer": pulumi.String("letsencrypt-prod"),
|
|
"kubernetes.io/ingress.class": pulumi.String("nginx"),
|
|
},
|
|
},
|
|
Spec: &networkingv1.IngressSpecArgs{
|
|
Tls: networkingv1.IngressTLSArray{
|
|
&networkingv1.IngressTLSArgs{
|
|
Hosts: pulumi.ToStringArray(hosts),
|
|
SecretName: pulumi.Sprintf("mcp-registry-%s-tls", environment),
|
|
},
|
|
},
|
|
Rules: pulumi.ToStringArray(hosts).ToStringArrayOutput().ApplyT(func(hosts []string) networkingv1.IngressRuleArray {
|
|
rules := make(networkingv1.IngressRuleArray, 0, len(hosts))
|
|
for _, host := range hosts {
|
|
rules = append(rules, &networkingv1.IngressRuleArgs{
|
|
Host: pulumi.String(host),
|
|
Http: &networkingv1.HTTPIngressRuleValueArgs{
|
|
Paths: networkingv1.HTTPIngressPathArray{
|
|
&networkingv1.HTTPIngressPathArgs{
|
|
Path: pulumi.String("/"),
|
|
PathType: pulumi.String("Prefix"),
|
|
Backend: &networkingv1.IngressBackendArgs{
|
|
Service: &networkingv1.IngressServiceBackendArgs{
|
|
Name: service.Metadata.Name().Elem(),
|
|
Port: &networkingv1.ServiceBackendPortArgs{
|
|
Number: pulumi.Int(80),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
return rules
|
|
}).(networkingv1.IngressRuleArrayOutput),
|
|
},
|
|
}, pulumi.Provider(cluster.Provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx.Export("ingressHosts", ingress.Spec.Rules().ApplyT(func(rules []networkingv1.IngressRule) []string {
|
|
hosts := make([]string, 0, len(rules))
|
|
for _, rule := range rules {
|
|
hosts = append(hosts, *rule.Host)
|
|
}
|
|
return hosts
|
|
}))
|
|
|
|
return service, nil
|
|
}
|