Contributing
Contributions are welcome. This page covers project structure, how to add a new service, and development workflow.
Getting started
Requirements:
- Go 1.26+
- Make (optional but recommended)
Project structure
miniblue/
cmd/
miniblue/ # Server entrypoint
main.go # HTTP + HTTPS listeners, TLS cert generation
azlocal/ # CLI client
main.go # Command routing, HTTP helpers
internal/
azerr/ # Azure-compatible error responses
server/
server.go # Router setup, middleware, service registration
middleware.go # Azure headers, API version check
services/
acr/ # Container Registry
appconfig/ # App Configuration
auth/ # OAuth2 token endpoints
blob/ # Blob Storage
cosmosdb/ # Cosmos DB
dns/ # DNS Zones
eventgrid/ # Event Grid
functions/ # Azure Functions
identity/ # Managed Identity (IMDS)
keyvault/ # Key Vault
metadata/ # Cloud metadata endpoint
network/ # Virtual Networks + Subnets
queue/ # Queue Storage
resourcegroups/ # Resource Groups
servicebus/ # Service Bus
subscriptions/ # Subscription + tenant listing
table/ # Table Storage
store/
store.go # Thread-safe in-memory key-value store
examples/
terraform/ # Terraform example
scripts/
trust-cert.sh # Trust self-signed cert
az-login-local.sh # Legacy (use azlocal instead)
website/
docs/ # This documentation (mkdocs)
mkdocs.yml
Adding a new Azure service
Every service follows the same pattern. Here is a step-by-step guide.
1. Create the handler package
Create internal/services/yourservice/handler.go:
package yourservice
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/moabukar/miniblue/internal/store"
)
type Handler struct {
store *store.Store
}
func NewHandler(s *store.Store) *Handler {
return &Handler{store: s}
}
func (h *Handler) Register(r chi.Router) {
r.Route("/yourservice/{resourceName}", func(r chi.Router) {
r.Put("/", h.CreateOrUpdate)
r.Get("/", h.Get)
r.Delete("/", h.Delete)
})
}
func (h *Handler) CreateOrUpdate(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "resourceName")
var input map[string]interface{}
json.NewDecoder(r.Body).Decode(&input)
h.store.Set("yourservice:"+name, input)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(input)
}
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "resourceName")
v, ok := h.store.Get("yourservice:" + name)
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(v)
}
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "resourceName")
h.store.Delete("yourservice:" + name)
w.WriteHeader(http.StatusOK)
}
2. Register in the server
Edit internal/server/server.go:
import (
// ... existing imports ...
"github.com/moabukar/miniblue/internal/services/yourservice"
)
func (s *Server) setupRoutes() {
// ... existing routes ...
yourservice.NewHandler(s.store).Register(s.router)
}
3. Add to the health check
In internal/server/server.go, add the service name to the services slice in healthHandler.
4. Add azlocal commands (optional)
Add a handler function in cmd/azlocal/main.go and register it in the switch statement.
5. Write tests
6. Update documentation
Add a page under website/docs/services/ and update website/mkdocs.yml.
Key patterns
Store
The store.Store is a thread-safe in-memory key-value store. Every service uses string keys with a prefix:
h.store.Set("rg:sub1:myRG", resourceGroup) // Resource group
h.store.Set("kv:myvault:dbpass", secret) // Key Vault secret
h.store.Set("blob:blob:acct:cont:file.txt", blob) // Blob
Useful store methods:
| Method | Description |
|---|---|
Set(key, value) |
Store a value |
Get(key) |
Retrieve a value (returns value, bool) |
Delete(key) |
Delete a key (returns bool) |
Exists(key) |
Check if a key exists |
ListByPrefix(prefix) |
List all values matching a prefix |
CountByPrefix(prefix) |
Count keys matching a prefix |
DeleteByPrefix(prefix) |
Delete all keys matching a prefix |
Error responses
Use internal/azerr for Azure-compatible error responses:
import "github.com/moabukar/miniblue/internal/azerr"
azerr.NotFound(w, "Microsoft.YourService/resources", name)
azerr.BadRequest(w, "Invalid input")
azerr.Conflict(w, "Microsoft.YourService/resources", name)
ARM-style routes
For resources managed through Azure Resource Manager, use the standard ARM URL pattern:
r.Route("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.YourService/resources", func(r chi.Router) {
r.Get("/", h.List)
r.Route("/{resourceName}", func(r chi.Router) {
r.Put("/", h.CreateOrUpdate)
r.Get("/", h.Get)
r.Delete("/", h.Delete)
})
})
Code style
- Run
go vet ./...before committing - Use
gofmtformatting - One service per package under
internal/services/ - Keep handlers focused -- no cross-service dependencies
- Use regular dashes (
-) not em dashes
Development workflow
# Run the server with live reload (if using air or similar)
make run
# Run all tests
make test
# Lint
make lint
# Build everything
make build
# Clean build artifacts
make clean
Submitting changes
- Fork the repository
- Create a branch:
git checkout -b feature/my-feature - Make your changes and add tests
- Run
go build ./... && go test ./... - Commit and push
- Open a Pull Request
Reporting bugs
Open an issue at github.com/moabukar/miniblue/issues with:
- Steps to reproduce
- Expected vs actual behaviour
- Environment (OS, Go version, Docker or binary)
Suggesting new services
Open a feature request issue with the Azure service name and a link to its REST API documentation. This helps match the real API surface.
Licence
MIT. By contributing, you agree your contributions are licensed under the MIT licence.