A Crossplane provider for managing Nutanix resources. This provider enables you to provision and manage Nutanix virtual machines directly from Kubernetes using Crossplane's declarative approach.
- Virtual Machine Management: Create, update, and delete Nutanix VMs
- Multi-Datacenter Support: Configure different Prism Central endpoints for different datacenters
- Dynamic Resource Resolution: Automatically resolve cluster, subnet, and image UUIDs from names
- Flexible Authentication: Support for datacenter-specific credentials
- Line of Business (LoB) Validation: Optional validation of LoB fields for compliance
- Additional Disks: Support for attaching multiple disks to VMs
- External Facts: Attach custom metadata to VMs
Option 1: Using kubectl (Recommended - Latest Stable)
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: mgeorge67701-provider-nutanix
spec:
package: ghcr.io/mgeorge67701/provider-nutanix:v0.3.0
packagePullPolicy: Always
EOFOption 2: Using Crossplane CLI (GitHub Container Registry)
kubectl crossplane install provider ghcr.io/mgeorge67701/provider-nutanix:v0.3.0Option 3: Using Upbound Marketplace
kubectl crossplane install provider xpkg.upbound.io/mgeorge67701/provider-nutanix:v0.3.0Verify Installation
# Check provider status
kubectl get providers.pkg.crossplane.io -n crossplane-system
# Check provider pod is running
kubectl get pods -n crossplane-system | grep nutanix
# Check CRDs are installed
kubectl get crd | grep nutanixExpected output:
NAME INSTALLED HEALTHY PACKAGE
mgeorge67701-provider-nutanix True True ghcr.io/mgeorge67701/provider-nutanix:v0.3.0
providerconfigs.nutanix.crossplane.io
virtualmachines.nutanix.crossplane.io
Create a secret with your Nutanix credentials for each prism central endpoint.
kubectl create secret generic nutanix-creds-alpha \
--from-literal=credentials='{"username":"admin-alpha","password":"alpha-password"}' \
-n crossplane-system
kubectl create secret generic nutanix-creds-beta \
--from-literal=credentials='{"username":"admin-beta","password":"beta-password"}' \
-n crossplane-systemor
apiVersion: v1
kind: Secret
metadata:
name: nutanix-creds-alpha
namespace: crossplane-system
type: Opaque
stringData:
credentials: '{"username":"admin-alpha","password":"alpha-password"}'
apiVersion: v1
kind: Secret
metadata:
name: nutanix-creds-beta
namespace: crossplane-system
type: Opaque
stringData:
credentials: '{"username":"admin-beta","password":"beta-password"}'Apply the ProviderConfig:
apiVersion: nutanix.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: nutanix-creds-default
key: credentials
prismCentralEndpoints:
dc-alpha: "https://pc-alpha.example.com:9440"
dc-beta: "https://pc-beta.example.com:9440"
# Define datacenter-specific credentials
datacenterCredentials:
dc-alpha:
source: Secret
secretRef:
namespace: crossplane-system
name: nutanix-creds-alpha
key: credentials
dc-beta:
source: Secret
secretRef:
namespace: crossplane-system
name: nutanix-creds-beta
key: credentialsapiVersion: nutanix.crossplane.io/v1alpha1
kind: VirtualMachine
metadata:
name: my-vm
spec:
name: "my-crossplane-vm"
numVcpus: 2
memorySizeMib: 4096
clusterName: "my-cluster"
datacenter: "dc-alpha"
imageName: "ubuntu-22.04-cloud"
network: "network-details" # References the ConfigMap named 'network-details'
providerConfigRef:
name: defaultThe examples/ directory contains comprehensive examples.
File: examples/virtualmachine.yaml
Description: Simple VM with basic configuration.
apiVersion: nutanix.crossplane.io/v1alpha1
kind: VirtualMachine
metadata:
name: example-vm
spec:
name: "my-crossplane-vm"
numVcpus: 2
memorySizeMib: 4096
clusterName: "aza-ntnx-01" # Specify the cluster name to fetch details dynamically
datacenter: "dc-alpha" # Specify the datacenter for Prism Central endpoint selection
imageName: "ubuntu-22.04-cloud" # Can be a full or partial image name (e.g., "rhel8", "win2022"). The provider will dynamically resolve the image UUID from Nutanix Prism Central, selecting the latest matching image if a partial name is provided.
network: "network-details" # The provider will fetch network details from the ConfigMap named 'network-details'.
lob: "CLOUD" # Must be one of the allowedLobs in ProviderConfig if mandatory
providerConfigRef:
name: all-features-configFile: examples/virtualmachine-advanced.yaml
Description: VM with additional disks and external facts.
apiVersion: nutanix.crossplane.io/v1alpha1
kind: VirtualMachine
metadata:
name: example-vm-advanced
spec:
name: "my-advanced-crossplane-vm"
numVcpus: 4
memorySizeMib: 8192
clusterName: "aza-ntnx-01"
datacenter: "dc-beta" # Specify the datacenter for Prism Central endpoint selection
imageName: "rhel8" # Example: partial image name, provider selects latest RHEL 8
network: "network-details"
lob: "SECURITY" # Example: another valid LoB from ProviderConfig
additionalDisks:
- deviceIndex: 1
sizeGb: 50
imageName: "data-disk-template" # Optional: use an image for the disk
- deviceIndex: 2
sizeGb: 100
externalFacts:
environment: "production"
application: "webserver"
owner: "devops-team"
providerConfigRef:
name: all-features-configFile: examples/providerconfig-all-features.yaml
Description: Complete ProviderConfig showing all available features.
apiVersion: nutanix.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: all-features-config
spec:
# Default credentials for the provider. These are used if no datacenter-specific
# credentials are provided or if no datacenter is specified in the VM spec.
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: nutanix-creds-default
key: credentials
# Configure LoB (Line of Business) validation for VirtualMachines.
# If 'isLobMandatory' is true, the 'lob' field in VirtualMachine spec is required.
# If 'lob' is provided, its value must be one of the 'allowedLobs'.
allowedLobs:
- CLOUD
- SECURITY
- DEV
- PROD
isLobMandatory: true # Set to false if LoB field should be optional
# Define Prism Central endpoints for different datacenters.
# The provider will use the 'datacenter' field in the VirtualMachine spec
# to select the appropriate endpoint from this map.
prismCentralEndpoints:
dc-alpha: "https://pc-alpha.example.com:9440"
dc-beta: "https://pc-beta.example.com:9440"
dc-gamma: "https://pc-gamma.example.com:9440"
# Define datacenter-specific credentials.
# These credentials will override the default 'credentials' for the specified datacenter.
# If a datacenter is specified in the VM, the provider will first look for
# credentials here. If not found, it falls back to the default credentials.
datacenterCredentials:
dc-alpha:
source: Secret
secretRef:
namespace: crossplane-system
name: nutanix-creds-alpha
key: credentials
dc-beta:
source: Secret
secretRef:
namespace: crossplane-system
name: nutanix-creds-beta
key: credentialsFile: examples/network-details-configmap.yaml
Description: Example ConfigMap for network configuration data.
apiVersion: v1
kind: ConfigMap
metadata:
name: network-details
namespace: crossplane-system
data:
network-details.json: |
{
"domain": "example.com",
"nameserver": "192.168.1.1",
"gateway": "192.168.1.254",
"network": "192.168.1.0/24",
"subnet": "example-subnet",
"email": "admin@example.com",
"puppet_master": "puppet.example.com",
"network_management_server": "nms.example.com",
"foreman_host": "foreman.example.com",
"allowed_repos": [
"test1",
"test2"
]
}| Field | Description | Required |
|---|---|---|
credentials |
Default authentication credentials | Yes |
allowedLobs |
List of allowed Line of Business values | No |
isLobMandatory |
Whether LoB field is required for VMs | No |
prismCentralEndpoints |
Map of datacenter names to PC endpoints | No |
datacenterCredentials |
Datacenter-specific credentials | No |
enableAvailabilityZoneMapping |
Enable AZ to cluster mapping | No |
availabilityZoneMappingURL |
URL for AZ mapping CSV | No |
| Field | Description | Required |
|---|---|---|
name |
VM name in Nutanix | Yes |
numVcpus |
Number of vCPUs | Yes |
memorySizeMib |
Memory size in MiB | Yes |
clusterName or clusterUuid |
Target cluster | Yes (one of) |
subnetName or subnetUuid |
Network subnet | No |
imageName or imageUuid |
Base image | No |
datacenter |
Datacenter for PC selection | No |
lob |
Line of Business | No |
additionalDisks |
Extra disks to attach | No |
externalFacts |
Custom metadata | No |
providerConfigRef |
Reference to ProviderConfig | No |
The provider can dynamically resolve UUIDs from names:
- Clusters: Specify
clusterNameinstead ofclusterUuid - Subnets: Specify
subnetNameinstead ofsubnetUuid - Images: Specify
imageNameinstead ofimageUuid(supports partial names)
When using names, the provider will query the appropriate Prism Central to find matching resources.
Configure multiple Prism Central endpoints:
spec:
prismCentralEndpoints:
dc-alpha: "https://pc-alpha.example.com:9440"
dc-beta: "https://pc-beta.example.com:9440"
datacenterCredentials:
dc-beta:
source: Secret
secretRef:
name: nutanix-creds-beta
key: credentialsVMs can specify which datacenter to use:
spec:
datacenter: "dc-beta" # Uses pc-beta.example.com and specific credentialsBelow is the architecture of the Crossplane Nutanix Provider:
This diagram shows how Crossplane, your custom Nutanix provider controller, and Kubernetes resources interact to provision Nutanix VMs. The controller reads custom resources and network details, validates and resolves them, and provisions VMs via Nutanix Prism Central APIs.
- Go 1.21+
- Docker
- kubectl
- Crossplane CLI (
kubectl crossplane)
Using Make (Recommended)
# Show all available commands
make help
# Build and test
make test
make docker-build
make xpkg-build
# Complete release (build multi-platform image and package)
make release VERSION=v1.0.0
# Install provider in current cluster
make install VERSION=v1.0.0
# Check provider status
make statusManual Building
Single Architecture (local development):
# Build the Docker image
docker build -t ghcr.io/mgeorge67701/provider-nutanix:latest .
# Build the Crossplane package (xpkg)
up xpkg build --package-root=package --controller=ghcr.io/mgeorge67701/provider-nutanix:latest -o provider-nutanix-latest.xpkg
# Push both the container image and package
docker push ghcr.io/mgeorge67701/provider-nutanix:latest
up xpkg push ghcr.io/mgeorge67701/provider-nutanix:latest -f provider-nutanix-latest.xpkgMulti-Architecture Build (Production - Latest: v0.3.0)
✅ CONFIRMED WORKING: Both GHCR and Upbound registries work with this approach
Note: Upbound doesn't support multi-architecture builds. Build the image for amd64 or arm64 on Linux or Mac, or use GHCR.
echo $GITHUB_TOKEN | docker login ghcr.io -u mgeorge67701 --password-stdin
up login
# Set your version
export VERSION=v0.3.0
# Step 1: Build and push multi-architecture controller image (with -controller suffix)
make docker-buildx VERSION=${VERSION}
# This runs: docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/mgeorge67701/provider-nutanix:v0.3.0-controller --push .
# Pull the image
docker pull ghcr.io/mgeorge67701/provider-nutanix:v0.1.0-controller 2>&1
# Step 2: Build Crossplane package using the controller image
make xpkg-build VERSION=${VERSION}
# This runs: up xpkg build --package-root=package --controller=ghcr.io/mgeorge67701/provider-nutanix:v0.3.0-controller -o provider-nutanix-v0.3.0.xpkg
# Step 3: Push to GHCR (GitHub Container Registry)
make xpkg-push VERSION=${VERSION}
# This runs: up xpkg push ghcr.io/mgeorge67701/provider-nutanix:v0.3.0 -f provider-nutanix-v0.3.0.xpkg
# OR Step 3: Push to Upbound Registry (alternative)
up xpkg push xpkg.upbound.io/mgeorge67701/provider-nutanix:${VERSION} -f provider-nutanix-${VERSION}.xpkg
# Step 4: Create a GitHub release
# First, create and push a git tag
git tag ${VERSION}
git push origin ${VERSION}
# Set your GitHub token (create one at https://github.com/settings/tokens)
# with 'repo' scope for private repos or 'public_repo' for public repos
export GITHUB_TOKEN=your_token_here
# Create the release using GitHub API
curl -X POST -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/mgeorge67701/crossplane-nutanix/releases" \
-d "{
\"tag_name\": \"${VERSION}\",
\"name\": \"${VERSION}\",
\"body\": \"Release ${VERSION} of provider-nutanix\\n\\n## What's Changed\\n- Provider package built and published to ghcr.io/mgeorge67701/provider-nutanix:${VERSION}-controller\\n- Package also available on Upbound registry: xpkg.upbound.io/mgeorge67701/provider-nutanix:${VERSION}\",
\"draft\": false,
\"prerelease\": false,
\"generate_release_notes\": true
}"Manual Multi-Architecture Commands (without Make):
# Set version
export VERSION=v0.3.0
# Step 1: Setup Docker Buildx (one-time setup)
docker buildx create --name multiarch --driver docker-container --bootstrap --use
# Step 2: Build and push multi-architecture controller image
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag ghcr.io/mgeorge67701/provider-nutanix:${VERSION}-controller \
--push \
.
# Step 3: Build Crossplane package using controller image
up xpkg build \
--package-root=package \
--controller=ghcr.io/mgeorge67701/provider-nutanix:${VERSION}-controller \
-o provider-nutanix-${VERSION}.xpkg
# Step 4: Push Crossplane package to GHCR
up xpkg push ghcr.io/mgeorge67701/provider-nutanix:${VERSION} -f provider-nutanix-${VERSION}.xpkg
# Optional: Push to Upbound Registry
up xpkg push xpkg.upbound.io/mgeorge67701/provider-nutanix:${VERSION} -f provider-nutanix-${VERSION}.xpkg
# Step 5: Create GitHub Release
git tag ${VERSION}
git push origin ${VERSION}
# Set your GitHub token (if not already set)
export GITHUB_TOKEN=your_token_here
# Create the release with release notes
curl -X POST -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/mgeorge67701/crossplane-nutanix/releases" \
-d "{
\"tag_name\": \"${VERSION}\",
\"name\": \"${VERSION}\",
\"body\": \"Release ${VERSION} of provider-nutanix\\n\\n## What's Changed\\n- Provider package built and published to ghcr.io/mgeorge67701/provider-nutanix:${VERSION}-controller\\n- Package also available on Upbound registry: xpkg.upbound.io/mgeorge67701/provider-nutanix:${VERSION}\",
\"draft\": false,
\"prerelease\": false,
\"generate_release_notes\": true
}"Verify Multi-Architecture Build:
# Check controller image has both architectures
docker buildx imagetools inspect ghcr.io/mgeorge67701/provider-nutanix:v0.3.0-controller
# Test deployment from GHCR (recommended)
kubectl patch provider.pkg.crossplane.io mgeorge67701-provider-nutanix --type='merge' -p='{"spec":{"package":"ghcr.io/mgeorge67701/provider-nutanix:v0.3.0"}}'
kubectl get provider.pkg.crossplane.io mgeorge67701-provider-nutanix
# Test deployment from Upbound Registry (alternative)
kubectl patch provider.pkg.crossplane.io mgeorge67701-provider-nutanix --type='merge' -p='{"spec":{"package":"xpkg.upbound.io/mgeorge67701/provider-nutanix:v0.3.0"}}'
kubectl get provider.pkg.crossplane.io mgeorge67701-provider-nutanixTest Results (Confirmed Working - Latest Release):
- ✅ GHCR v0.3.0:
ghcr.io/mgeorge67701/provider-nutanix:v0.3.0→ INSTALLED: True, HEALTHY: True - ✅ Upbound v0.3.0:
xpkg.upbound.io/mgeorge67701/provider-nutanix:v0.3.0→ INSTALLED: True, HEALTHY: True - ✅ Multi-arch: Both linux/amd64 and linux/arm64 platforms supported
- ✅ Pod Status: Running successfully with proper entrypoint
- ✅ GitHub Release: Available at v0.3.0
# Run unit tests
go test ./...
# Run controller locally (requires Kubernetes cluster and KUBECONFIG)
go run cmd/provider/main.go --debugThis project uses automated versioning and releases via GitHub Actions:
-
Create a git tag: The CI pipeline will detect the tag and build/publish automatically
git tag v1.0.0 git push origin v1.0.0
-
Manual Release: For manual releases, follow these steps:
# Build and push Docker image docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/mgeorge67701/provider-nutanix:v1.0.0-controller --push . # Build and push Crossplane package up xpkg build --package-root=package --controller=ghcr.io/mgeorge67701/provider-nutanix:v1.0.0-controller -o provider-nutanix-v1.0.0.xpkg up xpkg push ghcr.io/mgeorge67701/provider-nutanix:v1.0.0 -f provider-nutanix-v1.0.0.xpkg # Create GitHub Release (set GITHUB_TOKEN environment variable first) curl -X POST -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ -H "Content-Type: application/json" \ "https://api.github.com/repos/mgeorge67701/crossplane-nutanix/releases" \ -d "{ \"tag_name\": \"v1.0.0\", \"name\": \"v1.0.0\", \"body\": \"Release v1.0.0 of provider-nutanix\\n\\n## What's Changed\\n- Provider package built and published to ghcr.io/mgeorge67701/provider-nutanix:v1.0.0-controller\\n- Package also available on Upbound registry: xpkg.upbound.io/mgeorge67701/provider-nutanix:v1.0.0\", \"draft\": false, \"prerelease\": false, \"generate_release_notes\": true }"
-
Available Versions: Check GitHub Releases for all available versions
To publish your provider to the Upbound Marketplace (like provider-kubernetes):
-
Set up Upbound credentials in GitHub Secrets:
UPBOUND_ACCESS_ID: Your Upbound access IDUPBOUND_TOKEN: Your Upbound token
-
Create a release - the CI/CD will automatically publish to both:
- GitHub Container Registry:
ghcr.io/mgeorge67701/provider-nutanix:VERSION - Upbound Registry:
xpkg.upbound.io/mgeorge67701/provider-nutanix:VERSION
- GitHub Container Registry:
-
Your provider will appear in the Upbound Marketplace at:
https://marketplace.upbound.io/providers/mgeorge67701/provider-nutanix
Manual publish to Upbound:
# Login to Upbound
up login
# Build and push package
up xpkg build --package-root=package --controller=ghcr.io/mgeorge67701/provider-nutanix:v1.0.0 -o provider.xpkg
up xpkg push xpkg.upbound.io/mgeorge67701/provider-nutanix:v1.0.0 -f provider.xpkgThis error occurs when the Crossplane package (xpkg) is built incorrectly. The issue is usually:
- Hardcoded controller image in package.yaml: The
spec.controller.imagefield should NOT be present inpackage/package.yaml - Incorrect xpkg build: Use
up xpkg build --controller=<image>to properly reference the controller image - Broken Docker image: Ensure your Dockerfile has proper
ENTRYPOINTandCMDinstructions - Pipeline overwrites controller image: In CI/CD, the xpkg build can overwrite the controller image if not done properly
Solution (Multi-Architecture - Recommended):
# Use the multi-architecture build process (see Manual Building section above)
export VERSION=v0.3.0
make docker-buildx VERSION=${VERSION} # Multi-arch controller image
make xpkg-build VERSION=${VERSION} # Package with proper controller reference
make xpkg-push VERSION=${VERSION} # Push packageSolution (Single Architecture):
# 1. Remove spec.controller.image from package/package.yaml
# 2. Build Docker image first
docker build -t ghcr.io/mgeorge67701/provider-nutanix:v1.0.0 .
# 3. Build xpkg with --controller flag
up xpkg build --package-root=package --controller=ghcr.io/mgeorge67701/provider-nutanix:v1.0.0 -o provider.xpkg
# 4. Push both
docker push ghcr.io/mgeorge67701/provider-nutanix:v1.0.0
up xpkg push ghcr.io/mgeorge67701/provider-nutanix:v1.0.0 -f provider.xpkgCheck the provider revision status:
kubectl get providerrevisions.pkg.crossplane.io -o wide
kubectl describe providerrevision <revision-name>Ensure your CRD files are in the correct location and referenced properly in package/package.yaml.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run
make testandmake lint - Submit a pull request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Crossplane Community: Crossplane Slack