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
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,35 @@ build-installer: manifests generate kustomize ## Generate a consolidated YAML wi
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > dist/install.yaml

PLUGIN_NAME ?= kubectl-documentdb
PLUGIN_DIST_DIR ?= dist/$(PLUGIN_NAME)
PLUGIN_PLATFORMS ?= linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64

##@ kubectl Plugin

.PHONY: build-kubectl-plugin
build-kubectl-plugin: ## Build the kubectl-documentdb plugin for the host platform.
mkdir -p bin
cd plugins/documentdb-kubectl-plugin && go build -o $(CURDIR)/bin/$(PLUGIN_NAME) .

.PHONY: package-kubectl-plugin
package-kubectl-plugin: ## Build cross-platform archives for the kubectl-documentdb plugin.
rm -rf $(PLUGIN_DIST_DIR)
mkdir -p $(PLUGIN_DIST_DIR)
@set -e; for platform in $(PLUGIN_PLATFORMS); do \
os=$${platform%/*}; \
arch=$${platform#*/}; \
ext=""; \
if [ "$$os" = "windows" ]; then ext=".exe"; fi; \
tmpdir=$$(mktemp -d); \
echo "Building $(PLUGIN_NAME) for $$os/$$arch"; \
( cd plugins/documentdb-kubectl-plugin && GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build -o $$tmpdir/$(PLUGIN_NAME)$$ext . ); \
cp LICENSE $$tmpdir/; \
printf "kubectl-documentdb plugin bundle\n\nInstall: place $(PLUGIN_NAME)%s on your PATH (for example ~/.local/bin) and ensure it is executable.\nUsage: run 'kubectl documentdb --help'.\nDocumentation: https://github.com/microsoft/documentdb-kubernetes-operator/blob/main/docs/kubectl-plugin.md\n" "$$ext" > $$tmpdir/README.txt; \
tar -C $$tmpdir -czf $(PLUGIN_DIST_DIR)/$(PLUGIN_NAME)-$$os-$$arch.tar.gz $(PLUGIN_NAME)$$ext LICENSE README.txt; \
rm -rf $$tmpdir; \
done

##@ Deployment

ifndef ignore-not-found
Expand Down
61 changes: 61 additions & 0 deletions docs/kubectl-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# kubectl-documentdb Plugin

The `kubectl documentdb` plugin provides operational tooling for Azure Cosmos DB for MongoDB (DocumentDB) deployments managed by this operator. It targets day-two operations such as status inspection, event triage, and primary promotion workflows.

## Installation

Prebuilt archives are produced by the release workflow under `dist/kubectl-documentdb/` (GitHub Actions download). Each archive contains a platform-specific binary plus this project's MIT license. To install:

1. Download the archive that matches your operating system and CPU architecture.
2. Extract the archive and place the `kubectl-documentdb` binary somewhere on your `PATH` (for example `~/.local/bin`).
3. Ensure the binary is executable (`chmod +x ~/.local/bin/kubectl-documentdb` on Linux and macOS).

To build from source:

```bash
make build-kubectl-plugin # builds bin/kubectl-documentdb for the host platform
make package-kubectl-plugin # creates release archives for all supported platforms
```

Copy `bin/kubectl-documentdb` onto your `PATH` (renaming is not required). Verify installation with `kubectl documentdb --help`.

## Supported Commands

| Command | Purpose |
| --- | --- |
| `kubectl documentdb status` | Collects cluster-wide health information for a DocumentDB CR across all member clusters. |
| `kubectl documentdb events` | Streams Kubernetes events scoped to a DocumentDB CR, optionally following new events. |
| `kubectl documentdb promote` | Switches the primary cluster in a fleet by patching `spec.clusterReplication.primary` and waiting for convergence. |

Run `kubectl documentdb <command> --help` to review all flags. Key options include:

- `--documentdb`: (required) name of the `DocumentDB` custom resource.
- `--namespace/-n`: namespace containing the resource. Defaults to `documentdb-preview-ns` for all commands.
- `--context`: kubeconfig context to use for hub-level operations (defaults to the current context).
- `--show-connections`: include connection strings in `status` output.
- `--follow/-f`: follow mode for `events` (enabled by default).
- `--since`: limit historical events to a relative duration (for example `--since=1h`).
- `--target-cluster`: target cluster name for `promote` (required).
- `--hub-context` and `--cluster-context`: override hub and target kubeconfig contexts when promoting.

## Kubeconfig Expectations

`status` gathers information from every cluster listed in `spec.clusterReplication.clusterList`. For each entry the plugin attempts to load a kubeconfig context with the same name. Create or rename contexts accordingly so that `kubectl documentdb status` can authenticate to each member cluster.

