Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/security-trivy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v4

- name: Trivy filesystem scan
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@v0.33.1
with:
scan-type: fs
scan-ref: .
Expand All @@ -33,7 +33,7 @@ jobs:
run: docker build --pull -f Dockerfile.operator -t mcp-runtime-operator:ci .

- name: Trivy image scan
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@v0.33.1
with:
image-ref: mcp-runtime-operator:ci
vuln-type: 'os,library'
Expand Down
12 changes: 10 additions & 2 deletions internal/cli/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package cli

import (
"io"
"math"
"os"

"github.com/pterm/pterm"
Expand Down Expand Up @@ -195,14 +196,21 @@ func (p *Printer) Cyan(msg string) string {

func isTerminalWriter(writer io.Writer) bool {
if writer == nil {
return term.IsTerminal(int(os.Stdout.Fd()))
return isTerminalFD(os.Stdout.Fd())
}
if f, ok := writer.(interface{ Fd() uintptr }); ok {
return term.IsTerminal(int(f.Fd()))
return isTerminalFD(f.Fd())
}
return false
}

func isTerminalFD(fd uintptr) bool {
if fd > uintptr(math.MaxInt) {
return false
}
return term.IsTerminal(int(fd)) // #nosec G115 -- fd is range-checked above before conversion.
}

// SpinnerStart starts a spinner with the given message. Returns a stop function.
func (p *Printer) SpinnerStart(msg string) func(success bool, finalMsg string) {
if p.Quiet {
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"bytes"
"errors"
"math"
"strings"
"testing"
)
Expand Down Expand Up @@ -150,6 +151,12 @@ func TestPrinterSpinnerWithWriter(t *testing.T) {
stop(false, "failed")
}

func TestIsTerminalFDOverflowGuard(t *testing.T) {
if isTerminalFD(uintptr(math.MaxInt) + 1) {
t.Fatal("expected overflow guard to report non-terminal")
}
}

func TestPrinterTablesWithWriterError(t *testing.T) {
p := &Printer{Writer: errWriter{}}
data := [][]string{
Expand Down
15 changes: 14 additions & 1 deletion internal/cli/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,26 @@ func saveExternalRegistryConfig(cfg *ExternalRegistryConfig) error {
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
}
data, err := yaml.Marshal(cfg)
data, err := marshalExternalRegistryConfig(cfg)
if err != nil {
return err
}
return os.WriteFile(path, data, 0o600)
}

func marshalExternalRegistryConfig(cfg *ExternalRegistryConfig) ([]byte, error) {
data := map[string]string{
"url": cfg.URL,
}
if cfg.Username != "" {
data["username"] = cfg.Username
}
if cfg.Password != "" {
data["password"] = cfg.Password
}
return yaml.Marshal(data)
}

func loadExternalRegistryConfig() (*ExternalRegistryConfig, error) {
path, err := registryConfigPath()
if err != nil {
Expand Down
144 changes: 136 additions & 8 deletions internal/cli/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
)

const defaultRegistrySecretName = "mcp-runtime-registry-creds" // #nosec G101 -- default secret name, not a credential.
const testModeOperatorImage = "docker.io/library/mcp-runtime-operator:latest"

type ClusterManagerAPI interface {
InitCluster(kubeconfig, context string) error
Expand All @@ -46,7 +47,7 @@ type SetupDeps struct {
EnsureNamespace func(namespace string) error
GetPlatformRegistryURL func(logger *zap.Logger) string
PushOperatorImageToInternal func(logger *zap.Logger, sourceImage, targetImage, helperNamespace string) error
DeployOperatorManifests func(logger *zap.Logger, operatorImage string) error
DeployOperatorManifests func(logger *zap.Logger, operatorImage string, operatorArgs []string) error
ConfigureProvisionedRegistryEnv func(ext *ExternalRegistryConfig, secretName string) error
RestartDeployment func(name, namespace string) error
CheckCRDInstalled func(name string) error
Expand Down Expand Up @@ -127,6 +128,10 @@ func NewSetupCmd(logger *zap.Logger) *cobra.Command {
var ingressManifest string
var forceIngressInstall bool
var tlsEnabled bool
var testMode bool
var operatorMetricsAddr string
var operatorProbeAddr string
var operatorLeaderElect bool
cmd := &cobra.Command{
Use: "setup",
Short: "Setup the complete MCP platform",
Expand All @@ -139,6 +144,14 @@ func NewSetupCmd(logger *zap.Logger) *cobra.Command {
The platform deploys an internal Docker registry by default, which teams
will use to push and pull container images.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Build operator args from flags
operatorArgs := buildOperatorArgs(
operatorMetricsAddr,
operatorProbeAddr,
operatorLeaderElect,
cmd.Flags().Changed("operator-leader-elect"),
)

plan := BuildSetupPlan(SetupPlanInput{
RegistryType: registryType,
RegistryStorageSize: registryStorageSize,
Expand All @@ -147,6 +160,8 @@ will use to push and pull container images.`,
IngressManifestChanged: cmd.Flags().Changed("ingress-manifest"),
ForceIngressInstall: forceIngressInstall,
TLSEnabled: tlsEnabled,
TestMode: testMode,
OperatorArgs: operatorArgs,
})

return setupPlatform(logger, plan)
Expand All @@ -159,9 +174,31 @@ will use to push and pull container images.`,
cmd.Flags().StringVar(&ingressManifest, "ingress-manifest", "config/ingress/overlays/http", "Manifest to apply when installing the ingress controller")
cmd.Flags().BoolVar(&forceIngressInstall, "force-ingress-install", false, "Force ingress install even if an ingress class already exists")
cmd.Flags().BoolVar(&tlsEnabled, "with-tls", false, "Enable TLS overlays (ingress/registry); default is HTTP for dev")
cmd.Flags().BoolVar(&testMode, "test-mode", false, "Test mode: skip operator build and use kind-loaded image")
cmd.Flags().StringVar(&operatorMetricsAddr, "operator-metrics-addr", "", "Operator metrics bind address (default: :8080 from manager.yaml)")
cmd.Flags().StringVar(&operatorProbeAddr, "operator-probe-addr", "", "Operator health probe bind address (default: :8081 from manager.yaml)")
cmd.Flags().BoolVar(&operatorLeaderElect, "operator-leader-elect", false, "Override operator leader election when set")
return cmd
}

// buildOperatorArgs constructs operator command-line arguments from flags.
// Only includes flags that were explicitly set.
func buildOperatorArgs(metricsAddr, probeAddr string, leaderElect, leaderElectChanged bool) []string {
var args []string

if metricsAddr != "" {
args = append(args, "--metrics-bind-address="+metricsAddr)
}
if probeAddr != "" {
args = append(args, "--health-probe-bind-address="+probeAddr)
}
if leaderElectChanged {
args = append(args, fmt.Sprintf("--leader-elect=%t", leaderElect))
}

return args
}

func setupPlatform(logger *zap.Logger, plan SetupPlan) error {
return setupPlatformWithDeps(logger, plan, SetupDeps{}.withDefaults(logger))
}
Expand Down Expand Up @@ -303,13 +340,21 @@ func setupRegistryStep(logger *zap.Logger, extRegistry *ExternalRegistryConfig,
return nil
}

func prepareOperatorImage(logger *zap.Logger, extRegistry *ExternalRegistryConfig, usingExternalRegistry bool, deps SetupDeps) (string, error) {
func prepareOperatorImage(logger *zap.Logger, extRegistry *ExternalRegistryConfig, usingExternalRegistry, testMode bool, deps SetupDeps) (string, error) {
// Step 5: Deploy operator
Step("Step 5: Deploy operator")

operatorImage := deps.OperatorImageFor(extRegistry)
if testMode && GetOperatorImageOverride() == "" {
operatorImage = testModeOperatorImage
}
Info(fmt.Sprintf("Image: %s", operatorImage))

if testMode {
Info("Test mode: skipping operator build and push, using kind-loaded image")
return operatorImage, nil
}

Info("Building operator image")
if err := deps.BuildOperatorImage(operatorImage); err != nil {
wrappedErr := wrapWithSentinelAndContext(
Expand Down Expand Up @@ -370,9 +415,9 @@ func prepareOperatorImage(logger *zap.Logger, extRegistry *ExternalRegistryConfi
return internalOperatorImage, nil
}

func deployOperatorStep(logger *zap.Logger, operatorImage string, extRegistry *ExternalRegistryConfig, registrySecretName string, usingExternalRegistry bool, deps SetupDeps) error {
func deployOperatorStep(logger *zap.Logger, operatorImage string, extRegistry *ExternalRegistryConfig, registrySecretName string, usingExternalRegistry bool, operatorArgs []string, deps SetupDeps) error {
Info("Deploying operator manifests")
if err := deps.DeployOperatorManifests(logger, operatorImage); err != nil {
if err := deps.DeployOperatorManifests(logger, operatorImage, operatorArgs); err != nil {
wrappedErr := wrapWithSentinelAndContext(
ErrOperatorDeploymentFailed,
err,
Expand Down Expand Up @@ -751,13 +796,13 @@ func printDeploymentDiagnosticsWithKubectl(kubectl KubectlRunner, deploy, namesp

// deployOperatorManifests deploys operator manifests without requiring kustomize or controller-gen.
// It applies CRD, RBAC, and manager manifests directly, replacing the image name in the process.
func deployOperatorManifests(logger *zap.Logger, operatorImage string) error {
return deployOperatorManifestsWithKubectl(kubectlClient, logger, operatorImage)
func deployOperatorManifests(logger *zap.Logger, operatorImage string, operatorArgs []string) error {
return deployOperatorManifestsWithKubectl(kubectlClient, logger, operatorImage, operatorArgs)
}

// deployOperatorManifestsWithKubectl deploys operator manifests without requiring kustomize or controller-gen.
// It applies CRD, RBAC, and manager manifests directly, replacing the image name in the process.
func deployOperatorManifestsWithKubectl(kubectl KubectlRunner, logger *zap.Logger, operatorImage string) error {
// It applies CRD, RBAC, and manager manifests directly, replacing the image name and injecting operator args.
func deployOperatorManifestsWithKubectl(kubectl KubectlRunner, logger *zap.Logger, operatorImage string, operatorArgs []string) error {
// Step 1: Apply CRD
Info("Applying CRD manifests")
// #nosec G204 -- fixed file path from repository.
Expand Down Expand Up @@ -814,6 +859,11 @@ func deployOperatorManifestsWithKubectl(kubectl KubectlRunner, logger *zap.Logge
re := regexp.MustCompile(`(?m)^(\s*)image:\s*\S+`)
managerYAMLStr := re.ReplaceAllString(string(managerYAML), fmt.Sprintf("${1}image: %s", operatorImage))

// Inject operator args if provided
if len(operatorArgs) > 0 {
managerYAMLStr = injectOperatorArgs(managerYAMLStr, operatorArgs)
}

// Write to temp file under the working directory so kubectl path validation passes.
tmpFile, err := os.CreateTemp(".", "manager-*.yaml")
if err != nil {
Expand Down Expand Up @@ -874,6 +924,84 @@ func deployOperatorManifestsWithKubectl(kubectl KubectlRunner, logger *zap.Logge
return nil
}

// injectOperatorArgs injects operator command-line arguments into the manager deployment YAML.
// It merges explicit overrides into the existing args section or adds one if it doesn't exist.
func injectOperatorArgs(yamlContent string, args []string) string {
if len(args) == 0 {
return yamlContent
}

argsPattern := regexp.MustCompile(`(?m)^(\s*)args:\s*$\n((?:\s+-\s+[^\n]+\n?)*)`)
if matches := argsPattern.FindStringSubmatch(yamlContent); len(matches) == 3 {
replacement := renderOperatorArgsBlock(matches[1], mergeOperatorArgs(parseOperatorArgs(matches[2]), args))
loc := argsPattern.FindStringIndex(yamlContent)
return yamlContent[:loc[0]] + replacement + yamlContent[loc[1]:]
}

commandPattern := regexp.MustCompile(`(?m)^(\s*)command:\s*$\n((?:\s+-\s+[^\n]+\n?)+)`)
if matches := commandPattern.FindStringSubmatch(yamlContent); len(matches) == 3 {
loc := commandPattern.FindStringIndex(yamlContent)
return yamlContent[:loc[0]] + yamlContent[loc[0]:loc[1]] + renderOperatorArgsBlock(matches[1], args) + yamlContent[loc[1]:]
}

imagePattern := regexp.MustCompile(`(?m)^(\s*)image:\s*\S+$`)
if matches := imagePattern.FindStringSubmatch(yamlContent); len(matches) == 2 {
loc := imagePattern.FindStringIndex(yamlContent)
return yamlContent[:loc[1]] + "\n" + renderOperatorArgsBlock(matches[1], args) + yamlContent[loc[1]:]
}

return yamlContent
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func renderOperatorArgsBlock(indent string, args []string) string {
var builder strings.Builder
builder.WriteString(indent)
builder.WriteString("args:\n")
for _, arg := range args {
builder.WriteString(indent)
builder.WriteString("- ")
builder.WriteString(arg)
builder.WriteByte('\n')
}
return builder.String()
}

func parseOperatorArgs(block string) []string {
var args []string
for _, line := range strings.Split(strings.TrimSpace(block), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "- ") {
args = append(args, strings.TrimSpace(strings.TrimPrefix(line, "- ")))
}
}
return args
}

func mergeOperatorArgs(existing, overrides []string) []string {
merged := append([]string(nil), existing...)
indexByKey := make(map[string]int, len(existing))
for i, arg := range merged {
indexByKey[operatorArgKey(arg)] = i
}
for _, arg := range overrides {
key := operatorArgKey(arg)
if idx, ok := indexByKey[key]; ok {
merged[idx] = arg
continue
}
indexByKey[key] = len(merged)
merged = append(merged, arg)
}
return merged
}

func operatorArgKey(arg string) string {
if idx := strings.Index(arg, "="); idx >= 0 {
return arg[:idx]
}
return arg
}

// setupTLS configures TLS by applying cert-manager resources.
// Prerequisites: cert-manager must be installed and CA secret must exist.
func setupTLS(logger *zap.Logger) error {
Expand Down
Loading
Loading