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
6 changes: 3 additions & 3 deletions .github/workflows/build-apiserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
attestations: write
uses: datum-cloud/actions/.github/workflows/publish-docker.yaml@v1.9.0
with:
image-name: example-service # TEMPLATE: Change to your service name
image-name: search
secrets: inherit

publish-kustomize-bundles:
Expand All @@ -44,8 +44,8 @@ jobs:
packages: write
uses: datum-cloud/actions/.github/workflows/publish-kustomize-bundle.yaml@v1.9.0
with:
bundle-name: ghcr.io/example-org/example-service-kustomize # TEMPLATE: Update org/name
bundle-name: ghcr.io/datum-cloud/search-kustomize
bundle-path: config
image-overlays: config/base
image-name: ghcr.io/example-org/example-service # TEMPLATE: Update org/name
image-name: ghcr.io/datum-cloud/search
secrets: inherit
22 changes: 8 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,19 @@ COPY cmd/ cmd/
COPY pkg/ pkg/
COPY internal/ internal/

# TEMPLATE NOTE: Update the ldflags module path and binary name
# Change 'github.com/example-org/example-service' to your module path
# Change 'example-service' output name to your service name
# Change './cmd/example-service' to your cmd directory
# Build the binary with version information
# Build the binary with Search-specific version information
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-X 'github.com/example-org/example-service/internal/version.Version=${VERSION}' \
-X 'github.com/example-org/example-service/internal/version.GitCommit=${GIT_COMMIT}' \
-X 'github.com/example-org/example-service/internal/version.GitTreeState=${GIT_TREE_STATE}' \
-X 'github.com/example-org/example-service/internal/version.BuildDate=${BUILD_DATE}'" \
-a -o example-service ./cmd/example-service
-ldflags="-X 'go.datum.net/search/internal/version.Version=${VERSION}' \
-X 'go.datum.net/search/internal/version.GitCommit=${GIT_COMMIT}' \
-X 'go.datum.net/search/internal/version.GitTreeState=${GIT_TREE_STATE}' \
-X 'go.datum.net/search/internal/version.BuildDate=${BUILD_DATE}'" \
-a -o search ./cmd/search

# Runtime stage
FROM gcr.io/distroless/static:nonroot

WORKDIR /
# TEMPLATE NOTE: Update binary name to match your service
COPY --from=builder /workspace/example-service .
COPY --from=builder /workspace/search .
USER 65532:65532

# TEMPLATE NOTE: Update entrypoint to match your binary name
ENTRYPOINT ["/example-service"]
ENTRYPOINT ["/search"]
117 changes: 8 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,109 +1,8 @@
# Kubernetes API Server Template

> **This is a GitHub template repository.** Click "Use this template" to create a new Kubernetes aggregated API server for your own custom resources.

A production-ready template for building Kubernetes aggregated API servers. This provides all the scaffolding and boilerplate needed to create custom Kubernetes APIs that integrate seamlessly with `kubectl` and the Kubernetes ecosystem.

## What's Included

- **Full API server scaffolding**: Production-ready Kubernetes aggregated API server setup
- **Example custom resource**: `ExampleResource` showing best practices
- **OpenAPI/Swagger integration**: Automatic API documentation generation
- **Metrics & monitoring**: Prometheus metrics built-in
- **Version management**: Git-based version injection
- **Development tooling**: Task-based build system, Docker support
- **Code generation**: Kubernetes code-gen integration for deepcopy and clients

The template includes a working `ExampleResource` as a concrete example. You'll customize this to create your own resources like `DataProcessing`, `BackupJob`, `AnalyticsQuery`, etc.

## Using This Template

### 1. Create Repository
Click "Use this template" on GitHub to create your new repository.

### 2. Find & Replace

Use global find-and-replace (case-sensitive) across all files:

| Find | Replace With | Example |
|------|-------------|---------|
| `github.com/example-org/example-service` | Your module path | `github.com/myorg/myservice` |
| `example.example-org.io` | Your API group | `myresource.mycompany.io` |
| `ghcr.io/example-org/example-service` | Your container registry | `ghcr.io/myorg/myservice` |
| `example-service` | Your service name | `myservice` |
| `ExampleService` | Your service name | `MyService` |
| `ExampleResource` | Your resource type | `DataProcessing` |