The plugin never modifies kubeconfig files; it only reads them through `client-go`.

## Output Highlights

- **Status** prints a table containing cluster role, phase, pod readiness, service endpoints, and any retrieval errors per member cluster. Pass `--show-connections` to include the hub-reported primary connection string.
- **Events** prints the latest matching events immediately and switches to watch mode while `--follow` remains true.
- **Promote** patches the DocumentDB resource in the fleet hub, then (unless `--skip-wait` is used) polls both the hub and the target cluster until the reconciliation reports the desired primary cluster.

## Troubleshooting

- Ensure the operator has already synchronized status for the target resource; otherwise `status` may report unknown phases.
- If you see context lookup errors, verify the context name exists via `kubectl config get-contexts` and matches the cluster list entry.
- Promotion waits until `status.status` reports a healthy phase on both hub and target contexts. Use `--poll-interval` and `--wait-timeout` to tune.

## Contributing

The plugin is a standalone Go module located in `plugins/documentdb-kubectl-plugin`. Use the Makefile targets above to rebuild after code changes. Unit tests for the plugin should live alongside the command implementations under `plugins/documentdb-kubectl-plugin/cmd`.
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ theme:
nav:
- Version 1.0:
- Get Started: v1/index.md
- Tools:
- Kubectl Plugin: kubectl-plugin.md

plugins:
- search
Expand Down
5 changes: 5 additions & 0 deletions plugins/documentdb-kubectl-plugin/cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cmd

const (
defaultDocumentDBNamespace = "documentdb-preview-ns"
)
56 changes: 56 additions & 0 deletions plugins/documentdb-kubectl-plugin/cmd/document_health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cmd

import (
"strings"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func isDocumentReady(doc *unstructured.Unstructured, targetCluster string) bool {
if doc == nil {
return false
}

primary, _, err := unstructured.NestedString(doc.Object, "spec", "clusterReplication", "primary")
if err != nil || primary != targetCluster {
return false
}

healthy, _ := isDocumentHealthy(doc)
return healthy
}

func isDocumentHealthy(doc *unstructured.Unstructured) (bool, string) {
if doc == nil {
return false, ""
}

phase, found, err := unstructured.NestedString(doc.Object, "status", "status")
if err != nil {
return false, ""
}
phase = strings.TrimSpace(phase)
if !found || phase == "" {
return true, ""
}

return isHealthyPhase(phase), phase
}

func isHealthyPhase(phase string) bool {
phase = strings.ToLower(strings.TrimSpace(phase))
if phase == "" {
return true
}

switch phase {
case "healthy", "ready", "running", "succeeded":
return true
}

if strings.Contains(phase, "healthy") || strings.Contains(phase, "ready") {
return true
}

return false
}
60 changes: 60 additions & 0 deletions plugins/documentdb-kubectl-plugin/cmd/document_health_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cmd

import (
"testing"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)

func TestIsDocumentHealthy(t *testing.T) {
doc := newDocument("test", defaultDocumentDBNamespace, "cluster-a", "Ready")

healthy, phase := isDocumentHealthy(doc)
if !healthy {
t.Fatal("expected document to be healthy")
}
if phase != "Ready" {
t.Fatalf("expected phase to be 'Ready', got %q", phase)
}

healthy, _ = isDocumentHealthy(nil)
if healthy {
t.Fatal("expected nil document to be unhealthy")
}
}

func TestIsDocumentReady(t *testing.T) {
doc := newDocument("test", defaultDocumentDBNamespace, "cluster-a", "Healthy")

if !isDocumentReady(doc, "cluster-a") {
t.Fatal("expected document to be ready for cluster-a")
}

if isDocumentReady(doc, "cluster-b") {
t.Fatal("expected document to be not ready for cluster-b")
}

unstructured.SetNestedField(doc.Object, "failed", "status", "status")
if isDocumentReady(doc, "cluster-a") {
t.Fatal("expected document to be not ready when status indicates failure")
}
}

func newDocument(name, namespace, primary, phase string) *unstructured.Unstructured {
doc := &unstructured.Unstructured{Object: map[string]any{
"spec": map[string]any{
"clusterReplication": map[string]any{
"primary": primary,
},
},
"status": map[string]any{
"status": phase,
},
}}
gvk := schema.GroupVersionKind{Group: documentDBGVRGroup, Version: documentDBGVRVersion, Kind: "DocumentDB"}
doc.SetGroupVersionKind(gvk)
doc.SetName(name)
doc.SetNamespace(namespace)
return doc
}
Loading
Loading