Skip to content

Experimental(kyma-service-instance) - Adding a kyma service instance crd#366

Draft
SatabdiG wants to merge 23 commits intomainfrom
feat/kyma-service-instance-spike
Draft

Experimental(kyma-service-instance) - Adding a kyma service instance crd#366
SatabdiG wants to merge 23 commits intomainfrom
feat/kyma-service-instance-spike

Conversation

@SatabdiG
Copy link
Contributor

@SatabdiG SatabdiG commented Oct 13, 2025

KymaServiceInstance PR

Status

🧟 Living/Dead waiting for revival, aka interest.

Demoed and ready, but the user requirements seemed low.

Issue: #271

Table of Contents


Overview

Implement KymaServiceInstance CRD to simplify creating BTP service instances in Kyma runtime.

Goal: User-friendly abstraction that hides manual wrapping complexity.

Scope:

  • ✅ KymaServiceInstance CRD and controller
  • ✅ Direct Kubernetes API (no Object wrapper)
  • 🔄 KymaServiceBinding (stretch goal)
  • ❌ Not for main branch (experimental/RC only)

Problem Statement

Current State (Painful)

Users must manually wrap BTP Service Operator resources in kubernetes.crossplane.io/Object:

apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
metadata:
  name: k8s-destination-service-instance-workload
spec:
  forProvider:
    manifest:
      apiVersion: services.cloud.sap.com/v1
      kind: ServiceInstance
      metadata:
        name: destination-service
        namespace: consumption
      spec:
        serviceOfferingName: destination
        servicePlanName: lite
        externalName: destination-service
  providerConfigRef:
    name: kyma-k8s-providerconfig

Problems:

  • 🔴 Verbose
  • 🔴 Error-prone (nested YAML)
  • 🔴 Inconsistent with ServiceInstance/CloudFoundryEnvironment patterns
  • 🔴 Difficult to migrate between runtimes

Desired State (Simple)

apiVersion: account.btp.sap.crossplane.io/v1alpha1
kind: KymaServiceInstance
metadata:
  name: destination-service
spec:
  kymaEnvironmentBindingRef:
    name: my-kyma-binding
  forProvider:
    namespace: consumption
    serviceOfferingName: destination
    servicePlanName: lite
    externalName: destination-service

Benefits:

  • ✅ Concise (10 lines)
  • ✅ Consistent with provider patterns
  • ✅ No external dependencies
  • ✅ Easy runtime migration

Architecture Investigation

The Complete Resource Hierarchy

Investigation of KymaModule/KymaEnvironment/KymaEnvironmentBinding revealed the dependency chain:

GlobalAccount
  ↓
Subaccount
  ↓
  ├─ ServiceManager (creates secret: service-manager-credentials)
  │    ↓
  ├─ CloudManagement (references ServiceManager, creates CIS credentials)
  │    ↓
  └─ KymaEnvironment (creates initial kubeconfig secret)
       ↓
     KymaEnvironmentBinding (creates rotating kubeconfig with expiry)
       ├─ Secret key: "kubeconfig"
       ├─ Secret key: "expires_at"
       ├─ Rotation: 1h (default)
       └─ TTL: 1h15m (default)
       ↓
     KymaModule / KymaServiceInstance (uses rotating credentials)
       ↓
     Resources in Kyma Cluster

Design Decisions

1. Proposed CRD Structure

File: apis/environment/v1alpha1/kymaserviceinstance_types.go

type KymaServiceInstanceParameters struct {
    // Name of the ServiceInstance resource in Kyma cluster
   // +kubebuilder:validation:Required
   Name string `json:"name"`
    // Namespace in Kyma cluster where ServiceInstance will be created
    // +kubebuilder:validation:Required
    Namespace string `json:"namespace"`
    
    // BTP service offering name (e.g., "destination", "xsuaa")
    // +kubebuilder:validation:Required
    ServiceOfferingName string `json:"serviceOfferingName"`
    
    // Service plan name (e.g., "lite", "standard")
    // +kubebuilder:validation:Required
    ServicePlanName string `json:"servicePlanName"`
    
    // External name for the service instance in BTP
    // +kubebuilder:validation:Optional
    ExternalName string `json:"externalName,omitempty"`
    
    // Service configuration parameters
    // +kubebuilder:pruning:PreserveUnknownFields
    // +kubebuilder:validation:Optional
    Parameters runtime.RawExtension `json:"parameters,omitempty"`
    
    // TODO: Add ParameterSecretRefs like ServiceInstance?
}