### 3. Rename Directories

```bash
mv cmd/example-service cmd/myservice
mv pkg/apis/example-service pkg/apis/myservice
```

### 4. Customize API Types

Edit `pkg/apis/myservice/v1alpha1/types.go`:
- Rename `ExampleResource` to your resource type
- Update `ExampleResourceSpec` fields for your use case
- Update `ExampleResourceStatus` fields for your status
- Review `genclient` directives for your needs (namespaced vs cluster-scoped, allowed verbs)

### 5. Build & Generate

```bash
go mod tidy
task generate
task build
task test
```

### 6. Implement Storage Backend

The main TODO is in `internal/apiserver/apiserver.go` - look for the `TEMPLATE NOTE` comment. You need to:
- Create a REST storage implementation
- Connect to your backend (database, API, cache, etc.)
- Implement CRUD operations
- Register storage in the apiserver

See the [Kubernetes sample-apiserver](https://github.com/kubernetes/sample-apiserver/tree/master/pkg/registry) for examples.

### 7. Deploy (Optional)

Test deployment to a Kubernetes cluster:

```bash
# Review deployment manifests
cat config/README.md

# Generate and review manifests
kubectl kustomize config/base

# Deploy to cluster (requires TLS certs)
./config/deploy-dev.sh
```

See [config/README.md](config/README.md) for detailed deployment instructions.

### 8. Clean Up

Search for `TEMPLATE NOTE` comments throughout the codebase and remove them once you've addressed each customization point.

## Prerequisites

**For users:**
- Kubernetes 1.34+ cluster
- kubectl configured to access your cluster

**For developers:**
- Go 1.25.0 or later
- [Task](https://taskfile.dev) for development workflows
- Docker for building container images

## License

See [LICENSE](LICENSE) for details.

---

**Questions or feedback?** Open an issue—we're here to help!
# Search

Search is a Kubernetes-native aggregated API server that provides advanced
resource discovery through field filtering and full-text search. It indexes
cluster resources in real-time via audit log events consumed from NATS
JetStream, with declarative indexing policies controlling what gets indexed
using CEL-based filtering. The service integrates natively with kubectl/RBAC and
targets Meilisearch as the search backend.
29 changes: 11 additions & 18 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
version: '3'

# TEMPLATE NOTE: Update these variables for your service:
# - IMAGE_NAME: Change to your container registry and image name
# - Update all references to "example-service" throughout this file
vars:
TOOL_DIR: "{{.USER_WORKING_DIR}}/bin"
IMAGE_NAME: "ghcr.io/example-org/example-service" # TEMPLATE: Change to your registry/image
IMAGE_NAME: "ghcr.io/datum-cloud/search"
IMAGE_TAG: "dev"

tasks:
Expand All @@ -18,11 +15,10 @@ tasks:
# Build tasks
build:
desc: Build the search binary
# TEMPLATE NOTE: Update the ldflags to use your module path
cmds:
- |
set -e
echo "Building example-service..."
echo "Building search..."
mkdir -p {{.TOOL_DIR}}

# Get git information for version injection
Expand All @@ -36,14 +32,13 @@ tasks:

echo "Version: ${VERSION}, Commit: ${GIT_COMMIT:0:7}, Tree: ${GIT_TREE_STATE}"

# TEMPLATE: Update module path and binary name
go build \
-ldflags="-X 'github.com/example-org/example-service/internal/version.Version=${VERSION}' \
-X 'github.com/example-org/example-service/internal/version.GitCommit=${GIT_COMMIT}' \
-X 'github.com/example-org/example-service/internal/version.GitTreeState=${GIT_TREE_STATE}' \
-X 'github.com/example-org/example-service/internal/version.BuildDate=${BUILD_DATE}'" \
-o {{.TOOL_DIR}}/example-service ./cmd/example-service
echo "✅ Binary built: {{.TOOL_DIR}}/example-service"
-ldflags="-X 'go.datum.net/search/internal/version.Version=${VERSION}' \
-X 'go.datum.net/search/internal/version.GitCommit=${GIT_COMMIT}' \
-X 'go.datum.net/search/internal/version.GitTreeState=${GIT_TREE_STATE}' \
-X 'go.datum.net/search/internal/version.BuildDate=${BUILD_DATE}'" \
-o {{.TOOL_DIR}}/search ./cmd/search
echo "✅ Binary built: {{.TOOL_DIR}}/search"
silent: true

# Development tasks
Expand Down Expand Up @@ -77,19 +72,17 @@ tasks:
# Code generation tasks
generate:
desc: Generate deepcopy and client code
# TEMPLATE NOTE: Update the package path to match your module and API group
cmds:
- |
set -e
echo "Generating code..."

# Run deepcopy code generator
# TEMPLATE: Update module path and API package name
# Run code generators
go run k8s.io/code-generator/cmd/deepcopy-gen \
--go-header-file hack/boilerplate.go.txt \
--output-file zz_generated.deepcopy.go \
--bounding-dirs github.com/example-org/example-service/pkg/apis \
github.com/example-org/example-service/pkg/apis/example-service/v1alpha1
--bounding-dirs go.datum.net/search/pkg/apis \
go.datum.net/search/pkg/apis/search/v1alpha1

echo "✅ Code generation complete"
silent: true
Expand Down
58 changes: 26 additions & 32 deletions cmd/example-service/main.go → cmd/search/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (

"github.com/spf13/cobra"
"github.com/spf13/pflag"
searchapiserver "github.com/example-org/example-service/internal/apiserver"
"github.com/example-org/example-service/internal/version"
"github.com/example-org/example-service/pkg/generated/openapi"
searchapiserver "go.datum.net/search/internal/apiserver"
"go.datum.net/search/internal/version"
"go.datum.net/search/pkg/generated/openapi"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
apiopenapi "k8s.io/apiserver/pkg/endpoints/openapi"
genericapiserver "k8s.io/apiserver/pkg/server"
Expand All @@ -32,22 +32,20 @@ func init() {
}

func main() {
cmd := NewExampleServiceServerCommand()
cmd := NewSearchServerCommand()
code := cli.Run(cmd)
os.Exit(code)
}

// NewExampleServiceServerCommand creates the root command with subcommands for the search server.
//
// TEMPLATE NOTE: Rename this function and update the cobra.Command to match your service name.
func NewExampleServiceServerCommand() *cobra.Command {
// NewSearchServerCommand creates the root command with subcommands for the search server.
func NewSearchServerCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "search",
Short: "ExampleService - generic aggregated API server",
Long: `ExampleService is a generic Kubernetes aggregated API server that can be extended
Short: "Search - generic aggregated API server",
Long: `Search is a generic Kubernetes aggregated API server that can be extended
with custom search implementations.

Exposes ExampleResource resources accessible through kubectl or any Kubernetes client.`,
Exposes SearchQuery resources accessible through kubectl or any Kubernetes client.`,
}

cmd.AddCommand(NewServeCommand())
Expand All @@ -58,14 +56,14 @@ Exposes ExampleResource resources accessible through kubectl or any Kubernetes c

// NewServeCommand creates the serve subcommand that starts the API server.
func NewServeCommand() *cobra.Command {
options := NewExampleServiceServerOptions()
options := NewSearchServerOptions()

cmd := &cobra.Command{
Use: "serve",
Short: "Start the API server",
Long: `Start the API server and begin serving requests.

Exposes ExampleResource resources through kubectl.`,
Exposes SearchQuery resources through kubectl.`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := options.Complete(); err != nil {
return err
Expand Down Expand Up @@ -94,7 +92,7 @@ func NewVersionCommand() *cobra.Command {
Long: `Show the version, git commit, and build details.`,
Run: func(cmd *cobra.Command, args []string) {
info := version.Get()
fmt.Printf("ExampleService Server\n")
fmt.Printf("Search Server\n")
fmt.Printf(" Version: %s\n", info.Version)
fmt.Printf(" Git Commit: %s\n", info.GitCommit)
fmt.Printf(" Git Tree: %s\n", info.GitTreeState)
Expand All @@ -108,21 +106,17 @@ func NewVersionCommand() *cobra.Command {
return cmd
}

// ExampleServiceServerOptions contains configuration for the search server.
//
// TEMPLATE NOTE: Rename this type to match your service.
// Add custom configuration fields here as needed.
type ExampleServiceServerOptions struct {
// SearchServerOptions contains configuration for the search server.
type SearchServerOptions struct {
RecommendedOptions *options.RecommendedOptions
Logs *logsapi.LoggingConfiguration
// Add your custom options here
}

// NewExampleServiceServerOptions creates options with default values.
func NewExampleServiceServerOptions() *ExampleServiceServerOptions {
o := &ExampleServiceServerOptions{
// NewSearchServerOptions creates options with default values.
func NewSearchServerOptions() *SearchServerOptions {
o := &SearchServerOptions{
RecommendedOptions: options.NewRecommendedOptions(
"/registry/example.example-org.io", // TEMPLATE NOTE: Change this to your API group
"/registry/search.miloapis.com",
searchapiserver.Codecs.LegacyCodec(searchapiserver.Scheme.PrioritizedVersionsAllGroups()...),
),
Logs: logsapi.NewLoggingConfiguration(),
Expand All @@ -137,22 +131,22 @@ func NewExampleServiceServerOptions() *ExampleServiceServerOptions {
return o
}

func (o *ExampleServiceServerOptions) AddFlags(fs *pflag.FlagSet) {
func (o *SearchServerOptions) AddFlags(fs *pflag.FlagSet) {
o.RecommendedOptions.AddFlags(fs)
}

func (o *ExampleServiceServerOptions) Complete() error {
func (o *SearchServerOptions) Complete() error {
return nil
}

// Validate ensures required configuration is provided.
func (o *ExampleServiceServerOptions) Validate() error {
func (o *SearchServerOptions) Validate() error {
// Add validation as needed
return nil
}

// Config builds the complete server configuration from options.
func (o *ExampleServiceServerOptions) Config() (*searchapiserver.Config, error) {
func (o *SearchServerOptions) Config() (*searchapiserver.Config, error) {
if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts(
"localhost", nil, nil); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
Expand All @@ -165,12 +159,12 @@ func (o *ExampleServiceServerOptions) Config() (*searchapiserver.Config, error)

namer := apiopenapi.NewDefinitionNamer(searchapiserver.Scheme)
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(openapi.GetOpenAPIDefinitions, namer)
genericConfig.OpenAPIV3Config.Info.Title = "ExampleService"
genericConfig.OpenAPIV3Config.Info.Title = "Search"
genericConfig.OpenAPIV3Config.Info.Version = version.Version

// Configure OpenAPI v2
genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.GetOpenAPIDefinitions, namer)
genericConfig.OpenAPIConfig.Info.Title = "ExampleService"
genericConfig.OpenAPIConfig.Info.Title = "Search"
genericConfig.OpenAPIConfig.Info.Version = version.Version

if err := o.RecommendedOptions.ApplyTo(genericConfig); err != nil {
Expand All @@ -186,7 +180,7 @@ func (o *ExampleServiceServerOptions) Config() (*searchapiserver.Config, error)
}

// Run initializes and starts the server.
func Run(options *ExampleServiceServerOptions, ctx context.Context) error {
func Run(options *SearchServerOptions, ctx context.Context) error {
if err := logsapi.ValidateAndApply(options.Logs, utilfeature.DefaultMutableFeatureGate); err != nil {
return fmt.Errorf("failed to apply logging configuration: %w", err)
}
Expand All @@ -203,7 +197,7 @@ func Run(options *ExampleServiceServerOptions, ctx context.Context) error {

defer logs.FlushLogs()

klog.Info("Starting ExampleService server...")
klog.Info("Starting Search server...")
klog.Info("Metrics available at https://<server-address>/metrics")
return server.Run(ctx)
}
Loading