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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,9 @@ cover.out
# Local config
config.toml
!config.example.toml
.env
deploy/litellm/.env
deploy/litellm/secret.env
deploy/docker-desktop/secret.env
cmd/kubectl-gt/kubectl-gt
cmd/kubectl-gt/bin/kubectl-gt-*
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ demo: setup-test-e2e docker-build-e2e ## Deploy to Kind and show a sample Poleca
@echo "Cleanup:"
@echo " make cleanup-test-e2e"

.PHONY: demo-docker-desktop
demo-docker-desktop: ## Deploy full stack (operator + LiteLLM/MadEye + smoke Polecat) to Docker Desktop K8s.
./deploy/docker-desktop/apply-full-stack.sh

.PHONY: smoke-test
smoke-test: ## Smoke test published release artifacts in Kind (requires VERSION).
./scripts/smoke-test.sh --version $(VERSION)
Expand Down
16 changes: 11 additions & 5 deletions api/v1alpha1/polecat_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (v *PolecatCustomValidator) validatePolecat(polecat *Polecat) (admission.Wa
if polecat.Spec.Kubernetes == nil {
allErrs = append(allErrs, "spec.kubernetes: is required when executionMode is 'kubernetes'")
} else {
errs := validateKubernetesSpec(polecat.Spec.Kubernetes)
errs := validateKubernetesSpec(polecat.Spec.Kubernetes, polecat.Spec.AgentConfig)
allErrs = append(allErrs, errs...)
}
}
Expand Down Expand Up @@ -163,7 +163,7 @@ func containsPathTraversal(s string) bool {
}

// validateKubernetesSpec validates the kubernetes execution spec.
func validateKubernetesSpec(k *KubernetesSpec) []string {
func validateKubernetesSpec(k *KubernetesSpec, agentConfig *AgentConfig) []string {
var errs []string

// GitRepository is required (validated by CRD, but double-check)
Expand All @@ -186,11 +186,15 @@ func validateKubernetesSpec(k *KubernetesSpec) []string {
errs = append(errs, "spec.kubernetes.gitSecretRef.name: is required")
}

// Either ClaudeCredsSecretRef or ApiKeySecretRef is required for authentication
// Auth: OAuth creds, direct API key, or LiteLLM/gateway key via agentConfig
hasOAuth := k.ClaudeCredsSecretRef != nil && k.ClaudeCredsSecretRef.Name != ""
hasAPIKey := k.ApiKeySecretRef != nil && k.ApiKeySecretRef.Name != ""
if !hasOAuth && !hasAPIKey {
errs = append(errs, "spec.kubernetes: either claudeCredsSecretRef or apiKeySecretRef is required")
hasGateway := agentConfig != nil &&
agentConfig.ModelProvider != nil &&
agentConfig.ModelProvider.APIKeySecretRef != nil &&
agentConfig.ModelProvider.APIKeySecretRef.Name != ""
if !hasOAuth && !hasAPIKey && !hasGateway {
errs = append(errs, "spec.kubernetes: either claudeCredsSecretRef, apiKeySecretRef, or agentConfig.modelProvider.apiKeySecretRef is required")
}

// Validate ActiveDeadlineSeconds
Expand All @@ -210,11 +214,13 @@ func validateKubernetesSpec(k *KubernetesSpec) []string {

// validateImageRegistry checks that the image is from an allowed registry.
// This prevents attackers from running arbitrary container images in the cluster.
// Local Docker Desktop builds (polecat-agent:local) are allowed in addition to published registries.
var allowedRegistries = []string{
"ghcr.io/boshu2/", // Official Gas Town images
"docker.io/boshu2/", // Docker Hub official images
"registry.access.redhat.com/", // Red Hat UBI images for FIPS
"quay.io/boshu2/", // Quay official images
"polecat-agent:", // Local Docker Desktop image (name:tag, no registry)
}

func validateImageRegistry(image string) error {
Expand Down
6 changes: 3 additions & 3 deletions api/v1alpha1/polecat_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func TestPolecatCustomValidator_ValidateCreate(t *testing.T) {
},
},
wantErr: true,
errContains: "spec.kubernetes: either claudeCredsSecretRef or apiKeySecretRef is required",
errContains: "spec.kubernetes: either claudeCredsSecretRef, apiKeySecretRef, or agentConfig.modelProvider.apiKeySecretRef is required",
},
{
name: "high resource usage warning",
Expand Down Expand Up @@ -537,7 +537,7 @@ func TestValidateKubernetesSpec(t *testing.T) {
errContains: []string{
"spec.kubernetes.gitRepository: is required",
"spec.kubernetes.gitSecretRef.name: is required",
"spec.kubernetes: either claudeCredsSecretRef or apiKeySecretRef is required",
"spec.kubernetes: either claudeCredsSecretRef, apiKeySecretRef, or agentConfig.modelProvider.apiKeySecretRef is required",
},
},
{
Expand Down Expand Up @@ -586,7 +586,7 @@ func TestValidateKubernetesSpec(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validateKubernetesSpec(tt.spec)
errs := validateKubernetesSpec(tt.spec, nil)
assert.Len(t, errs, tt.wantErrs)
for _, expected := range tt.errContains {
found := false
Expand Down
14 changes: 14 additions & 0 deletions cmd/promo-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Promo API - thin HTTP front door for Gastown promo script jobs
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache git ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/promo-api ./cmd/promo-api

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /out/promo-api /promo-api
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/promo-api"]
19 changes: 19 additions & 0 deletions cmd/promo-api/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"os"

"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

func restConfigFromKubeconfig() (*rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if kc := os.Getenv("KUBECONFIG"); kc != "" {
loadingRules.ExplicitPath = kc
}
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{},
).ClientConfig()
}
Loading
Loading