type KymaServiceInstanceSpec struct {
    xpv1.ResourceSpec `json:",inline"`
    ForProvider       KymaServiceInstanceParameters `json:"forProvider"`
    
    // Reference to KymaEnvironmentBinding (for rotating kubeconfig)
    // +crossplane:generate:reference:type=KymaEnvironmentBinding
    KymaEnvironmentBindingRef *xpv1.Reference `json:"kymaEnvironmentBindingRef,omitempty"`
    
    // TODO: Generated fields for secret name/namespace resolution
}

type KymaServiceInstanceObservation struct {
    // Ready status from Kyma ServiceInstance
    Ready bool `json:"ready,omitempty"`
    
    // Instance ID from BTP
    InstanceID string `json:"instanceID,omitempty"`
    
    // TODO: Add more status fields based on services.cloud.sap.com/v1/ServiceInstance status
}

Field Mapping (BTP Crossplane → Kyma Operator):

  • namespacemetadata.namespace in Kyma
  • serviceOfferingNamespec.serviceOfferingName
  • servicePlanNamespec.servicePlanName
  • externalNamespec.externalName
  • parametersspec.parameters

2. Client Implementation

File: internal/clients/kymaserviceinstance/client.go

Pattern (following kymamodule.NewKymaModuleClient):

Target CRD (in Kyma cluster):

var GVKServiceInstance = schema.GroupVersionKind{
    Group:   "services.cloud.sap.com",
    Version: "v1",
    Kind:    "ServiceInstance",
}

Libraries:

  • sigs.k8s.io/controller-runtime/pkg/client - Kubernetes client
  • k8s.io/client-go/tools/clientcmd - Kubeconfig parsing
  • k8s.io/apimachinery/pkg/apis/meta/v1/unstructured - Dynamic CRD handling

Code Reference: internal/clients/kymamodule/module.go:NewKymaModuleClient()

3. Controller Pattern

File: internal/controller/kymaserviceinstance/kymaserviceinstance.go

Code Reference: internal/controller/kymamodule/kymamodule.go

4. Re-use SecretFetcher Pattern

File: internal/controller/kymaserviceinstance/secretfetcher.go

Code Reference: internal/controller/kymamodule/secretfetcher.go


Implementation Plan

Phase 1: CRD & API Types

  • Create apis/account/v1alpha1/kymaserviceinstance_types.go
  • Define KymaServiceInstanceParameters
  • Define KymaServiceInstanceObservation
  • Define KymaServiceInstanceSpec with KymaEnvironmentBindingRef

Phase 2: Client Implementation

  • Create internal/clients/kymaserviceinstance/client.go
  • Define Client interface
  • Implement NewKymaServiceInstanceClient(kubeconfig []byte)
  • Define BTP Service Operator ServiceInstance struct (or use unstructured)
  • Implement GetServiceInstance(ctx, namespace, name)
  • Implement CreateServiceInstance(ctx, cr)
  • Implement DeleteServiceInstance(ctx, namespace, name)

Phase 3: Controller Scaffolding

  • Create internal/controller/kymaserviceinstance/controller.go
  • Create internal/controller/kymaserviceinstance/secretfetcher.go
  • Define connector struct
  • Define external struct
  • Implement Connect() with secret fetching

Phase 4: Controller Lifecycle

  • Implement Observe() - check resource exists
  • Implement Create() - create ServiceInstance in Kyma
  • Implement Delete() - remove ServiceInstance from Kyma
  • Implement Update() - no-op for now (or future)
  • Handle status updates properly

Phase 5: Testing & Validation

Phase 6: Documentation

  • Create example YAMLs

Testing Strategy

Prerequisites

Required Resources:

# 1. GlobalAccount (existing)
# 2. Subaccount
# 3. ServiceManager
# 4. CloudManagement
# 5. KymaEnvironment
# 6. KymaEnvironmentBinding

Error Scenarios to Test

  • ✅ Happy path
  • ❌ Invalid KymaEnvironmentBindingRef
  • ❌ Invalid service offering/plan

Open Questions

1. Parameter Handling

Question: How to handle complex/secret parameters?

