Skip to content
Open
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
13 changes: 13 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(go mod tidy:*)",
"Bash(go version:*)",
"Bash(curl:*)",
"Bash(gofmt:*)",
"Bash(go install:*)",
"Bash(make:*)"
]
}
}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ BASE_NAME := hana
PROJECT_NAME := crossplane-provider-$(BASE_NAME)
PROJECT_REPO := github.com/SAP/$(PROJECT_NAME)

TARGETARCH ?= amd64

PLATFORMS ?= linux_amd64

VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || git rev-parse HEAD)
Expand Down
79 changes: 60 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,75 @@

## About this project

`crossplane-provider-hana` is a minimal [Crossplane](https://crossplane.io/) Provider
that is meant to be used as a hana for implementing new Providers. It comes
with the following features that are meant to be refactored:
`crossplane-provider-hana` is a [Crossplane](https://crossplane.io/) Provider for managing SAP HANA Cloud resources and instance mappings. It provides Kubernetes-native management of:

- A `ProviderConfig` type that only points to a credentials `Secret`.
- A `MyType` resource type that serves as an example managed resource.
- A managed resource controller that reconciles `MyType` objects and simply
prints their configuration in its `Observe` method.
- **HANA Database Resources**: Users, roles, schemas, audit policies, and security configurations
- **Instance Mapping**: Map HANA Cloud instances to Kubernetes namespaces via `KymaInstanceMapping`
- Single-cluster deployment: Controller and ServiceInstance on same cluster
- Cross-cluster deployment: Controller accesses remote Kyma cluster via kubeconfig

See the [examples directory](./examples/) for detailed usage guides and example manifests.

## Requirements and Setup

### Provider
### Installation

1. Install Crossplane on your Kubernetes cluster:
```bash
kubectl create namespace crossplane-system
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm install crossplane --namespace crossplane-system crossplane-stable/crossplane
```

2. Install the HANA provider:
```bash
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-hana
spec:
package: ghcr.io/sap/crossplane-provider-hana:latest
EOF
```

3. Configure the provider with credentials:
```bash
# For SQL-based resources (User, Role, Schema, etc.)
kubectl create secret generic hana-provider-creds \
--from-literal=username=SYSTEM \
--from-literal=password=YourPassword \
--from-literal=endpoint=your-instance.hanacloud.ondemand.com \
--from-literal=port=443 \
-n crossplane-system

kubectl apply -f examples/providerconfig.yaml
```

1. Use this repository as a hana to create a new one.
1. Run `make submodules` to initialize the "build" Make submodule we use for CI/CD.
1. Rename the provider by running the follwing command:
4. Create resources:
```bash
# For instance mapping, see examples/instancemapping/
kubectl apply -f examples/instancemapping/kymainstancemapping-local.yaml
```
make provider.prepare provider={PascalProviderName}

### Development Setup

1. Clone the repository and initialize submodules:
```bash
git clone https://github.com/SAP/crossplane-provider-hana.git
cd crossplane-provider-hana
make submodules
```
4. Add your new type by running the following command:

2. Build the provider:
```bash
make build
```
make provider.addtype provider={PascalProviderName} group={group} kind={type}

3. Run locally for development:
```bash
make dev
```
5. Replace the *sample* group with your new group in apis/{provider}.go
5. Replace the *mytype* type with your new type in internal/controller/{provider}.go
5. Replace the default controller and ProviderConfig implementations with your own
5. Run `make generate` to run code generation, this created the CRDs from the API definition.
5. Run `make build` to build the provider.

### Client

Expand Down
2 changes: 2 additions & 0 deletions apis/custom_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package apis

import (
adminv1alpha1 "github.com/SAP/crossplane-provider-hana/apis/admin/v1alpha1"
inventoryv1alpha1 "github.com/SAP/crossplane-provider-hana/apis/inventory/v1alpha1"
schemav1alpha1 "github.com/SAP/crossplane-provider-hana/apis/schema/v1alpha1"
)

func init() {
// Register the types with the Scheme so the components can map objects to GroupVersionKinds and back
AddToSchemes = append(AddToSchemes,
adminv1alpha1.SchemeBuilder.AddToScheme,
inventoryv1alpha1.SchemeBuilder.AddToScheme,
schemav1alpha1.SchemeBuilder.AddToScheme,
)
}
6 changes: 6 additions & 0 deletions apis/inventory/inventory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
Copyright 2026 SAP SE
*/

// Package inventory contains inventory group API versions
package inventory
9 changes: 9 additions & 0 deletions apis/inventory/v1alpha1/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Copyright 2026 SAP SE.
*/

// Package v1alpha1 contains managed resources for HANA Cloud inventory operations.
// +kubebuilder:object:generate=true
// +groupName=inventory.hana.orchestrate.cloud.sap
// +versionName=v1alpha1
package v1alpha1
31 changes: 31 additions & 0 deletions apis/inventory/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright 2026 SAP SE.
*/

// Package v1alpha1 contains the v1alpha1 group inventory resources of the hana provider.
// +kubebuilder:object:generate=true
// +groupName=inventory.hana.orchestrate.cloud.sap
// +versionName=v1alpha1
package v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

// Package type metadata.
const (
Group = "inventory.hana.orchestrate.cloud.sap"
Version = "v1alpha1"
)

var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

// AddToScheme is used by e2e tests
AddToScheme = SchemeBuilder.AddToScheme
)
182 changes: 182 additions & 0 deletions apis/inventory/v1alpha1/kymainstancemapping_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
Copyright 2026 SAP SE.
*/

package v1alpha1

import (
"reflect"

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

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
)

// SecretReference references a Secret in a specific namespace
type SecretReference struct {
// Name is the name of the Secret
Name string `json:"name"`
// Namespace is the namespace of the Secret
Namespace string `json:"namespace"`
}

// ResourceReference references a Kubernetes resource in a specific namespace
type ResourceReference struct {
// Name is the name of the resource
Name string `json:"name"`
// Namespace is the namespace of the resource
Namespace string `json:"namespace"`
}

// KymaConnectionReference describes how to connect to the remote Kyma cluster
type KymaConnectionReference struct {
// SecretRef references a Secret containing the kubeconfig on the management cluster
SecretRef SecretReference `json:"secretRef"`

// KubeconfigKey is the key in the secret containing kubeconfig data
// +kubebuilder:validation:Optional
// +kubebuilder:default="kubeconfig"
KubeconfigKey string `json:"kubeconfigKey,omitempty"`
}

// KymaInstanceMappingParameters are the configurable fields of a KymaInstanceMapping.
type KymaInstanceMappingParameters struct {
// KymaConnectionRef references the kubeconfig secret for connecting to a remote Kyma cluster.
// If not specified, the controller uses the local cluster where it's running.
// +kubebuilder:validation:Optional
KymaConnectionRef *KymaConnectionReference `json:"kymaConnectionRef,omitempty"`

// AdminBindingRef references the ServiceBinding that provides admin API credentials
// +kubebuilder:validation:Required
AdminBindingRef ResourceReference `json:"adminBindingRef"`

// ServiceInstanceRef references the ServiceInstance (to extract instanceID)
// +kubebuilder:validation:Required
ServiceInstanceRef ResourceReference `json:"serviceInstanceRef"`

// TargetNamespace is the Kubernetes namespace to map (immutable)
// If not specified, defaults to the namespace of the ServiceInstance
// +kubebuilder:validation:Optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="targetNamespace is immutable"
TargetNamespace *string `json:"targetNamespace,omitempty"`

// ClusterIDConfigMapRef references the ConfigMap containing CLUSTER_ID
// Defaults to kyma-system/sap-btp-operator-config if not specified
// +kubebuilder:validation:Optional
ClusterIDConfigMapRef *ResourceReference `json:"clusterIdConfigMapRef,omitempty"`

// IsDefault sets this mapping as the default for the namespace
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
IsDefault bool `json:"isDefault,omitempty"`
}

// KymaClusterObservation contains information extracted from the remote Kyma cluster
type KymaClusterObservation struct {
// ServiceInstanceID is the GUID extracted from the ServiceInstance status
// +kubebuilder:validation:Optional
ServiceInstanceID string `json:"serviceInstanceID,omitempty"`

// ClusterID is extracted from the BTP operator ConfigMap
// +kubebuilder:validation:Optional
ClusterID string `json:"clusterID,omitempty"`

// ServiceInstanceName is the name of the ServiceInstance on the Kyma cluster
// +kubebuilder:validation:Optional
ServiceInstanceName string `json:"serviceInstanceName,omitempty"`

// ServiceInstanceReady indicates if the ServiceInstance on Kyma is ready
// +kubebuilder:validation:Optional
ServiceInstanceReady bool `json:"serviceInstanceReady,omitempty"`
}

// MappingID uniquely identifies a mapping in the HANA Cloud API
type MappingID struct {
// ServiceInstanceID is the GUID of the HANA Cloud instance
ServiceInstanceID string `json:"serviceInstanceID"`
// PrimaryID is the cluster ID
PrimaryID string `json:"primaryID"`
// SecondaryID is the namespace (optional)
// +kubebuilder:validation:Optional
SecondaryID *string `json:"secondaryID,omitempty"`
}

// HANACloudObservation contains information about the HANA Cloud mapping
type HANACloudObservation struct {
// MappingID contains the full mapping identifier as known by HANA Cloud
// +kubebuilder:validation:Optional
MappingID *MappingID `json:"mappingId,omitempty"`

// Ready indicates if the mapping is active and ready
// +kubebuilder:validation:Optional
Ready bool `json:"ready,omitempty"`
}

// KymaInstanceMappingObservation are the observable fields of a KymaInstanceMapping.
type KymaInstanceMappingObservation struct {
// Kyma contains information extracted from the remote Kyma cluster
// +kubebuilder:validation:Optional
Kyma *KymaClusterObservation `json:"kyma,omitempty"`

// Hana contains information about the HANA Cloud mapping status
// +kubebuilder:validation:Optional
Hana *HANACloudObservation `json:"hana,omitempty"`
}

// A KymaInstanceMappingSpec defines the desired state of a KymaInstanceMapping.
type KymaInstanceMappingSpec struct {
xpv1.ResourceSpec `json:",inline"`
ForProvider KymaInstanceMappingParameters `json:"forProvider"`
}

// A KymaInstanceMappingStatus represents the observed state of a KymaInstanceMapping.
type KymaInstanceMappingStatus struct {
xpv1.ResourceStatus `json:",inline"`
AtProvider KymaInstanceMappingObservation `json:"atProvider,omitempty"`
}

// +kubebuilder:object:root=true

// A KymaInstanceMapping maps a HANA Cloud database instance from a remote Kyma cluster to a namespace.
// It runs on a management cluster and connects to a remote Kyma cluster to fetch ServiceInstance,
// ServiceBinding, and ConfigMap resources to create the mapping.
// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status"
// +kubebuilder:printcolumn:name="CLUSTER-ID",type="string",JSONPath=".status.atProvider.kyma.clusterID"
// +kubebuilder:printcolumn:name="SERVICE-INSTANCE",type="string",JSONPath=".status.atProvider.kyma.serviceInstanceID"
// +kubebuilder:printcolumn:name="NAMESPACE",type="string",JSONPath=".spec.forProvider.targetNamespace"
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,inventory}
type KymaInstanceMapping struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec KymaInstanceMappingSpec `json:"spec"`
Status KymaInstanceMappingStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// KymaInstanceMappingList contains a list of KymaInstanceMapping
type KymaInstanceMappingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []KymaInstanceMapping `json:"items"`
}

// KymaInstanceMapping type metadata.
var (
KymaInstanceMappingKind = reflect.TypeOf(KymaInstanceMapping{}).Name()
KymaInstanceMappingGroupKind = schema.GroupKind{Group: Group, Kind: KymaInstanceMappingKind}.String()
KymaInstanceMappingKindAPIVersion = KymaInstanceMappingKind + "." + SchemeGroupVersion.String()
KymaInstanceMappingGroupVersionKind = SchemeGroupVersion.WithKind(KymaInstanceMappingKind)
)

func init() {
SchemeBuilder.Register(
&KymaInstanceMapping{},
&KymaInstanceMappingList{},
)
}
Loading
Loading