Options:

  • A) Only inline JSON (simple)
  • B) Add ParameterSecretRefs like ServiceInstance (complex but consistent)

Proposal: Start with A, add B if needed

2. Status Fields

Question: What fields from Kyma ServiceInstance status should we expose?

From services.cloud.sap.com/v1/ServiceInstance:

  • status.ready (bool)
  • status.instanceID (string)
  • status.conditions ([]Condition)
  • status.operationURL (string)
  • status.operationType (string)

Proposal: Start minimal (ready, instanceID), expand as needed

3. Update Support

Question: Should Update() actually update or be no-op?

Considerations:

  • Can BTP services be updated in place?
  • What fields are mutable?
  • Terraform pattern is typically replace-on-change

Proposal: Start with no-op (like KymaModule), add if needed

4. Multiple Bindings per Instance

Question: Can one ServiceInstance have multiple ServiceBindings?

Impact: Affects how we structure KymaServiceBinding CRD

Decision: Research Kyma operator behavior


Known Limitations

make generate currently fails due to schema.json generation not supporting
mixed Upjet/custom controller architectures. This is a pre-existing issue
affecting all custom controllers (KymaModule, KymaEnvironmentBinding, etc.).

Workaround: Use make generate.run for code generation, which skips
schema.json generation and works correctly for custom controllers.


References

Code References

  • KymaModule Controller: internal/controller/kymamodule/
  • KymaModule Client: internal/clients/kymamodule/
  • KymaEnvironment Types: apis/environment/v1alpha1/kymaenvironment_types.go
  • KymaEnvironmentBinding Types: apis/environment/v1alpha1/kymaenvironmentbinding_types.go

External Documentation

TODO

  • Add screenshots from BTP cockpit
  • Test all code examples
  • Clean up informal language
Screenshot 2025-10-16 at 20 22 19 Screenshot 2025-10-16 at 20 24 47

@SatabdiG SatabdiG self-assigned this Oct 13, 2025
@SatabdiG SatabdiG added do-not-merge On PRs, this prevents merging through the status checks release-notes/ignore This PR will be ignored for the release note generation labels Oct 13, 2025
Copy link
Contributor

@Lasserich Lasserich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work and super impressive how fast you got this working!! 🥇
I have left some comments which should be addressed. Let us wait however for Tuesday to hear about the ideas from the Kyma folks and our Stakeholders to see the importance. 😄

// External name for display in BTP cockpit (optional)
// If not specified, uses the Name field
// +kubebuilder:validation:Optional
ExternalName string `json:"externalName,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could lead to confusion from a naming perspective, I already envision the support tickets with confusion about two external-names. Is there another way we could name this? + Optional fields should be pointer.

Conditions []ServiceInstanceCondition `json:"conditions,omitempty"`
}

type ServiceInstanceCondition struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment with a reference to the original structure in the Kyma environments? This helps us determine the effects of changes from their side better.


}

func extractConditions(si *ServiceInstance) []v1alpha1.ServiceInstanceCondition {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this doing, copying over the Conditions from the BTP Operator? A small comment would be nice here. :D

)

// This matches services.cloud.sap.com/v1/ServiceInstance in Kyma
type ServiceInstance struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are copied over right? Also here some reference link would be very nice!

return managed.ExternalObservation{}, errors.New(errNotKymaServiceInstance)
}

if cr.Spec.KymaEnvironmentBindingRef == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should move into the SecretFetcher logic. Do not like to have this bloating the Observe method too much.

}

// Describe the instance in Kyma
observation, err := e.client.DescribeInstance(ctx, cr.Spec.ForProvider.Namespace, cr.Spec.ForProvider.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an option to use the resource metadata.annotation.external-name as a look-up? Even if the BTP Operator does not support such we should use its value as the look-up to stay consistent with our importing scheme.

// Update status
cr.Status.AtProvider = *observation

if hasFailedCondition(observation.Conditions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate the concerns of conditions into an extra function, either in the controller or client directly.
Also the Messages should go into constants on top. Then the Observe() gets a bit cleaner again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should unify this for KymaModule and ServiceInstance and have it in some common folder, otherwise we are duplicating a lot of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree. I was holding off touching any parts that would require refactoring any existing code. I would like it do this eventually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge On PRs, this prevents merging through the status checks release-notes/ignore This PR will be ignored for the release note generation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments