diff --git a/cmd/controller/tmp.go b/cmd/controller/tmp.go index 9991b1ba..b8429360 100644 --- a/cmd/controller/tmp.go +++ b/cmd/controller/tmp.go @@ -12,8 +12,10 @@ import ( "strings" "github.com/sap/cap-operator/internal/controller" + "github.com/sap/cap-operator/internal/util" "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" "github.com/sap/cap-operator/pkg/client/clientset/versioned" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" @@ -145,6 +147,9 @@ func migrateAppsAndSecrets(migrationDone chan bool, crdClient versioned.Interfac migrateCAPApplicationVersions(crdClient, ca.Namespace, ca.Name, appIdHash, appId) migrateCAPTenants(crdClient, ca.Namespace, ca.Name, appIdHash, appId) + // Create SubscriptionProvider and Subscription resources for any existing consumer tenants + createSubscriptionResourcesForCA(crdClient, kubeClient, &ca) + // Remove secrets that were preserved by the finalizer in the past. cleanupSecrets(ca.Namespace, kubeClient) } @@ -227,3 +232,139 @@ func annotateAllTenants(crdClient versioned.Interface) { } klog.InfoS("Annotated CAPTenants with subscription-guid", "count", count) } + +// createSubscriptionResourcesForCA creates a SubscriptionProvider for the given CAPApplication (if not present) +// and a Subscription for each existing consumer CAPTenant that has a subscription context secret. +// Skipped when the CA has no providerSubaccountId set, or when it is a services-only scenario. +func createSubscriptionResourcesForCA(crdClient versioned.Interface, kubeClient kubernetes.Interface, ca *v1alpha1.CAPApplication) { + if ca.Spec.ProviderSubaccountId == "" || ca.IsServicesOnly() { + return + } + createSubscriptionProviderIfNeeded(crdClient, ca) + createSubscriptionsForTenants(crdClient, kubeClient, ca) +} + +func createSubscriptionProviderIfNeeded(crdClient versioned.Interface, ca *v1alpha1.CAPApplication) { + _, err := crdClient.SmeV1alpha1().SubscriptionProviders(ca.Namespace).Get(context.TODO(), ca.Name, metav1.GetOptions{}) + if err == nil { + return + } + if !k8sErrors.IsNotFound(err) { + klog.ErrorS(err, "Failed to check SubscriptionProvider existence", "name", ca.Name, "namespace", ca.Namespace) + return + } + + var subscriptionInfo v1alpha1.SubscriptionInfo + for _, svc := range ca.Spec.BTP.Services { + switch svc.Class { + case "subscription-manager": + subscriptionInfo.Type = "subscription-manager" + subscriptionInfo.SubscriptionSecret = svc.Secret + case "saas-registry": + subscriptionInfo.Type = "saas-registry" + subscriptionInfo.SubscriptionSecret = svc.Secret + if xsuaaInfo := util.GetXSUAAInfo(ca.Spec.BTP.Services, ca); xsuaaInfo != nil { + subscriptionInfo.AuthSecret = xsuaaInfo.Secret + } + } + if subscriptionInfo.SubscriptionSecret != "" { + break + } + } + + appIdHash := sha1Sum(ca.Spec.ProviderSubaccountId, ca.Spec.BTPAppName) + _, err = crdClient.SmeV1alpha1().SubscriptionProviders(ca.Namespace).Create(context.TODO(), &v1alpha1.SubscriptionProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: ca.Name, + Namespace: ca.Namespace, + Labels: map[string]string{controller.LabelAppIdHash: appIdHash}, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind)), + }, + }, + Spec: v1alpha1.SubscriptionProviderSpec{ + AppName: ca.Spec.BTPAppName, + ProviderSubaccountID: ca.Spec.ProviderSubaccountId, + SubscriptionInfo: subscriptionInfo, + }, + }, metav1.CreateOptions{}) + if err != nil { + klog.ErrorS(err, "Failed to create SubscriptionProvider", "name", ca.Name, "namespace", ca.Namespace) + return + } + klog.InfoS("Created SubscriptionProvider", "name", ca.Name, "namespace", ca.Namespace) +} + +func createSubscriptionsForTenants(crdClient versioned.Interface, kubeClient kubernetes.Interface, ca *v1alpha1.CAPApplication) { + cats, err := crdClient.SmeV1alpha1().CAPTenants(ca.Namespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: ownerIdSelector(ca.Namespace, ca.Name), + }) + if err != nil { + klog.ErrorS(err, "Failed to list CAPTenants for subscription migration", "capApplication", ca.Name, "namespace", ca.Namespace) + return + } + + appIdHash := sha1Sum(ca.Spec.ProviderSubaccountId, ca.Spec.BTPAppName) + + for _, cat := range cats.Items { + // Skip provider tenants: require both subscription-guid label and annotation to be present + guid := cat.Labels[controller.MetadataSubscriptionGUID] + if guid == "" || cat.Annotations[controller.MetadataSubscriptionGUID] == "" { + continue + } + + // Skip tenants without a subscription context secret (no payload to migrate) + secretName := cat.Annotations[controller.AnnotationSubscriptionContextSecret] + if secretName == "" { + klog.InfoS("Skipping tenant without subscription context secret annotation", "tenant", cat.Name, "namespace", cat.Namespace) + continue + } + + // Skip if a Subscription for this tenant already exists + existingSubs, err := crdClient.SmeV1alpha1().Subscriptions(ca.Namespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(map[string]string{ + controller.LabelAppIdHash: appIdHash, + controller.LabelTenantId: cat.Spec.TenantId, + }).String(), + }) + if err != nil { + klog.ErrorS(err, "Failed to check existing Subscriptions", "tenant", cat.Name, "namespace", cat.Namespace) + continue + } + if len(existingSubs.Items) > 0 { + continue + } + + // Read the subscription context secret to get the original request payload + secret, err := kubeClient.CoreV1().Secrets(ca.Namespace).Get(context.TODO(), secretName, metav1.GetOptions{}) + if err != nil { + klog.ErrorS(err, "Failed to read subscription context secret", "secret", secretName, "tenant", cat.Name, "namespace", cat.Namespace) + continue + } + + _, err = crdClient.SmeV1alpha1().Subscriptions(ca.Namespace).Create(context.TODO(), &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: ca.Name + "-", + Namespace: ca.Namespace, + Labels: map[string]string{ + controller.LabelAppIdHash: appIdHash, + controller.LabelTenantId: cat.Spec.TenantId, + controller.MetadataSubscriptionGUID: guid, + }, + }, + Spec: v1alpha1.SubscriptionSpec{ + AppName: ca.Spec.BTPAppName, + ProviderSubaccountId: ca.Spec.ProviderSubaccountId, + TenantId: cat.Spec.TenantId, + Subdomain: cat.Spec.SubDomain, + SubscriptionGuid: guid, + SubscriptionRequestPayload: string(secret.Data["subscriptionContext"]), + }, + }, metav1.CreateOptions{}) + if err != nil { + klog.ErrorS(err, "Failed to create Subscription", "tenant", cat.Name, "namespace", cat.Namespace) + continue + } + klog.InfoS("Created Subscription for tenant", "tenant", cat.Name, "namespace", cat.Namespace, "guid", guid) + } +} diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 35c62178..38d9c72e 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -22,7 +22,6 @@ import ( "sync" "time" - corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -35,15 +34,12 @@ import ( ) const ( - AnnotationSubscriptionContextSecret = "sme.sap.com/subscription-context-secret" - AnnotationSaaSAdditionalOutput = "sme.sap.com/saas-additional-output" - AnnotationSubscriptionDomain = "sme.sap.com/subscription-domain" + AnnotationSaaSAdditionalOutput = "sme.sap.com/saas-additional-output" ) const ( LabelAppIdHash = "sme.sap.com/app-identifier-hash" LabelTenantId = "sme.sap.com/btp-tenant-id" - LabelTenantType = "sme.sap.com/tenant-type" MetadataSubscriptionGUID = "sme.sap.com/subscription-guid" ) @@ -56,7 +52,6 @@ const ( TenantNotFound = "tenant not found" ) -const SubscriptionDomain = "subscription domain" const ErrorOccurred = "Error occurred " const InvalidRequestMethod = "invalid request method" const AuthorizationCheckFailed = "authorization check failed" @@ -115,8 +110,8 @@ type requestHeaderDetails struct { } type Result struct { - Tenant *v1alpha1.CAPTenant - Message string + Subscription *v1alpha1.Subscription + Message string } type SubscriptionHandler struct { @@ -158,20 +153,6 @@ type tenantInfo struct { tenantSubDomain string } -type serviceCredentials struct { - XSAppName string `json:"xsappname"` - SaasRegistryEnabled bool `json:"saasregistryenabled"` - UAA *struct { - XSAppName string `json:"xsappname"` - } `json:"uaa"` -} - -// Credentials with plan -type serviceMetaInfo struct { - Plan string `json:"plan"` - Credentials serviceCredentials `json:"credentials"` -} - type GetDependenciesAuthError struct{} func (err *GetDependenciesAuthError) Error() string { @@ -184,61 +165,49 @@ func (s *SubscriptionHandler) CreateTenant(reqInfo *RequestInfo) *Result { var saasData *util.SaasRegistryCredentials var smsData *util.SmsCredentials - // Check if CAPApplication instance for the given btpApp exists - ca, err := s.checkCAPApp(reqInfo.payload.providerSubaccountId, reqInfo.payload.appName) + // Check if a SubscriptionProvider exists matching the providerSubaccountID and appName + subPro, err := s.checkSubscriptionProvider(reqInfo.payload.providerSubaccountId, reqInfo.payload.appName) if err != nil { - util.LogError(err, ErrorOccurred, TenantProvisioning, ca, nil) - return &Result{Tenant: nil, Message: err.Error()} - } - if ca.IsServicesOnly() { - err := errors.New("no multi-tenant capapplication found") - util.LogError(err, "CAPApplication invalid", TenantProvisioning, ca, nil) - return &Result{Tenant: nil, Message: err.Error()} + util.LogError(err, ErrorOccurred, TenantProvisioning, subPro, nil) + return &Result{Subscription: nil, Message: err.Error()} } - saasData, smsData, err = s.authorizationCheck(reqInfo.headerDetails, ca, reqInfo.subscriptionType, TenantProvisioning) + saasData, smsData, err = s.authorizationCheck(reqInfo.headerDetails, subPro, reqInfo.subscriptionType, TenantProvisioning) if err != nil { - util.LogError(err, AuthorizationCheckFailed, TenantProvisioning, ca, nil) - return &Result{Tenant: nil, Message: err.Error()} + util.LogError(err, AuthorizationCheckFailed, TenantProvisioning, subPro, nil) + return &Result{Subscription: nil, Message: err.Error()} } - appUrl, err := s.getAppURL(reqInfo.subscriptionDomain, reqInfo.payload.subdomain, ca) - if err != nil { - util.LogError(err, ErrorOccurred, TenantProvisioning, ca, nil) - return &Result{Tenant: nil, Message: "Error constructing subscription URL: " + err.Error()} - } - - // Check if A CRO for CAPTenant already exists - tenant := s.getTenantByAppIdentifier(ca.Spec.ProviderSubaccountId, reqInfo.payload.appName, reqInfo.payload.tenantId, ca.Namespace, TenantProvisioning).Tenant + // Check if a Subscription resource already exists for this payload + sub := s.getSubscriptionByAppIdentifier(reqInfo.payload.providerSubaccountId, reqInfo.payload.appName, reqInfo.payload.tenantId, subPro.Namespace, TenantProvisioning).Subscription // If the resource doesn't exist, we'll create it - if tenant == nil { + if sub == nil { created = true - tenant, err = s.createTenant(reqInfo, ca) + sub, err = s.createSubscription(reqInfo, subPro) if err != nil { - return &Result{Tenant: nil, Message: err.Error()} + return &Result{Subscription: nil, Message: err.Error()} } } else { - // Update the tenant metadata and subscription context secret with new subscription guid and context if needed (subscriptionGUID maybe different when a new provisioning request comes in for an existing tenant) - updated, err = s.updateTenant(reqInfo, ca, tenant) + // Update the subscription with the new subscription guid, payload and additional context if needed (subscriptionGUID maybe different when a new provisioning request comes in for an existing tenant) + updated, err = s.updateSubscription(reqInfo, subPro, sub) if err != nil { - return &Result{Tenant: nil, Message: err.Error()} + return &Result{Subscription: nil, Message: err.Error()} } } - // TODO: consider retrying tenant creation if it is in Error state - if tenant != nil { + if sub != nil { tenantIn := tenantInfo{tenantId: reqInfo.payload.tenantId, tenantSubDomain: reqInfo.payload.subdomain} callbackReqInfo := s.getCallbackReqInfo(reqInfo.subscriptionType, reqInfo.headerDetails.callbackInfo, saasData, smsData) - s.initializeCallback(appUrl, tenant.Name, ca, callbackReqInfo, tenantIn, true) + s.initializeCallback(sub.Name, sub.Namespace, subPro, callbackReqInfo, tenantIn, true) } if created { - util.LogInfo("Tenant successfully created", TenantProvisioning, ca, tenant, "message", getMessage(created, updated)) + util.LogInfo("Subscription successfully created", TenantProvisioning, subPro, sub, "message", getMessage(created, updated)) } else if updated { - util.LogInfo("Tenant successfully updated", TenantProvisioning, ca, tenant, "message", getMessage(created, updated)) + util.LogInfo("Subscription successfully updated", TenantProvisioning, subPro, sub, "message", getMessage(created, updated)) } - return &Result{Tenant: tenant, Message: getMessage(created, updated)} + return &Result{Subscription: sub, Message: getMessage(created, updated)} } func getMessage(isCreated, isUpdated bool) string { @@ -252,105 +221,65 @@ func getMessage(isCreated, isUpdated bool) string { } } -func (s *SubscriptionHandler) createTenant(reqInfo *RequestInfo, ca *v1alpha1.CAPApplication) (tenant *v1alpha1.CAPTenant, err error) { +func (s *SubscriptionHandler) createSubscription(reqInfo *RequestInfo, subPro *v1alpha1.SubscriptionProvider) (*v1alpha1.Subscription, error) { subscriptionGUID := reqInfo.payload.subscriptionGUID jsonReqByte, _ := json.Marshal(reqInfo.payload.raw) - // Create a secret to store the subscription context (payload from the request) - secret, err := s.KubeClientset.CoreV1().Secrets(ca.Namespace).Create(context.TODO(), &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: ca.Name + "-consumer-", - Namespace: ca.Namespace, - Labels: map[string]string{ - LabelTenantId: reqInfo.payload.tenantId, - MetadataSubscriptionGUID: subscriptionGUID, - }, - }, - StringData: map[string]string{ - "subscriptionContext": string(jsonReqByte), - }, - }, metav1.CreateOptions{}) - if err != nil { - // Log error and exit if secret creation fails - util.LogError(err, "Error creating subscription context secret", TenantProvisioning, ca, nil) - return nil, err - } - util.LogInfo("Creating tenant", TenantProvisioning, ca, nil) - tenant, err = s.Clientset.SmeV1alpha1().CAPTenants(ca.Namespace).Create(context.TODO(), &v1alpha1.CAPTenant{ + util.LogInfo("Creating subscription", TenantProvisioning, subPro, nil, "tenantId", reqInfo.payload.tenantId, "subscriptionGuid", subscriptionGUID) + + sub, err := s.Clientset.SmeV1alpha1().Subscriptions(subPro.Namespace).Create(context.TODO(), &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ - GenerateName: ca.Name + "-", - Namespace: ca.Namespace, - Annotations: map[string]string{ - AnnotationSubscriptionContextSecret: secret.Name, // Store the secret name in the tenant annotation - MetadataSubscriptionGUID: subscriptionGUID, - }, + GenerateName: subPro.Name + "-", + Namespace: subPro.Namespace, Labels: map[string]string{ + LabelAppIdHash: sha1Sum(reqInfo.payload.providerSubaccountId, reqInfo.payload.appName), LabelTenantId: reqInfo.payload.tenantId, MetadataSubscriptionGUID: subscriptionGUID, - LabelTenantType: "consumer", // Default tenant type for consumer tenants }, }, - Spec: v1alpha1.CAPTenantSpec{ - CAPApplicationInstance: ca.Name, - BTPTenantIdentification: v1alpha1.BTPTenantIdentification{ - SubDomain: reqInfo.payload.subdomain, - TenantId: reqInfo.payload.tenantId, - }, + Spec: v1alpha1.SubscriptionSpec{ + AppName: reqInfo.payload.appName, + ProviderSubaccountId: reqInfo.payload.providerSubaccountId, + TenantId: reqInfo.payload.tenantId, + Subdomain: reqInfo.payload.subdomain, + SubscriptionGuid: subscriptionGUID, + SubscriptionDomain: reqInfo.subscriptionDomain, + SubscriptionRequestPayload: string(jsonReqByte), }, }, metav1.CreateOptions{}) - if err != nil || tenant == nil { - // Log error and exit if tenant creation fails - util.LogError(err, "Error creating tenant", TenantProvisioning, ca, nil) + if err != nil || sub == nil { + util.LogError(err, "Error creating subscription", TenantProvisioning, subPro, nil, "tenantId", reqInfo.payload.tenantId) return nil, err } - // Update secret with tenant info and return - return tenant, s.updateSecret(tenant, secret) + return sub, nil } -func (s *SubscriptionHandler) updateTenant(reqInfo *RequestInfo, ca *v1alpha1.CAPApplication, tenant *v1alpha1.CAPTenant) (bool, error) { - updated := false - - // Update the tenant labels if needed - if tenant.Labels[MetadataSubscriptionGUID] != reqInfo.payload.subscriptionGUID { - tenant.Labels[MetadataSubscriptionGUID] = reqInfo.payload.subscriptionGUID - tenant.Annotations[MetadataSubscriptionGUID] = reqInfo.payload.subscriptionGUID - util.LogInfo("Updating tenant subscriptionGUID label", TenantProvisioning, tenant, nil) - if _, err := s.Clientset.SmeV1alpha1().CAPTenants(ca.Namespace).Update(context.TODO(), tenant, metav1.UpdateOptions{}); err != nil { - util.LogError(err, "Error updating tenant labels", TenantProvisioning, tenant, nil) - return false, err - } - updated = true - } +func (s *SubscriptionHandler) updateSubscription(reqInfo *RequestInfo, subPro *v1alpha1.SubscriptionProvider, sub *v1alpha1.Subscription) (bool, error) { + jsonReqByte, _ := json.Marshal(reqInfo.payload.raw) - // Update the secret to store the new subscription context (payload from the request) if needed - if tenant.Annotations[AnnotationSubscriptionContextSecret] == "" { - return updated, nil + // Nothing changed --> no update needed + if sub.Spec.SubscriptionGuid == reqInfo.payload.subscriptionGUID && + sub.Spec.SubscriptionDomain == reqInfo.subscriptionDomain && + sub.Spec.SubscriptionRequestPayload == string(jsonReqByte) { + return false, nil } - secret, err := s.KubeClientset.CoreV1().Secrets(ca.Namespace).Get(context.TODO(), tenant.Annotations[AnnotationSubscriptionContextSecret], metav1.GetOptions{}) - if err != nil { - util.LogError(err, "subscription context secret not found", TenantProvisioning, tenant, nil, "secretName", tenant.Annotations[AnnotationSubscriptionContextSecret]) - return updated, err + sub.Spec.SubscriptionGuid = reqInfo.payload.subscriptionGUID + sub.Spec.SubscriptionDomain = reqInfo.subscriptionDomain + sub.Spec.SubscriptionRequestPayload = string(jsonReqByte) + if sub.Labels == nil { + sub.Labels = map[string]string{} } + sub.Labels[MetadataSubscriptionGUID] = reqInfo.payload.subscriptionGUID - if secret.Labels[MetadataSubscriptionGUID] != reqInfo.payload.subscriptionGUID { - secret.Labels[MetadataSubscriptionGUID] = reqInfo.payload.subscriptionGUID - jsonReqByte, _ := json.Marshal(reqInfo.payload.raw) - secret.StringData = map[string]string{ - "subscriptionContext": string(jsonReqByte), - } - - util.LogInfo("Updating tenant subscription context secret", TenantProvisioning, secret, nil, "tenantName", tenant.Name) - _, err = s.KubeClientset.CoreV1().Secrets(ca.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) - if err != nil { - util.LogError(err, "Error updating subscription context secret", TenantProvisioning, secret, secret) - return false, err - } - updated = true + util.LogInfo("Updating subscription", TenantProvisioning, sub, nil, "tenantId", reqInfo.payload.tenantId, "subscriptionGuid", reqInfo.payload.subscriptionGUID) + if _, err := s.Clientset.SmeV1alpha1().Subscriptions(subPro.Namespace).Update(context.TODO(), sub, metav1.UpdateOptions{}); err != nil { + util.LogError(err, "Error updating subscription", TenantProvisioning, sub, nil) + return false, err } - return updated, nil + return true, nil } func extractTimeoutInMillis(appUrls string, isSMS bool) string { @@ -423,56 +352,43 @@ func (s *SubscriptionHandler) getCallbackReqInfo(subscriptionType subscriptionTy return callbackReqInfo } -func (s *SubscriptionHandler) updateSecret(tenant *v1alpha1.CAPTenant, secret *corev1.Secret) error { - secret.OwnerReferences = []metav1.OwnerReference{ - *metav1.NewControllerRef(tenant, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind)), - } - _, err := s.KubeClientset.CoreV1().Secrets(tenant.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) - if err != nil { - util.LogError(err, "Error updating payload tenant subscription secret", TenantProvisioning, tenant, secret) - } - return err -} - -func (s *SubscriptionHandler) getTenantByAppIdentifier(providerSubaccountId, btpAppName, tenantId, namespace, step string) (result *Result) { - tenantLabels := map[string]string{ - LabelTenantId: tenantId, +func (s *SubscriptionHandler) getSubscriptionByAppIdentifier(providerSubaccountId, btpAppName, tenantId, namespace, step string) (result *Result) { + labelsMap := map[string]string{ + LabelTenantId: tenantId, + LabelAppIdHash: sha1Sum(providerSubaccountId, btpAppName), } - labelsMaps := maps.Clone(tenantLabels) - labelsMaps[LabelAppIdHash] = sha1Sum(providerSubaccountId, btpAppName) - - return s.getTenantByLabels(labelsMaps, namespace, step, "getTenantByAppIdentifier") + return s.getSubscriptionByLabels(labelsMap, namespace, step, "getSubscriptionByAppIdentifier") } -func (s *SubscriptionHandler) getTenantBySubscriptionGUID(subscriptionGUID, tenantId, step string) *Result { +func (s *SubscriptionHandler) getSubscriptionBySubscriptionGUID(subscriptionGUID, tenantId, step string) *Result { labelsMap := map[string]string{ MetadataSubscriptionGUID: subscriptionGUID, LabelTenantId: tenantId, } - return s.getTenantByLabels(labelsMap, metav1.NamespaceAll, step, "getTenantBySubscriptionGUID") + return s.getSubscriptionByLabels(labelsMap, metav1.NamespaceAll, step, "getSubscriptionBySubscriptionGUID") } -func (s *SubscriptionHandler) getTenantByLabels(labelsMap map[string]string, namespace, step, methodName string) *Result { +func (s *SubscriptionHandler) getSubscriptionByLabels(labelsMap map[string]string, namespace, step, methodName string) *Result { labelSelector, err := labels.ValidatedSelectorFromSet(labelsMap) if err != nil { util.LogError(err, "Error in "+methodName, step, methodName, nil, flattenLabels(labelsMap)...) - return &Result{Tenant: nil, Message: err.Error()} + return &Result{Subscription: nil, Message: err.Error()} } - ctList, err := s.Clientset.SmeV1alpha1().CAPTenants(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) + subList, err := s.Clientset.SmeV1alpha1().Subscriptions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) if err != nil { util.LogError(err, "Error in "+methodName, step, methodName, nil, flattenLabels(labelsMap)...) - return &Result{Tenant: nil, Message: err.Error()} + return &Result{Subscription: nil, Message: err.Error()} } - if len(ctList.Items) == 0 { - util.LogInfo("No tenant found", step, methodName, nil, flattenLabels(labelsMap)...) - return &Result{Tenant: nil, Message: ResourceNotFound} + if len(subList.Items) == 0 { + util.LogInfo("No subscription found", step, methodName, nil, flattenLabels(labelsMap)...) + return &Result{Subscription: nil, Message: ResourceNotFound} } - // Assume only 1 tenant actually matches the selector! - util.LogInfo("Tenant found", step, &ctList.Items[0], nil, flattenLabels(labelsMap, "namespace", &ctList.Items[0].Namespace)...) - return &Result{Tenant: &ctList.Items[0], Message: ResourceFound} + // Assume only 1 subscription actually matches the selector! + util.LogInfo("Subscription found", step, &subList.Items[0], nil, flattenLabels(labelsMap, "namespace", &subList.Items[0].Namespace)...) + return &Result{Subscription: &subList.Items[0], Message: ResourceFound} } func flattenLabels(labelsMap map[string]string, args ...any) []any { @@ -488,61 +404,61 @@ func flattenLabels(labelsMap map[string]string, args ...any) []any { func (s *SubscriptionHandler) DeleteTenant(reqInfo *RequestInfo) *Result { var saasData *util.SaasRegistryCredentials var smsData *util.SmsCredentials - var tenant *v1alpha1.CAPTenant - var ca *v1alpha1.CAPApplication + var sub *v1alpha1.Subscription + var subPro *v1alpha1.SubscriptionProvider var err error util.LogInfo("Delete Tenant triggered", TenantDeprovisioning, "DeleteTenant", nil) - // Check if tenant exists by subscriptionGUID and tenantId - tenant = s.getTenantBySubscriptionGUID(reqInfo.payload.subscriptionGUID, reqInfo.payload.tenantId, TenantDeprovisioning).Tenant - if tenant != nil { - ca, err = s.Clientset.SmeV1alpha1().CAPApplications(tenant.Namespace).Get(context.TODO(), tenant.Spec.CAPApplicationInstance, metav1.GetOptions{}) + // Check if a Subscription exists by subscriptionGUID and tenantId + sub = s.getSubscriptionBySubscriptionGUID(reqInfo.payload.subscriptionGUID, reqInfo.payload.tenantId, TenantDeprovisioning).Subscription + if sub != nil { + subPro, err = s.checkSubscriptionProviderInNamespace(sub.Spec.ProviderSubaccountId, sub.Spec.AppName, sub.Namespace) if err != nil { - util.LogError(err, "CAPApplication not found", TenantDeprovisioning, tenant, nil) - return &Result{Tenant: nil, Message: err.Error()} + util.LogError(err, "SubscriptionProvider not found", TenantDeprovisioning, sub, nil) + return &Result{Subscription: nil, Message: err.Error()} } } else if reqInfo.subscriptionType == SaaS { - ca, err = s.checkCAPApp(reqInfo.payload.providerSubaccountId, reqInfo.payload.appName) + subPro, err = s.checkSubscriptionProvider(reqInfo.payload.providerSubaccountId, reqInfo.payload.appName) if err != nil { - util.LogError(err, "CAPApplication not found", TenantDeprovisioning, tenant, nil) - return &Result{Tenant: nil, Message: TenantNotFound} + util.LogError(err, "SubscriptionProvider not found", TenantDeprovisioning, nil, nil) + return &Result{Subscription: nil, Message: TenantNotFound} } - // if tenant is not found in SaaS subscription scenario, check if it exists by btpApp identifier to handle cases where tenant was created without subscriptionGUID - util.LogInfo("Tenant not found by subscriptionGUID, checking by BTP app identifier", TenantDeprovisioning, "DeleteTenant", nil, "subscriptionGUID", reqInfo.payload.subscriptionGUID) - tenant = s.getTenantByAppIdentifier(ca.Spec.ProviderSubaccountId, reqInfo.payload.appName, reqInfo.payload.tenantId, metav1.NamespaceAll, TenantDeprovisioning).Tenant + // if subscription is not found in SaaS subscription scenario, check if it exists by btpApp identifier to handle cases where it was created without subscriptionGUID + util.LogInfo("Subscription not found by subscriptionGUID, checking by BTP app identifier", TenantDeprovisioning, "DeleteTenant", nil, "subscriptionGUID", reqInfo.payload.subscriptionGUID) + sub = s.getSubscriptionByAppIdentifier(subPro.Spec.ProviderSubaccountID, reqInfo.payload.appName, reqInfo.payload.tenantId, metav1.NamespaceAll, TenantDeprovisioning).Subscription } - if tenant == nil { - util.LogWarning("CAPTenant not found", TenantDeprovisioning) - return &Result{Tenant: nil, Message: TenantNotFound} + if sub == nil { + util.LogWarning("Subscription not found", TenantDeprovisioning) + return &Result{Subscription: nil, Message: TenantNotFound} } - saasData, smsData, err = s.authorizationCheck(reqInfo.headerDetails, ca, reqInfo.subscriptionType, TenantDeprovisioning) + saasData, smsData, err = s.authorizationCheck(reqInfo.headerDetails, subPro, reqInfo.subscriptionType, TenantDeprovisioning) if err != nil { - util.LogError(err, AuthorizationCheckFailed, TenantDeprovisioning, ca, nil) - return &Result{Tenant: nil, Message: err.Error()} + util.LogError(err, AuthorizationCheckFailed, TenantDeprovisioning, subPro, nil) + return &Result{Subscription: nil, Message: err.Error()} } - util.LogInfo("Tenant found", TenantDeprovisioning, ca, tenant) - err = s.Clientset.SmeV1alpha1().CAPTenants(tenant.Namespace).Delete(context.TODO(), tenant.Name, metav1.DeleteOptions{}) + util.LogInfo("Subscription found", TenantDeprovisioning, subPro, sub) + err = s.Clientset.SmeV1alpha1().Subscriptions(sub.Namespace).Delete(context.TODO(), sub.Name, metav1.DeleteOptions{}) if err != nil { - util.LogError(err, "Error deleting tenant", TenantDeprovisioning, ca, tenant) - return &Result{Tenant: nil, Message: err.Error()} + util.LogError(err, "Error deleting subscription", TenantDeprovisioning, subPro, sub) + return &Result{Subscription: nil, Message: err.Error()} } tenantIn := tenantInfo{tenantId: reqInfo.payload.tenantId, tenantSubDomain: reqInfo.payload.subdomain} callbackReqInfo := s.getCallbackReqInfo(reqInfo.subscriptionType, reqInfo.headerDetails.callbackInfo, saasData, smsData) - s.initializeCallback("", tenant.Name, ca, callbackReqInfo, tenantIn, false) + s.initializeCallback(sub.Name, sub.Namespace, subPro, callbackReqInfo, tenantIn, false) - return &Result{Tenant: tenant, Message: ResourceDeleted} + return &Result{Subscription: sub, Message: ResourceDeleted} } -func (s *SubscriptionHandler) authorizationCheck(headerDetails *requestHeaderDetails, ca *v1alpha1.CAPApplication, subscription subscriptionType, step string) (saasData *util.SaasRegistryCredentials, smsData *util.SmsCredentials, err error) { +func (s *SubscriptionHandler) authorizationCheck(headerDetails *requestHeaderDetails, subPro *v1alpha1.SubscriptionProvider, subscription subscriptionType, step string) (saasData *util.SaasRegistryCredentials, smsData *util.SmsCredentials, err error) { switch subscription { case SMS: // fetch SMS information - smsData = s.getSmsDetails(ca, step) + smsData = s.getSmsDetails(subPro, step) if smsData == nil { return nil, nil, errors.New(ResourceNotFound) } @@ -553,7 +469,7 @@ func (s *SubscriptionHandler) authorizationCheck(headerDetails *requestHeaderDet default: var uaaData *util.XSUAACredentials // fetch SaaS Registry and XSUAA information - saasData, uaaData = s.getServiceDetails(ca, step) + saasData, uaaData = s.getServiceDetails(subPro, step) if saasData == nil || uaaData == nil { return nil, nil, errors.New(ResourceNotFound) } @@ -564,25 +480,34 @@ func (s *SubscriptionHandler) authorizationCheck(headerDetails *requestHeaderDet return } -func (s *SubscriptionHandler) checkCAPApp(providerSubaccountId, btpAppName string) (*v1alpha1.CAPApplication, error) { - // First try to find CAPApplication by providerSubaccountId (appIdHash) +func (s *SubscriptionHandler) checkSubscriptionProvider(providerSubaccountId, btpAppName string) (*v1alpha1.SubscriptionProvider, error) { + // Find SubscriptionProvider by providerSubaccountId + appName (appIdHash) across all namespaces labelSelector, _ := labels.ValidatedSelectorFromSet(map[string]string{ LabelAppIdHash: sha1Sum(providerSubaccountId, btpAppName), }) - return s.getAppByLabelSelector(labelSelector) + return s.getSubscriptionProviderByLabelSelector(metav1.NamespaceAll, labelSelector) } -func (s *SubscriptionHandler) getAppByLabelSelector(labelSelector labels.Selector) (*v1alpha1.CAPApplication, error) { - capAppsList, err := s.Clientset.SmeV1alpha1().CAPApplications(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) +func (s *SubscriptionHandler) checkSubscriptionProviderInNamespace(providerSubaccountId, btpAppName, namespace string) (*v1alpha1.SubscriptionProvider, error) { + // Find SubscriptionProvider by providerSubaccountId + appName (appIdHash) in a specific namespace + labelSelector, _ := labels.ValidatedSelectorFromSet(map[string]string{ + LabelAppIdHash: sha1Sum(providerSubaccountId, btpAppName), + }) + + return s.getSubscriptionProviderByLabelSelector(namespace, labelSelector) +} + +func (s *SubscriptionHandler) getSubscriptionProviderByLabelSelector(namespace string, labelSelector labels.Selector) (*v1alpha1.SubscriptionProvider, error) { + subProList, err := s.Clientset.SmeV1alpha1().SubscriptionProviders(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) if err != nil { return nil, err } - if len(capAppsList.Items) == 0 { + if len(subProList.Items) == 0 { return nil, errors.New(ResourceNotFound) } - // Assume only 1 app actually matches the selector! - return &capAppsList.Items[0], nil + // Assume only 1 provider actually matches the selector! + return &subProList.Items[0], nil } func (s *SubscriptionHandler) checkAuthorization(authHeader string, saasData *util.SaasRegistryCredentials, uaaData *util.XSUAACredentials, step string) error { @@ -615,38 +540,38 @@ func (s *SubscriptionHandler) checkCertIssuerAndSubject(xForwardedClientCert str return nil } -func (s *SubscriptionHandler) initializeCallback(appUrl, tenantName string, ca *v1alpha1.CAPApplication, callbackReqInfo *CallbackReqInfo, tenantIn tenantInfo, isProvisioning bool) { +func (s *SubscriptionHandler) initializeCallback(subName, subNamespace string, subPro *v1alpha1.SubscriptionProvider, callbackReqInfo *CallbackReqInfo, tenantIn tenantInfo, isProvisioning bool) { step := TenantProvisioning if !isProvisioning { step = TenantDeprovisioning } - util.LogInfo("Callback initialized", step, ca, nil, "subscription URL", appUrl, "async callback path", callbackReqInfo.CallbackPath, "tenantName", tenantName) + util.LogInfo("Callback initialized", step, subPro, nil, "async callback path", callbackReqInfo.CallbackPath, "subscription", subName) go func() { - // create a context for tenant checks and outgoing requests + // create a context for subscription checks and outgoing requests ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // Check tenant status asynchronously - util.LogInfo("Starting tenant status check", step, ca, nil, "tenantName", tenantName) - status := s.checkCAPTenantStatus(ctx, ca.Namespace, tenantName, isProvisioning, callbackReqInfo.CallbackTimeoutMillis) - util.LogInfo("Tenant status check complete", step, ca, nil, "tenantName", tenantName, "status", status) + // Check subscription status asynchronously + util.LogInfo("Starting subscription status check", step, subPro, nil, "subscription", subName) + status, appUrl := s.checkSubscriptionStatus(ctx, subNamespace, subName, isProvisioning, callbackReqInfo.CallbackTimeoutMillis) + util.LogInfo("Subscription status check complete", step, subPro, nil, "subscription", subName, "status", status, "subscription URL", appUrl) additionalOutput := &map[string]any{} if isProvisioning { - saasAdditionalOutput := ca.Annotations[AnnotationSaaSAdditionalOutput] + saasAdditionalOutput := subPro.Annotations[AnnotationSaaSAdditionalOutput] if saasAdditionalOutput != "" { // Add additional output to the callback response err := json.Unmarshal([]byte(saasAdditionalOutput), additionalOutput) if err != nil { - util.LogError(err, "Error parsing additional output", step, ca, nil, "annotation value", saasAdditionalOutput) + util.LogError(err, "Error parsing additional output", step, subPro, nil, "annotation value", saasAdditionalOutput) additionalOutput = nil } } // Add tenant data to the additional output if it exists - err := s.enrichAdditionalOutput(ca.Namespace, tenantIn.tenantId, additionalOutput) + err := s.enrichAdditionalOutput(subPro.Namespace, tenantIn.tenantId, additionalOutput) if err != nil { - util.LogError(err, "Error updating tenant data", step, ca, nil, "tenantId", tenantIn.tenantId) + util.LogError(err, "Error updating tenant data", step, subPro, nil, "tenantId", tenantIn.tenantId) } } else { additionalOutput = nil @@ -656,89 +581,6 @@ func (s *SubscriptionHandler) initializeCallback(appUrl, tenantName string, ca * }() } -func (s *SubscriptionHandler) getAppURL(payloadSubscriptionDomain, tenantSubdomain string, ca *v1alpha1.CAPApplication) (string, error) { - needsValidation := true - var subscriptionDomain string - // Check if subscription domain is provided in the request payload. - if payloadSubscriptionDomain != "" { - subscriptionDomain = payloadSubscriptionDomain - util.LogInfo("Using subscription domain from request payload", TenantProvisioning, ca, nil, SubscriptionDomain, subscriptionDomain) - } else { - // Fallback: - // First, check if subscription domain is provided in the CAPApplication annotation. If not, fallback to calculating the primary domain from the CAPApplication domain refs and use that as the subscription domain. - subscriptionDomain = ca.Annotations[AnnotationSubscriptionDomain] - if subscriptionDomain == "" { - subscriptionDomain = s.getPrimaryDomain(ca) - needsValidation = false - util.LogInfo("Using subscription domain from fallback 'primary' calculation", TenantProvisioning, ca, nil, SubscriptionDomain, subscriptionDomain) - } else { - util.LogInfo("Using subscription domain from CAPApplication annotation", TenantProvisioning, ca, nil, SubscriptionDomain, subscriptionDomain) - } - } - - if needsValidation { - err := s.validateDomain(subscriptionDomain, ca.Namespace) - if err != nil { - return "", err - } - } - - return "https://" + tenantSubdomain + "." + subscriptionDomain, nil -} - -func (s *SubscriptionHandler) validateDomain(domain, namespace string) error { - // First check for Domains in the apps namespace - domainsList, err := s.Clientset.SmeV1alpha1().Domains(namespace).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return err - } - for _, d := range domainsList.Items { - if d.Spec.Domain == domain { - return nil - } - } - - // Check for ClusterDomains if not found in the namespace - clusterDomainsList, err := s.Clientset.SmeV1alpha1().ClusterDomains(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return err - } - for _, cd := range clusterDomainsList.Items { - if cd.Spec.Domain == domain { - return nil - } - } - - return fmt.Errorf("domain %s not found in Domains or ClusterDomains", domain) -} - -func (s *SubscriptionHandler) getPrimaryDomain(ca *v1alpha1.CAPApplication) string { - // If no domainRefs are specified, return an empty string - if len(ca.Spec.DomainRefs) == 0 { - return "" - } - // Return the first domain as the primary domain - primaryDomainRef := ca.Spec.DomainRefs[0] - domain := "" - if primaryDomainRef.Kind == v1alpha1.DomainKind { - primaryDom, err := s.Clientset.SmeV1alpha1().Domains(ca.Namespace).Get(context.TODO(), primaryDomainRef.Name, metav1.GetOptions{}) - if err != nil { - util.LogError(err, "Error getting primary domain", TenantProvisioning, ca, nil, "domainRef", primaryDomainRef.Name) - } else if primaryDom != nil { - domain = primaryDom.Spec.Domain - } - } else { - primaryDom, err := s.Clientset.SmeV1alpha1().ClusterDomains(metav1.NamespaceAll).Get(context.TODO(), primaryDomainRef.Name, metav1.GetOptions{}) - if err != nil { - util.LogError(err, "Error getting primary cluster domain", TenantProvisioning, ca, nil, "domainRef", primaryDomainRef.Name) - } else if primaryDom != nil { - domain = primaryDom.Spec.Domain - } - } - // Return the primary domain if it exists, else return an empty string - return domain -} - func (s *SubscriptionHandler) enrichAdditionalOutput(namespace string, tenantId string, additionalOutput *map[string]any) error { labelSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ LabelTenantId: tenantId, @@ -765,7 +607,7 @@ func (s *SubscriptionHandler) enrichAdditionalOutput(namespace string, tenantId return nil } -func (s *SubscriptionHandler) checkCAPTenantStatus(ctx context.Context, tenantNamespace string, tenantName string, provisioning bool, callbackTimeoutMs string) bool { +func (s *SubscriptionHandler) checkSubscriptionStatus(ctx context.Context, subNamespace string, subName string, provisioning bool, callbackTimeoutMs string) (ready bool, url string) { asyncCallbackTimeout := 15 * time.Minute if callbackTimeoutMs != "" { asyncCallbackTimeout, _ = time.ParseDuration(callbackTimeoutMs + "ms") @@ -776,27 +618,27 @@ func (s *SubscriptionHandler) checkCAPTenantStatus(ctx context.Context, tenantNa step = TenantDeprovisioning } - timedCtx, cancel := context.WithTimeout(ctx, asyncCallbackTimeout) // Assume tenants won't take over 15mins to be "Ready" + timedCtx, cancel := context.WithTimeout(ctx, asyncCallbackTimeout) // Assume subscriptions won't take over 15mins to be "Ready" defer cancel() for { select { case <-timedCtx.Done(): - klog.Warningf("tenant status check: %s", timedCtx.Err().Error()) - return false + klog.Warningf("subscription status check: %s", timedCtx.Err().Error()) + return false, "" default: - capTenant, err := s.Clientset.SmeV1alpha1().CAPTenants(tenantNamespace).Get(context.TODO(), tenantName, metav1.GetOptions{}) + sub, err := s.Clientset.SmeV1alpha1().Subscriptions(subNamespace).Get(context.TODO(), subName, metav1.GetOptions{}) if k8sErrors.IsNotFound(err) { - util.LogInfo("No tenant found.. Exiting CAPTenant status check.", step, "Tenant Status Check", nil, "tenantName", tenantName, "namespace", tenantNamespace) + util.LogInfo("No subscription found.. Exiting subscription status check.", step, "Subscription Status Check", nil, "subscription", subName, "namespace", subNamespace) if !provisioning { - return true + return true, "" } } - if capTenant != nil { - util.LogInfo("CAPTenant found", step, capTenant, nil, "tenantid", capTenant.Spec.TenantId, "status", capTenant.Status.State) - if provisioning && (capTenant.Status.State == v1alpha1.CAPTenantStateReady || capTenant.Status.State == v1alpha1.CAPTenantStateProvisioningError) { - util.LogInfo("Exiting CAPTenant status check", step, capTenant, nil, "tenantid", capTenant.Spec.TenantId, "status", capTenant.Status.State) - return capTenant.Status.State == v1alpha1.CAPTenantStateReady + if sub != nil { + util.LogInfo("Subscription found", step, sub, nil, "tenantid", sub.Spec.TenantId, "status", sub.Status.State) + if provisioning && (sub.Status.State == v1alpha1.SubscriptionStateReady || sub.Status.State == v1alpha1.SubscriptionStateError) { + util.LogInfo("Exiting subscription status check", step, sub, nil, "tenantid", sub.Spec.TenantId, "status", sub.Status.State) + return sub.Status.State == v1alpha1.SubscriptionStateReady, sub.Status.Url } } time.Sleep(5 * time.Second) @@ -804,79 +646,54 @@ func (s *SubscriptionHandler) checkCAPTenantStatus(ctx context.Context, tenantNa } } -func (s *SubscriptionHandler) getServiceDetails(ca *v1alpha1.CAPApplication, step string) (saasData *util.SaasRegistryCredentials, uaaData *util.XSUAACredentials) { +func (s *SubscriptionHandler) getServiceDetails(subPro *v1alpha1.SubscriptionProvider, step string) (saasData *util.SaasRegistryCredentials, uaaData *util.XSUAACredentials) { var wg sync.WaitGroup wg.Go(func() { - saasData = s.getSaasDetails(ca, step) + saasData = s.getSaasDetails(subPro, step) }) wg.Go(func() { - uaaData = s.getXSUAADetails(ca, step) + uaaData = s.getXSUAADetails(subPro, step) }) wg.Wait() return saasData, uaaData } -func (s *SubscriptionHandler) getSaasDetails(capApp *v1alpha1.CAPApplication, step string) *util.SaasRegistryCredentials { - var ( - result *util.SaasRegistryCredentials = nil - err error - info *v1alpha1.ServiceInfo - ) - if info, err = s.getServiceInfo(capApp, "saas-registry"); err == nil { - result, err = util.ReadServiceCredentialsFromSecret[util.SaasRegistryCredentials](info, capApp.Namespace, s.KubeClientset, false) - } +func (s *SubscriptionHandler) getSaasDetails(subPro *v1alpha1.SubscriptionProvider, step string) *util.SaasRegistryCredentials { + secret := subPro.Spec.SubscriptionInfo.SubscriptionSecret + info := &v1alpha1.ServiceInfo{Name: secret, Secret: secret} + result, err := util.ReadServiceCredentialsFromSecret[util.SaasRegistryCredentials](info, subPro.Namespace, s.KubeClientset, false) if err != nil { - util.LogError(err, "SaaS Registry credentials could not be read. Exiting..", step, capApp, nil) + util.LogError(err, "SaaS Registry credentials could not be read. Exiting..", step, subPro, nil) } return result } -func (s *SubscriptionHandler) getXSUAADetails(capApp *v1alpha1.CAPApplication, step string) *util.XSUAACredentials { - var ( - result *util.XSUAACredentials = nil - err error - info *v1alpha1.ServiceInfo - ) - info = util.GetXSUAAInfo(capApp.Spec.BTP.Services, capApp) - - if info == nil { - err = fmt.Errorf("could not find service with class %s in CAPApplication %s.%s", "xsuaa", capApp.Namespace, capApp.Name) - } else { - result, err = util.ReadServiceCredentialsFromSecret[util.XSUAACredentials](info, capApp.Namespace, s.KubeClientset, false) +func (s *SubscriptionHandler) getXSUAADetails(subPro *v1alpha1.SubscriptionProvider, step string) *util.XSUAACredentials { + secret := subPro.Spec.SubscriptionInfo.AuthSecret + if secret == "" { + util.LogError(fmt.Errorf("no auth secret configured in SubscriptionProvider %s.%s", subPro.Namespace, subPro.Name), "XSUAA credentials could not be read. Exiting..", step, subPro, nil) + return nil } - + info := &v1alpha1.ServiceInfo{Name: secret, Secret: secret} + result, err := util.ReadServiceCredentialsFromSecret[util.XSUAACredentials](info, subPro.Namespace, s.KubeClientset, false) if err != nil { - util.LogError(err, "XSUAA credentials could not be read. Exiting..", step, capApp, nil) + util.LogError(err, "XSUAA credentials could not be read. Exiting..", step, subPro, nil) } return result } -func (s *SubscriptionHandler) getSmsDetails(capApp *v1alpha1.CAPApplication, step string) *util.SmsCredentials { - var ( - result *util.SmsCredentials = nil - err error - info *v1alpha1.ServiceInfo - ) - if info, err = s.getServiceInfo(capApp, "subscription-manager"); err == nil { - result, err = util.ReadServiceCredentialsFromSecret[util.SmsCredentials](info, capApp.Namespace, s.KubeClientset, false) - } +func (s *SubscriptionHandler) getSmsDetails(subPro *v1alpha1.SubscriptionProvider, step string) *util.SmsCredentials { + secret := subPro.Spec.SubscriptionInfo.SubscriptionSecret + info := &v1alpha1.ServiceInfo{Name: secret, Secret: secret} + result, err := util.ReadServiceCredentialsFromSecret[util.SmsCredentials](info, subPro.Namespace, s.KubeClientset, false) if err != nil { - util.LogError(err, "SMS credentials could not be read. Exiting..", step, capApp, nil) + util.LogError(err, "SMS credentials could not be read. Exiting..", step, subPro, nil) } return result } -func (s *SubscriptionHandler) getServiceInfo(ca *v1alpha1.CAPApplication, serviceClass string) (*v1alpha1.ServiceInfo, error) { - for i := range ca.Spec.BTP.Services { - if ca.Spec.BTP.Services[i].Class == serviceClass { - return &ca.Spec.BTP.Services[i], nil - } - } - return nil, fmt.Errorf("could not find service with class %s in CAPApplication %s.%s", serviceClass, ca.Namespace, ca.Name) -} - func prepareTokenRequest(ctx context.Context, callbackReqInfo *CallbackReqInfo, client *http.Client) (tokenReq *http.Request, err error) { defer func() { if err != nil { @@ -1004,13 +821,13 @@ func (s *SubscriptionHandler) HandleRequest(w http.ResponseWriter, req *http.Req var subscriptionResult *Result // Always return a response defer func() { - subscriptionResult.Tenant = nil // Don't return tenant details in response + subscriptionResult.Subscription = nil // Don't return subscription details in response res, _ := json.Marshal(subscriptionResult) w.Write(res) }() if req.Method != http.MethodPut && req.Method != http.MethodDelete { - subscriptionResult = &Result{Tenant: nil, Message: InvalidRequestMethod} + subscriptionResult = &Result{Subscription: nil, Message: InvalidRequestMethod} w.WriteHeader(http.StatusMethodNotAllowed) return } @@ -1019,14 +836,14 @@ func (s *SubscriptionHandler) HandleRequest(w http.ResponseWriter, req *http.Req reqInfo, err := ProcessRequest(req, subscriptionType) if err != nil || reqInfo == nil { w.WriteHeader(http.StatusBadRequest) - subscriptionResult = &Result{Tenant: nil, Message: err.Error()} + subscriptionResult = &Result{Subscription: nil, Message: err.Error()} return } switch req.Method { case http.MethodPut: subscriptionResult = s.CreateTenant(reqInfo) - if subscriptionResult.Tenant == nil { + if subscriptionResult.Subscription == nil { w.WriteHeader(http.StatusNotAcceptable) } else { w.WriteHeader(http.StatusAccepted) @@ -1135,64 +952,8 @@ func getSubscriptionDomain(payload map[string]any) string { return "" } -func (c *serviceCredentials) xsAppName() string { - if c.XSAppName != "" { - return c.XSAppName - } - if c.UAA != nil && c.UAA.XSAppName != "" { - return c.UAA.XSAppName - } - return "" -} - -func (s *SubscriptionHandler) getServiceDependencies(capApp *v1alpha1.CAPApplication, service v1alpha1.ServiceInfo) map[string]string { - // Read credentials with metadata (as we need a check based on the plan - serviceCredInfo, err := util.ReadServiceCredentialsFromSecret[serviceMetaInfo](&service, capApp.Namespace, s.KubeClientset, true) - if err != nil { - util.LogError(err, "Failed to read secret for service", GetDependencies, capApp, nil, "service", service.Name, "secret", service.Secret) - return nil - } - - if isServiceRelevantForDependencies(service, serviceCredInfo) { - - if name := serviceCredInfo.Credentials.xsAppName(); name != "" { - if isSpecialDependency(service, serviceCredInfo) { - return map[string]string{ - "appName": service.Class, - "appId": name, - } - } - return map[string]string{"xsappname": name} - } - } - - return nil -} - -func isServiceRelevantForDependencies(serviceInfo v1alpha1.ServiceInfo, creds *serviceMetaInfo) bool { - if serviceInfo.GetSubscriptionDependency() == v1alpha1.SubscriptionDependencyAlways { - return true - } - - if serviceInfo.GetSubscriptionDependency() == v1alpha1.SubscriptionDependencyAuto { - return isSpecialDependency(serviceInfo, creds) || - creds.Credentials.SaasRegistryEnabled - } - - return false -} - -// These services might need some special handling for now, until there is some clarity from BTP as to how saas-registry differentiates b/w xsappname and appId/appName dependencies. -func isSpecialDependency(serviceInfo v1alpha1.ServiceInfo, creds *serviceMetaInfo) bool { - return serviceInfo.Class == "destination" || - serviceInfo.Class == "connectivity" || - (serviceInfo.Class == "auditlog" && creds.Plan == "oauth2") -} - func (s *SubscriptionHandler) getDependencies(req *http.Request, subscriptionType subscriptionType) ([]byte, error) { - var dependenciesArray []map[string]string - - // Read the cap application by using the provider subaccount id & app-name passed in the URI + // Read the subscription provider by using the provider subaccount id & app-name passed in the URI // URI format - /dependencies/providersubaccountId/app-name or /sms/dependencies/providersubaccountId/app-name/{app_tid} providersubaccountId := req.PathValue("providerSubaccountId") appName := req.PathValue("appName") @@ -1204,14 +965,9 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request, subscriptionTyp util.LogInfo("Get dependencies request received", GetDependencies, nil, nil, "subscriptionType", subscriptionType, "providerSubaccountId", providersubaccountId, "btpAppName", appName) - ca, err := s.checkCAPApp(providersubaccountId, appName) + subPro, err := s.checkSubscriptionProvider(providersubaccountId, appName) if err != nil { - util.LogError(err, "CAPApplication not found for providerSubaccountId and appName", GetDependencies, nil, nil, "providerSubaccountId", providersubaccountId, "btpAppName", appName) - return nil, err - } - if ca.IsServicesOnly() { - err := errors.New("no multi-tenant capapplication found") - util.LogError(err, "CAPApplication invalid", GetDependencies, nil, nil, "providerSubaccountId", providersubaccountId, "btpAppName", appName) + util.LogError(err, "SubscriptionProvider not found for providerSubaccountId and appName", GetDependencies, nil, nil, "providerSubaccountId", providersubaccountId, "btpAppName", appName) return nil, err } @@ -1223,31 +979,20 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request, subscriptionTyp headerDetails.authorization = req.Header.Get("Authorization") } - if _, _, err = s.authorizationCheck(&headerDetails, ca, subscriptionType, GetDependencies); err != nil { - util.LogError(err, "Authorization check failed for get dependencies request", GetDependencies, ca, nil, "subscriptionType", subscriptionType) + if _, _, err = s.authorizationCheck(&headerDetails, subPro, subscriptionType, GetDependencies); err != nil { + util.LogError(err, "Authorization check failed for get dependencies request", GetDependencies, subPro, nil, "subscriptionType", subscriptionType) return nil, &GetDependenciesAuthError{} } - for _, service := range ca.Spec.BTP.Services { - if serviceDependency := s.getServiceDependencies(ca, service); serviceDependency != nil { - dependenciesArray = append(dependenciesArray, serviceDependency) - } - } - - if len(dependenciesArray) == 0 { - util.LogInfo("No subscription dependencies found", GetDependencies, ca, nil) + // Dependencies are precomputed by the controller and published to the SubscriptionProvider status + if subPro.Status.Dependencies == "" { + util.LogInfo("No subscription dependencies found", GetDependencies, subPro, nil) return nil, nil } - dependencies, err := json.Marshal(dependenciesArray) - if err != nil { - util.LogError(err, "Failed to marshal dependencies to JSON", GetDependencies, ca, nil) - return nil, err - } - - util.LogInfo("Subscription dependencies resolved", GetDependencies, ca, nil, "count", len(dependenciesArray), "dependencies", string(dependencies)) + util.LogInfo("Subscription dependencies resolved", GetDependencies, subPro, nil, "dependencies", subPro.Status.Dependencies) - return dependencies, nil + return []byte(subPro.Status.Dependencies), nil } func (s *SubscriptionHandler) handleGetDependenciesRequest(w http.ResponseWriter, req *http.Request, subscriptionType subscriptionType) { diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go index 1b3940de..d9adf0e9 100644 --- a/cmd/server/internal/handler_test.go +++ b/cmd/server/internal/handler_test.go @@ -40,17 +40,19 @@ type httpTestClientGenerator struct { func (facade *httpTestClientGenerator) NewHTTPClient() *http.Client { return facade.client } const ( - caName = "ca-test-controller" - catName = caName + "-provider" - appName = "some-app-name" - globalAccountId = "cap-app-global" - providerSubaccountId = "012012012-1234-1234-123456012345" - subDomain = "foo" - tenantId = "012012012-1234-1234-123456" - subscriptionGUID = "012301234-2345-6789-ABCDEF" - subscriptionContextSecretName = catName + "-context" + providerName = "ca-test-controller" + subName = providerName + "-subscription" + appName = "some-app-name" + globalAccountId = "cap-app-global" + providerSubaccountId = "012012012-1234-1234-123456012345" + subDomain = "foo" + tenantId = "012012012-1234-1234-123456" + subscriptionGUID = "012301234-2345-6789-ABCDEF" ) +// dependenciesJSON is the precomputed dependency payload published by the controller to SubscriptionProvider.Status.Dependencies +const dependenciesJSON = `[{"xsappname":"saasappname!b15"},{"xsappname":"smappname!b15"},{"appId":"destappname!b15","appName":"destination"},{"xsappname":"rtappname!b15"}]` + func setup(client *http.Client, secrets []runtime.Object, objects ...runtime.Object) *SubscriptionHandler { subHandler := NewSubscriptionHandler(fake.NewSimpleClientset(objects...), k8sfake.NewSimpleClientset(secrets...)) if client != nil { @@ -78,23 +80,6 @@ func createSecrets() []runtime.Object { "credential-type": "instance-secret" }`), }, - }, &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-xsuaa-sec2", - Namespace: v1.NamespaceDefault, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "credentials": []byte(`{ - "uaadomain": "auth2.service.local", - "xsappname": "appname!b21", - "trustedclientidsuffix": "|appname!b21", - "verificationkey": "", - "sburl": "internal.auth2.service.local", - "url": "https://app2-domain.auth2.service.local", - "credential-type": "instance-secret" - }`), - }, }, &corev1.Secret{ ObjectMeta: v1.ObjectMeta{ Name: "test-saas-sec", @@ -115,63 +100,6 @@ func createSecrets() []runtime.Object { "credential-type": "instance-secret" }`), }, - }, &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-dest-sec", - Namespace: v1.NamespaceDefault, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "credentials": []byte(`{ - "saas_registry_url": "https://sm.service.local", - "clientid": "clientid", - "clientsecret": "clientsecret", - "uaadomain": "auth.service.local", - "sburl": "internal.auth.service.local", - "url": "https://app-domain.auth.service.local", - "saasregistryenabled": true, - "uaa": {"xsappname": "destappname!b15" }, - "credential-type": "instance-secret" - }`), - }, - }, &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-html-rt-sec", - Namespace: v1.NamespaceDefault, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "credentials": []byte(`{ - "saas_registry_url": "https://sm.service.local", - "clientid": "clientid", - "clientsecret": "clientsecret", - "uaadomain": "auth.service.local", - "sburl": "internal.auth.service.local", - "url": "https://app-domain.auth.service.local", - "saasregistryenabled": true, - "uaa": {"xsappname": "rtappname!b15" }, - "credential-type": "instance-secret" - }`), - }, - }, &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-sm-sec", - Namespace: v1.NamespaceDefault, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "credentials": []byte(`{ - "saas_registry_url": "https://sm.service.local", - "clientid": "clientid", - "clientsecret": "clientsecret", - "uaadomain": "auth.service.local", - "sburl": "internal.auth.service.local", - "url": "https://app-domain.auth.service.local", - "saasregistryenabled": true, - "xsappname": "smappname!b15", - "credential-type": "instance-secret" - }`), - }, }) return secs @@ -205,160 +133,61 @@ func createSmsSecret() []runtime.Object { return secs } -func createTenantSubscriptionContextSecret(subscriptionContext string) runtime.Object { - return &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: subscriptionContextSecretName, - Namespace: v1.NamespaceDefault, - Labels: map[string]string{ - LabelTenantId: tenantId, - MetadataSubscriptionGUID: subscriptionGUID, - }, - }, - StringData: map[string]string{ - "subscriptionContext": subscriptionContext, - }, +// createSubscriptionProvider builds the SubscriptionProvider fixture that the handler now resolves for auth + dependencies. +func createSubscriptionProvider(subType subscriptionType) *v1alpha1.SubscriptionProvider { + info := v1alpha1.SubscriptionInfo{} + if subType == SMS { + info.Type = "subscription-manager" + info.SubscriptionSecret = "test-sms-sec" + } else { + info.Type = "saas-registry" + info.SubscriptionSecret = "test-saas-sec" + info.AuthSecret = "test-xsuaa-sec" } -} - -func createCA() *v1alpha1.CAPApplication { - return &v1alpha1.CAPApplication{ + return &v1alpha1.SubscriptionProvider{ ObjectMeta: v1.ObjectMeta{ - Name: caName, + Name: providerName, Namespace: v1.NamespaceDefault, Labels: map[string]string{ LabelAppIdHash: sha1Sum(providerSubaccountId, appName), }, }, - Spec: v1alpha1.CAPApplicationSpec{ - ProviderSubaccountId: providerSubaccountId, - BTPAppName: appName, - Provider: &v1alpha1.BTPTenantIdentification{ - SubDomain: subDomain, - TenantId: tenantId, - }, - BTP: v1alpha1.BTP{ - Services: []v1alpha1.ServiceInfo{ - { - Class: "xsuaa", - Name: "test-xsuaa", - Secret: "test-xsuaa-sec", - }, - { - Class: "xsuaa", - Name: "test-xsuaa2", - Secret: "test-xsuaa-sec2", - }, - { - Class: "saas-registry", - Name: "test-saas", - Secret: "test-saas-sec", - }, - { - Class: "service-manager", - Name: "test-sm", - Secret: "test-sm-sec", - }, - { - Class: "destination", - Name: "test-dest", - Secret: "test-dest-sec", - }, - { - Class: "html5-apps-repo", - Name: "test-html-host", - Secret: "test-html-host-sec", - }, - { - Class: "html5-apps-repo", - Name: "test-html-rt", - Secret: "test-html-rt-sec", - }, - { - Class: "subscription-manager", - Name: "test-sms", - Secret: "test-sms-sec", - }, - }, - }, + Spec: v1alpha1.SubscriptionProviderSpec{ + AppName: appName, + ProviderSubaccountID: providerSubaccountId, + SubscriptionInfo: info, + }, + Status: v1alpha1.SubscriptionProviderStatus{ + State: v1alpha1.SubscriptionProviderStateReady, + Dependencies: dependenciesJSON, }, } } -func createCAT(ready bool, withProviderSubaccountId ...bool) *v1alpha1.CAPTenant { - cat := &v1alpha1.CAPTenant{ +// createSubscription builds an existing Subscription fixture correlated to the provider via app-identifier + tenant labels. +func createSubscription(state v1alpha1.SubscriptionState, guid string) *v1alpha1.Subscription { + sub := &v1alpha1.Subscription{ ObjectMeta: v1.ObjectMeta{ - Name: catName, + Name: subName, Namespace: v1.NamespaceDefault, Labels: map[string]string{ - LabelAppIdHash: sha1Sum(providerSubaccountId, appName), - LabelTenantId: tenantId, - }, - Annotations: map[string]string{ - AnnotationSubscriptionContextSecret: subscriptionContextSecretName, - }, - }, - Spec: v1alpha1.CAPTenantSpec{ - CAPApplicationInstance: caName, - BTPTenantIdentification: v1alpha1.BTPTenantIdentification{ - SubDomain: subDomain, - TenantId: tenantId, + LabelAppIdHash: sha1Sum(providerSubaccountId, appName), + LabelTenantId: tenantId, + MetadataSubscriptionGUID: guid, }, }, - } - if withProviderSubaccountId != nil && withProviderSubaccountId[0] { - cat.ObjectMeta.Labels[MetadataSubscriptionGUID] = subscriptionGUID - cat.ObjectMeta.Annotations[MetadataSubscriptionGUID] = subscriptionGUID - } - if ready { - cat.Status = v1alpha1.CAPTenantStatus{ - State: v1alpha1.CAPTenantStateReady, - CurrentCAPApplicationVersionInstance: "cap-version", - GenericStatus: v1alpha1.GenericStatus{ - Conditions: []v1.Condition{{ - Type: string(v1alpha1.ConditionTypeReady), - Status: "True", - Reason: "TenantReady", - }}, - }, - } - } - return cat -} - -func createDomain() *v1alpha1.Domain { - return &v1alpha1.Domain{ - ObjectMeta: v1.ObjectMeta{ - Name: "primary-domain", - Namespace: v1.NamespaceDefault, - }, - Spec: v1alpha1.DomainSpec{ - Domain: "auth.service.local", - IngressSelector: map[string]string{ - "istio": "ingressgateway", - "app": "istio-ingressgateway", - }, - TLSMode: v1alpha1.TlsModeSimple, - DNSTarget: "in.service.local", + Spec: v1alpha1.SubscriptionSpec{ + AppName: appName, + ProviderSubaccountId: providerSubaccountId, + TenantId: tenantId, + Subdomain: subDomain, + SubscriptionGuid: guid, }, } -} - -func createClusterDomain() *v1alpha1.ClusterDomain { - return &v1alpha1.ClusterDomain{ - ObjectMeta: v1.ObjectMeta{ - Name: "external-domain", - }, - Spec: v1alpha1.DomainSpec{ - Domain: "external.service.sap", - IngressSelector: map[string]string{ - "istio": "ingressgateway", - "app": "istio-ingressgateway", - }, - TLSMode: v1alpha1.TlsModeSimple, - DNSTarget: "in.service.sap", - }, + if state != "" { + sub.Status.State = state } + return sub } func TestMain(m *testing.M) { @@ -382,7 +211,7 @@ func Test_IncorrectMethod(t *testing.T) { t.Error("Unexpected error in expected response: ", res.Body) } - if resType.Tenant != nil && resType.Message != InvalidRequestMethod { + if resType.Subscription != nil && resType.Message != InvalidRequestMethod { t.Error("Response: ", res.Body, " does not match expected result: ", InvalidRequestMethod) } @@ -396,15 +225,10 @@ func Test_provisioning(t *testing.T) { createCROs bool withAdditionalData bool invalidAdditionalData bool - withSecretKey bool - existingTenant bool + existingSubscription bool existingTenantOutput bool expectedStatusCode int expectedResponse Result - existingDomain bool - existingClusterDomain bool - invalidDomain bool - invalidClusterDomain bool }{ { name: "Invalid Provisioning Request", @@ -412,7 +236,7 @@ func Test_provisioning(t *testing.T) { body: "", expectedStatusCode: http.StatusBadRequest, expectedResponse: Result{ - Message: "EOF", //TODO + Message: "EOF", }, }, { @@ -421,7 +245,7 @@ func Test_provisioning(t *testing.T) { body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, expectedStatusCode: http.StatusNotAcceptable, expectedResponse: Result{ - Message: "the server could not find the requested resource (get capapplications.sme.sap.com)", //TODO + Message: ResourceNotFound, }, }, { @@ -431,11 +255,11 @@ func Test_provisioning(t *testing.T) { createCROs: true, expectedStatusCode: http.StatusNotAcceptable, expectedResponse: Result{ - Message: "", //TODO + Message: ResourceNotFound, }, }, { - name: "Provisioning Request valid (without domains)", + name: "Provisioning Request valid", method: http.MethodPut, body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, createCROs: true, @@ -445,173 +269,64 @@ func Test_provisioning(t *testing.T) { }, }, { - name: "Provisioning Request valid (invalid domain)", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - createCROs: true, - invalidDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid (invalid clusterdomains)", + name: "Provisioning Request valid with additional data and existing subscription", method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `","subscriptionParams":""}`, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, createCROs: true, - invalidClusterDomain: true, + withAdditionalData: true, + existingSubscription: true, expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid (with domain)", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - createCROs: true, - existingDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid (with Cluster domain)", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - createCROs: true, - existingClusterDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid with additional data and existing tenant", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - createCROs: true, - withAdditionalData: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, + Message: ResourceUpdated, }, }, { - name: "Provisioning Request valid with additional data and existing tenant and existing tenant output", + name: "Provisioning Request valid with additional data and existing subscription and existing tenant output", method: http.MethodPut, body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, createCROs: true, withAdditionalData: true, - existingTenant: true, + existingSubscription: true, existingTenantOutput: true, expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ - Message: ResourceCreated, + Message: ResourceUpdated, }, }, { - name: "Provisioning Request valid with invalid additional data and existing tenant", + name: "Provisioning Request valid with invalid additional data and existing subscription", method: http.MethodPut, body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, createCROs: true, withAdditionalData: true, invalidAdditionalData: true, - existingTenant: true, + existingSubscription: true, expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request with existing tenant", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - createCROs: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceFound, - }, - }, - { - name: "Provisioning with subscriptionDomain in payload matching Domain", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `","subscriptionParams":{"subscriptionDomain":"auth.service.local"}}`, - createCROs: true, - existingDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning with subscriptionDomain in payload not matching any domain", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `","subscriptionParams":{"subscriptionDomain":"unknown.domain.com"}}`, - createCROs: true, - existingDomain: true, - expectedStatusCode: http.StatusNotAcceptable, - expectedResponse: Result{ - Message: "Error constructing subscription URL: domain unknown.domain.com not found in Domains or ClusterDomains", - }, - }, - { - name: "Provisioning with empty subscriptionParams (no subscriptionDomain)", - method: http.MethodPut, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `","subscriptionParams":{}}`, - createCROs: true, - existingDomain: true, - - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, + Message: ResourceUpdated, }, }, } for _, testData := range tests { t.Run(testData.name, func(t *testing.T) { - var ca *v1alpha1.CAPApplication - var cat *v1alpha1.CAPTenant - var ctout *v1alpha1.CAPTenantOutput runtimeObjs := []runtime.Object{} - if testData.existingDomain { - runtimeObjs = append(runtimeObjs, createDomain()) - } else if testData.existingClusterDomain { - runtimeObjs = append(runtimeObjs, createClusterDomain()) - } if testData.createCROs { - ca = createCA() + subPro := createSubscriptionProvider(SaaS) if testData.withAdditionalData { if !testData.invalidAdditionalData { - ca.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{\"foo\":\"bar\"}"} + subPro.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{\"foo\":\"bar\"}"} } else { - ca.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{foo\":\"bar\"}"} //invalid json + subPro.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{foo\":\"bar\"}"} //invalid json } } - // Update the CA with the correct domainRefs if needed - if testData.existingDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "Domain", Name: "primary-domain"}} - } else if testData.existingClusterDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "ClusterDomain", Name: "external-domain"}} - } else if testData.invalidDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "Domain", Name: "foo"}} - } else if testData.invalidClusterDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "ClusterDomain", Name: "foo"}} - } - runtimeObjs = append(runtimeObjs, ca) + runtimeObjs = append(runtimeObjs, subPro) } - if testData.existingTenant { - cat = createCAT(testData.withAdditionalData, true) - runtimeObjs = append(runtimeObjs, cat) + if testData.existingSubscription { + runtimeObjs = append(runtimeObjs, createSubscription("", subscriptionGUID)) } if testData.existingTenantOutput { - ctout = &v1alpha1.CAPTenantOutput{ObjectMeta: v1.ObjectMeta{Name: catName, Namespace: v1.NamespaceDefault, Labels: map[string]string{LabelTenantId: tenantId}}, Spec: v1alpha1.CAPTenantOutputSpec{SubscriptionCallbackData: "{\"foo3\":\"bar3\"}"}} - runtimeObjs = append(runtimeObjs, ctout) + runtimeObjs = append(runtimeObjs, &v1alpha1.CAPTenantOutput{ObjectMeta: v1.ObjectMeta{Name: subName, Namespace: v1.NamespaceDefault, Labels: map[string]string{LabelTenantId: tenantId}}, Spec: v1alpha1.CAPTenantOutputSpec{SubscriptionCallbackData: "{\"foo3\":\"bar3\"}"}}) } client, tokenString, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") @@ -619,11 +334,7 @@ func Test_provisioning(t *testing.T) { t.Fatal(err.Error()) } - secrets := createSecrets() - if testData.existingTenant { - secrets = append(secrets, createTenantSubscriptionContextSecret(testData.body)) - } - subHandler := setup(client, secrets, runtimeObjs...) + subHandler := setup(client, createSecrets(), runtimeObjs...) res := httptest.NewRecorder() req := httptest.NewRequest(testData.method, RequestPath, strings.NewReader(testData.body)) @@ -641,7 +352,7 @@ func Test_provisioning(t *testing.T) { t.Error("Unexpected error in expected response: ", res.Body) } - if resType.Tenant != testData.expectedResponse.Tenant && resType.Message != testData.expectedResponse.Message { + if resType.Subscription != testData.expectedResponse.Subscription && resType.Message != testData.expectedResponse.Message { t.Error("Response: ", res.Body, " does not match expected result: ", testData.expectedResponse) } }) @@ -656,15 +367,10 @@ func Test_sms_provisioning(t *testing.T) { createCROs bool withAdditionalData bool invalidAdditionalData bool - withSecretKey bool - existingTenant bool + existingSubscription bool existingTenantOutput bool expectedStatusCode int expectedResponse Result - existingDomain bool - existingClusterDomain bool - invalidDomain bool - invalidClusterDomain bool }{ { name: "Invalid Provisioning Request", @@ -672,7 +378,7 @@ func Test_sms_provisioning(t *testing.T) { body: "", expectedStatusCode: http.StatusBadRequest, expectedResponse: Result{ - Message: "EOF", //TODO + Message: "EOF", }, }, { @@ -681,7 +387,7 @@ func Test_sms_provisioning(t *testing.T) { body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, expectedStatusCode: http.StatusNotAcceptable, expectedResponse: Result{ - Message: "the server could not find the requested resource (get capapplications.sme.sap.com)", //TODO + Message: ResourceNotFound, }, }, { @@ -691,141 +397,42 @@ func Test_sms_provisioning(t *testing.T) { createCROs: true, expectedStatusCode: http.StatusNotAcceptable, expectedResponse: Result{ - Message: "", //TODO - }, - }, - { - name: "Provisioning Request valid (without domains)", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, + Message: ResourceNotFound, }, }, { - name: "Provisioning Request valid (invalid domain)", + name: "Provisioning Request valid", method: http.MethodPut, body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, createCROs: true, - invalidDomain: true, expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ Message: ResourceCreated, }, }, { - name: "Provisioning Request valid (invalid clusterdomains)", + name: "Provisioning Request valid with additional data and existing subscription", method: http.MethodPut, body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, createCROs: true, - invalidClusterDomain: true, + withAdditionalData: true, + existingSubscription: true, expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid (with domain)", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - existingDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid (with Cluster domain)", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - existingClusterDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid with additional data and existing tenant", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - withAdditionalData: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, + Message: ResourceUpdated, }, }, { - name: "Provisioning Request valid with additional data and existing tenant and existing tenant output", + name: "Provisioning Request with existing subscription but different subscriptionGUID", method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, + body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + "update" + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, createCROs: true, - withAdditionalData: true, - existingTenant: true, - existingTenantOutput: true, + existingSubscription: true, expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request valid with invalid additional data and existing tenant", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - withAdditionalData: true, - invalidAdditionalData: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceCreated, - }, - }, - { - name: "Provisioning Request with existing tenant", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceFound, - }, - }, - { - name: "Provisioning Request with existing tenant but different subscriptionGUID (If provisioning fails due to callback issue, the tenant exists and in BTP provisioned failed; retriggering sends a new subscriptionGUID in the payload)", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `"},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + "update" + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ Message: ResourceUpdated, }, }, - { - name: "SMS provisioning with subscriptionDomain in payload matching Domain", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `","subscriptionParams":{"subscriptionDomain":"auth.service.local"}},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - existingDomain: true, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{Message: ResourceCreated}, - }, - { - name: "SMS provisioning with subscriptionDomain in payload not matching any domain", - method: http.MethodPut, - body: `{"rootApplication":{"appName":"` + appName + `","providerSubaccountId":"` + providerSubaccountId + `","commercialAppName":"` + appName + `","subscriptionParams":{"subscriptionDomain":"unknown.domain.com"}},"subscriber":{"subscriptionGUID":"` + subscriptionGUID + `","app_tid":"` + tenantId + `","globalAccountId":"` + globalAccountId + `","subaccountSubdomain":"` + subDomain + `"}}`, - createCROs: true, - existingDomain: true, - expectedStatusCode: http.StatusNotAcceptable, - expectedResponse: Result{Message: "Error constructing subscription URL: domain unknown.domain.com not found in Domains or ClusterDomains"}, - }, } // Create and encode the client certificate once before all tests are executed @@ -835,55 +442,27 @@ func Test_sms_provisioning(t *testing.T) { for _, testData := range tests { t.Run(testData.name, func(t *testing.T) { - var ca *v1alpha1.CAPApplication - var cat *v1alpha1.CAPTenant - var ctout *v1alpha1.CAPTenantOutput runtimeObjs := []runtime.Object{} - if testData.existingDomain { - runtimeObjs = append(runtimeObjs, createDomain()) - } else if testData.existingClusterDomain { - runtimeObjs = append(runtimeObjs, createClusterDomain()) - } if testData.createCROs { - ca = createCA() + subPro := createSubscriptionProvider(SMS) if testData.withAdditionalData { if !testData.invalidAdditionalData { - ca.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{\"foo\":\"bar\"}"} + subPro.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{\"foo\":\"bar\"}"} } else { - ca.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{foo\":\"bar\"}"} //invalid json + subPro.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{foo\":\"bar\"}"} //invalid json } } - // Update the CA with the correct domainRefs if needed - if testData.existingDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "Domain", Name: "primary-domain"}} - } else if testData.existingClusterDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "ClusterDomain", Name: "external-domain"}} - } else if testData.invalidDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "Domain", Name: "foo"}} - } else if testData.invalidClusterDomain { - ca.Spec.DomainRefs = []v1alpha1.DomainRef{{Kind: "ClusterDomain", Name: "foo"}} - } - runtimeObjs = append(runtimeObjs, ca) + runtimeObjs = append(runtimeObjs, subPro) } - if testData.existingTenant { - cat = createCAT(testData.withAdditionalData, true) - runtimeObjs = append(runtimeObjs, cat) + if testData.existingSubscription { + runtimeObjs = append(runtimeObjs, createSubscription("", subscriptionGUID)) } if testData.existingTenantOutput { - ctout = &v1alpha1.CAPTenantOutput{ObjectMeta: v1.ObjectMeta{Name: catName, Namespace: v1.NamespaceDefault, Labels: map[string]string{LabelTenantId: tenantId}}, Spec: v1alpha1.CAPTenantOutputSpec{SubscriptionCallbackData: "{\"foo3\":\"bar3\"}"}} - runtimeObjs = append(runtimeObjs, ctout) - } - - client, _, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") - if err != nil { - t.Fatal(err.Error()) + runtimeObjs = append(runtimeObjs, &v1alpha1.CAPTenantOutput{ObjectMeta: v1.ObjectMeta{Name: subName, Namespace: v1.NamespaceDefault, Labels: map[string]string{LabelTenantId: tenantId}}, Spec: v1alpha1.CAPTenantOutputSpec{SubscriptionCallbackData: "{\"foo3\":\"bar3\"}"}}) } secrets := createSmsSecret() - if testData.existingTenant { - secrets = append(secrets, createTenantSubscriptionContextSecret(testData.body)) - } - subHandler := setup(client, secrets, runtimeObjs...) + subHandler := setup(nil, secrets, runtimeObjs...) res := httptest.NewRecorder() req := httptest.NewRequest(testData.method, SmsRequestPath, strings.NewReader(testData.body)) @@ -898,12 +477,12 @@ func Test_sms_provisioning(t *testing.T) { // Get the relevant response decoder := json.NewDecoder(res.Body) var resType Result - err = decoder.Decode(&resType) + err := decoder.Decode(&resType) if err != nil { t.Error("Unexpected error in expected response: ", res.Body) } - if resType.Tenant != testData.expectedResponse.Tenant && resType.Message != testData.expectedResponse.Message { + if resType.Subscription != testData.expectedResponse.Subscription && resType.Message != testData.expectedResponse.Message { t.Error("Response: ", res.Body, " does not match expected result: ", testData.expectedResponse) } }) @@ -912,15 +491,13 @@ func Test_sms_provisioning(t *testing.T) { func Test_deprovisioning(t *testing.T) { tests := []struct { - name string - method string - createCROs bool - existingTenant bool - body string - expectedStatusCode int - expectedResponse Result - withSecretKey bool - withProviderSubaccountId bool + name string + method string + createCROs bool + existingSubscription bool + body string + expectedStatusCode int + expectedResponse Result }{ { name: "Invalid Deprovisioning Request", @@ -928,47 +505,35 @@ func Test_deprovisioning(t *testing.T) { body: "", expectedStatusCode: http.StatusBadRequest, expectedResponse: Result{ - Message: "EOF", //TODO + Message: "EOF", }, }, { - name: "Deprovisioning Request without CAPApplication and CAPTenant", + name: "Deprovisioning Request without SubscriptionProvider and Subscription", method: http.MethodDelete, body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, expectedStatusCode: http.StatusNotFound, expectedResponse: Result{ - Message: "the server could not find the requested resource (get capapplications.sme.sap.com)", //TODO + Message: TenantNotFound, }, }, { - name: "Deprovisioning Request valid without existing tenant", + name: "Deprovisioning Request valid without existing subscription", method: http.MethodDelete, createCROs: true, body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, expectedStatusCode: http.StatusNotFound, expectedResponse: Result{ - Message: ResourceDeleted, - }, - }, - { - name: "Deprovisioning Request valid existing tenant", - method: http.MethodDelete, - createCROs: true, - existingTenant: true, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - expectedStatusCode: http.StatusAccepted, - expectedResponse: Result{ - Message: ResourceDeleted, + Message: TenantNotFound, }, }, { - name: "Deprovisioning Request valid existing tenant having provider subaccount id", - method: http.MethodDelete, - createCROs: true, - existingTenant: true, - withProviderSubaccountId: true, - body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, - expectedStatusCode: http.StatusAccepted, + name: "Deprovisioning Request valid existing subscription", + method: http.MethodDelete, + createCROs: true, + existingSubscription: true, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","providerSubaccountId":"` + providerSubaccountId + `","subscriptionGUID":"` + subscriptionGUID + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`, + expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ Message: ResourceDeleted, }, @@ -977,16 +542,12 @@ func Test_deprovisioning(t *testing.T) { for _, testData := range tests { t.Run(testData.name, func(t *testing.T) { - var ca *v1alpha1.CAPApplication - var cat *v1alpha1.CAPTenant runtimeObjs := []runtime.Object{} if testData.createCROs { - ca = createCA() - runtimeObjs = append(runtimeObjs, ca) + runtimeObjs = append(runtimeObjs, createSubscriptionProvider(SaaS)) } - if testData.existingTenant { - cat = createCAT(false, testData.withProviderSubaccountId) - runtimeObjs = append(runtimeObjs, cat) + if testData.existingSubscription { + runtimeObjs = append(runtimeObjs, createSubscription("", subscriptionGUID)) } // set custom client for testing @@ -995,11 +556,7 @@ func Test_deprovisioning(t *testing.T) { t.Fatal(err.Error()) } - secrets := createSecrets() - if testData.existingTenant { - secrets = append(secrets, createTenantSubscriptionContextSecret(testData.body)) - } - subHandler := setup(client, secrets, runtimeObjs...) + subHandler := setup(client, createSecrets(), runtimeObjs...) res := httptest.NewRecorder() req := httptest.NewRequest(testData.method, RequestPath, strings.NewReader(testData.body)) @@ -1017,7 +574,7 @@ func Test_deprovisioning(t *testing.T) { t.Error("Unexpected error in expected response: ", res.Body) } - if resType.Tenant != testData.expectedResponse.Tenant && resType.Message != testData.expectedResponse.Message { + if resType.Subscription != testData.expectedResponse.Subscription && resType.Message != testData.expectedResponse.Message { t.Error("Response: ", res.Body, " does not match expected result: ", testData.expectedResponse) } }) @@ -1026,14 +583,13 @@ func Test_deprovisioning(t *testing.T) { func Test_sms_deprovisioning(t *testing.T) { tests := []struct { - name string - method string - invalidReqUrl bool - createCROs bool - existingTenant bool - expectedStatusCode int - expectedResponse Result - withSecretKey bool + name string + method string + invalidReqUrl bool + createCROs bool + existingSubscription bool + expectedStatusCode int + expectedResponse Result }{ { name: "Invalid Deprovisioning Request", @@ -1041,32 +597,32 @@ func Test_sms_deprovisioning(t *testing.T) { invalidReqUrl: true, expectedStatusCode: http.StatusBadRequest, expectedResponse: Result{ - Message: "EOF", //TODO + Message: "EOF", }, }, { - name: "Deprovisioning Request without CAPApplication and CAPTenant", + name: "Deprovisioning Request without SubscriptionProvider and Subscription", method: http.MethodDelete, expectedStatusCode: http.StatusNotFound, expectedResponse: Result{ - Message: "the server could not find the requested resource (get capapplications.sme.sap.com)", //TODO + Message: TenantNotFound, }, }, { - name: "Deprovisioning Request valid without existing tenant", + name: "Deprovisioning Request valid without existing subscription", method: http.MethodDelete, createCROs: true, expectedStatusCode: http.StatusNotFound, expectedResponse: Result{ - Message: ResourceDeleted, + Message: TenantNotFound, }, }, { - name: "Deprovisioning Request valid existing tenant", - method: http.MethodDelete, - createCROs: true, - existingTenant: true, - expectedStatusCode: http.StatusAccepted, + name: "Deprovisioning Request valid existing subscription", + method: http.MethodDelete, + createCROs: true, + existingSubscription: true, + expectedStatusCode: http.StatusAccepted, expectedResponse: Result{ Message: ResourceDeleted, }, @@ -1075,30 +631,15 @@ func Test_sms_deprovisioning(t *testing.T) { for _, testData := range tests { t.Run(testData.name, func(t *testing.T) { - var ca *v1alpha1.CAPApplication - var cat *v1alpha1.CAPTenant runtimeObjs := []runtime.Object{} if testData.createCROs { - ca = createCA() - runtimeObjs = append(runtimeObjs, ca) + runtimeObjs = append(runtimeObjs, createSubscriptionProvider(SMS)) } - if testData.existingTenant { - cat = createCAT(false, true) - runtimeObjs = append(runtimeObjs, cat) + if testData.existingSubscription { + runtimeObjs = append(runtimeObjs, createSubscription("", subscriptionGUID)) } - // set custom client for testing - client, _, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") - if err != nil { - t.Fatal(err.Error()) - } - - secrets := createSmsSecret() - if testData.existingTenant { - secrets = append(secrets, createTenantSubscriptionContextSecret(`{"rootApplication":{"appName":"`+appName+`","commercialAppName":"`+appName+`"},"subscriber":{"subscriptionGUID":"`+subscriptionGUID+"update"+`","app_tid":"`+tenantId+`","globalAccountId":"`+globalAccountId+`","subaccountSubdomain":"`+subDomain+`"}}`)) - } - - subHandler := setup(client, secrets, runtimeObjs...) + subHandler := setup(nil, createSmsSecret(), runtimeObjs...) res := httptest.NewRecorder() @@ -1109,7 +650,7 @@ func Test_sms_deprovisioning(t *testing.T) { req := httptest.NewRequest(testData.method, requestTarget, nil) - certBytes, err := os.ReadFile("testdata/rootCA.pem") + certBytes, _ := os.ReadFile("testdata/rootCA.pem") certStr := strings.TrimSpace(string(certBytes)) encodedCert := url.QueryEscape(certStr) @@ -1122,12 +663,12 @@ func Test_sms_deprovisioning(t *testing.T) { // Get the relevant response decoder := json.NewDecoder(res.Body) var resType Result - err = decoder.Decode(&resType) + err := decoder.Decode(&resType) if err != nil { t.Error("Unexpected error in expected response: ", res.Body) } - if resType.Tenant != testData.expectedResponse.Tenant && resType.Message != testData.expectedResponse.Message { + if resType.Subscription != testData.expectedResponse.Subscription && resType.Message != testData.expectedResponse.Message { t.Error("Response: ", res.Body, " does not match expected result: ", testData.expectedResponse) } }) @@ -1366,242 +907,60 @@ func TestAsyncCallback(t *testing.T) { } } -func TestCheckTenantStatusContextCancellationAsyncTimeout(t *testing.T) { - execTestsWithBLI(t, "Check Tenant Status Context Cancellation AsyncTimeout", []string{"ERP4SMEPREPWORKAPPPLAT-2240"}, func(t *testing.T) { +func TestCheckSubscriptionStatusContextCancellationAsyncTimeout(t *testing.T) { + execTestsWithBLI(t, "Check Subscription Status Context Cancellation AsyncTimeout", []string{"ERP4SMEPREPWORKAPPPLAT-2240"}, func(t *testing.T) { // test context cancellation (like deadline) subHandler := setup(nil, createSecrets()) - notify := make(chan bool) + type result struct { + ready bool + url string + } + notify := make(chan result) go func() { - r := subHandler.checkCAPTenantStatus(context.Background(), "default", "test-cat", true, "4000") - notify <- r + ready, url := subHandler.checkSubscriptionStatus(context.Background(), "default", "test-sub", true, "4000") + notify <- result{ready, url} }() - timeout := time.After(6 * time.Second) // this is greater than the sleep duration of the tenant check routine + timeout := time.After(8 * time.Second) // this is greater than the sleep duration of the subscription check routine select { case r := <-notify: - if r != false { - t.Error("expected tenant check to return false") + if r.ready != false { + t.Error("expected subscription check to return false") } case <-timeout: - t.Fatal("failed to cancel tenant check routine") + t.Fatal("failed to cancel subscription check routine") } }) } -func TestCheckTenantStatusTenantReady(t *testing.T) { - // test context cancellation (like deadline) - cat := createCAT(true) - subHandler := setup(nil, createSecrets(), cat) - r := subHandler.checkCAPTenantStatus(context.TODO(), cat.Namespace, cat.Name, true, "") +func TestCheckSubscriptionStatusReady(t *testing.T) { + sub := createSubscription(v1alpha1.SubscriptionStateReady, subscriptionGUID) + sub.Status.Url = "https://" + subDomain + ".auth.service.local" + subHandler := setup(nil, createSecrets(), sub) + ready, u := subHandler.checkSubscriptionStatus(context.TODO(), sub.Namespace, sub.Name, true, "") - if r != true { - t.Error("expected tenant check to return false") + if !ready { + t.Error("expected subscription check to return true") + } + if u != sub.Status.Url { + t.Errorf("expected subscription url %q, got %q", sub.Status.Url, u) } } -func TestCheckTenantStatusWithCallbacktimeout(t *testing.T) { - execTestsWithBLI(t, "Check Tenant Status With Callback timeout", []string{"ERP4SMEPREPWORKAPPPLAT-2240"}, func(t *testing.T) { - // test context cancellation (like deadline) - cat := createCAT(false) - subHandler := setup(nil, createSecrets(), cat) - r := subHandler.checkCAPTenantStatus(context.TODO(), cat.Namespace, cat.Name, true, "4000") +func TestCheckSubscriptionStatusWithCallbacktimeout(t *testing.T) { + execTestsWithBLI(t, "Check Subscription Status With Callback timeout", []string{"ERP4SMEPREPWORKAPPPLAT-2240"}, func(t *testing.T) { + // subscription not ready --> should time out + sub := createSubscription("", subscriptionGUID) + subHandler := setup(nil, createSecrets(), sub) + ready, _ := subHandler.checkSubscriptionStatus(context.TODO(), sub.Namespace, sub.Name, true, "4000") - if r != false { - t.Error("expected tenant check to return false, due to timeout (async callback timeout exceeded)") + if ready != false { + t.Error("expected subscription check to return false, due to timeout (async callback timeout exceeded)") } }) } -func TestMultiXSUAA(t *testing.T) { - execTestsWithBLI(t, "Check Multiple xsuaa services used in a CA", []string{"ERP4SMEPREPWORKAPPPLAT-3773"}, func(t *testing.T) { - // CA without "sme.sap.com/primary-xsuaa" annotation - ca := createCA() - - subHandler := setup(nil, createSecrets(), ca) - uaaCreds := subHandler.getXSUAADetails(ca, "Test") - - if uaaCreds.AuthUrl != "https://app-domain.auth.service.local" { - t.Error("incorrect uaa returned") - } - - // CA with "sme.sap.com/primary-xsuaa" annotation - ca2 := createCA() - ca2.Annotations = map[string]string{ - util.AnnotationPrimaryXSUAA: "test-xsuaa2", - } - - uaaCreds = subHandler.getXSUAADetails(ca2, "Test") - - if uaaCreds.AuthUrl != "https://app2-domain.auth2.service.local" { - t.Error("incorrect uaa via annotations returned") - } - }) -} - -func TestAppURL(t *testing.T) { - tests := []struct { - name string - payloadSubscriptionDomain string - tenantSubdomain string - caAnnotations map[string]string - domainRefs []v1alpha1.DomainRef - createDomain bool - createClusterDomain bool - expectedURL string - expectError bool - }{ - { - name: "subscription domain from payload with matching Domain", - payloadSubscriptionDomain: "auth.service.local", - tenantSubdomain: subDomain, - createDomain: true, - expectedURL: "https://" + subDomain + ".auth.service.local", - }, - { - name: "subscription domain from payload with matching ClusterDomain", - payloadSubscriptionDomain: "external.service.sap", - tenantSubdomain: subDomain, - createClusterDomain: true, - expectedURL: "https://" + subDomain + ".external.service.sap", - }, - { - name: "subscription domain from payload not found in any domain resource", - payloadSubscriptionDomain: "unknown.domain.com", - tenantSubdomain: subDomain, - expectError: true, - }, - { - name: "fallback to annotation subscription domain with matching Domain", - payloadSubscriptionDomain: "", - tenantSubdomain: subDomain, - caAnnotations: map[string]string{AnnotationSubscriptionDomain: "auth.service.local"}, - createDomain: true, - expectedURL: "https://" + subDomain + ".auth.service.local", - }, - { - name: "fallback to annotation subscription domain not found in any domain", - payloadSubscriptionDomain: "", - tenantSubdomain: subDomain, - caAnnotations: map[string]string{AnnotationSubscriptionDomain: "unknown.domain.com"}, - expectError: true, - }, - { - name: "fallback to primary domain calculation (Domain ref)", - payloadSubscriptionDomain: "", - tenantSubdomain: subDomain, - domainRefs: []v1alpha1.DomainRef{{Kind: "Domain", Name: "primary-domain"}}, - createDomain: true, - expectedURL: "https://" + subDomain + ".auth.service.local", - }, - { - name: "fallback to primary domain calculation (ClusterDomain ref)", - payloadSubscriptionDomain: "", - tenantSubdomain: subDomain, - domainRefs: []v1alpha1.DomainRef{{Kind: "ClusterDomain", Name: "external-domain"}}, - createClusterDomain: true, - expectedURL: "https://" + subDomain + ".external.service.sap", - }, - { - name: "fallback to primary domain with no domain refs", - payloadSubscriptionDomain: "", - tenantSubdomain: subDomain, - expectedURL: "https://" + subDomain + ".", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ca := createCA() - if tt.caAnnotations != nil { - ca.Annotations = tt.caAnnotations - } - if tt.domainRefs != nil { - ca.Spec.DomainRefs = tt.domainRefs - } - - runtimeObjs := []runtime.Object{ca} - if tt.createDomain { - runtimeObjs = append(runtimeObjs, createDomain()) - } - if tt.createClusterDomain { - runtimeObjs = append(runtimeObjs, createClusterDomain()) - } - - subHandler := setup(nil, createSecrets(), runtimeObjs...) - appURL, err := subHandler.getAppURL(tt.payloadSubscriptionDomain, tt.tenantSubdomain, ca) - - if tt.expectError { - if err == nil { - t.Fatalf("expected error, got nil") - } - return - } - if err != nil { - t.Fatalf("unexpected error: %s", err.Error()) - } - if appURL != tt.expectedURL { - t.Errorf("getAppURL() = %q, want %q", appURL, tt.expectedURL) - } - }) - } -} - -func TestValidateDomain(t *testing.T) { - tests := []struct { - name string - domain string - createDomain bool - createClusterDomain bool - expectError bool - }{ - { - name: "domain found in namespace Domains", - domain: "auth.service.local", - createDomain: true, - }, - { - name: "domain found in ClusterDomains", - domain: "external.service.sap", - createClusterDomain: true, - }, - { - name: "domain not found anywhere", - domain: "nonexistent.domain.com", - expectError: true, - }, - { - name: "domain not matching but resources exist", - domain: "other.domain.com", - createDomain: true, - createClusterDomain: true, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - runtimeObjs := []runtime.Object{} - if tt.createDomain { - runtimeObjs = append(runtimeObjs, createDomain()) - } - if tt.createClusterDomain { - runtimeObjs = append(runtimeObjs, createClusterDomain()) - } - - subHandler := setup(nil, createSecrets(), runtimeObjs...) - err := subHandler.validateDomain(tt.domain, v1.NamespaceDefault) - - if tt.expectError && err == nil { - t.Error("expected error, got nil") - } - if !tt.expectError && err != nil { - t.Errorf("unexpected error: %s", err.Error()) - } - }) - } -} - func execTestsWithBLI(t *testing.T, name string, backlogItems []string, test func(t *testing.T)) { t.Run(name+", BLIs: "+strings.Join(backlogItems, ", "), test) } @@ -1612,104 +971,51 @@ func TestGetDependencies(t *testing.T) { method string invalidToken bool invalidURI bool + noDependencies bool expectedStatusCode int - expectedResponse []map[string]string - caModifier func(*v1alpha1.CAPApplication) + expectedResponse string }{ { name: "Invalid get dependency request - wrong method", method: http.MethodPut, expectedStatusCode: http.StatusMethodNotAllowed, - expectedResponse: nil, }, { name: "Not authorized request", method: http.MethodGet, invalidToken: true, expectedStatusCode: http.StatusUnauthorized, - expectedResponse: nil, }, { name: "Invalid URI", method: http.MethodGet, invalidURI: true, expectedStatusCode: http.StatusBadRequest, - expectedResponse: nil, }, { name: "Valid get dependency request", method: http.MethodGet, expectedStatusCode: http.StatusOK, - expectedResponse: []map[string]string{ - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"appId": "destappname!b15", "appName": "destination"}, - {"xsappname": "rtappname!b15"}, - }, - }, - { - name: "SubscriptionDependency Always - service included regardless of class", - method: http.MethodGet, - expectedStatusCode: http.StatusOK, - caModifier: func(ca *v1alpha1.CAPApplication) { - dep := v1alpha1.SubscriptionDependencyAlways - ca.Spec.BTP.Services[0].SubscriptionDependency = &dep // xsuaa: not auto-qualified, but Always forces inclusion - }, - expectedResponse: []map[string]string{ - {"xsappname": "appname!b14"}, - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"appId": "destappname!b15", "appName": "destination"}, - {"xsappname": "rtappname!b15"}, - }, - }, - { - name: "SubscriptionDependency Auto - non-qualifying service excluded", - method: http.MethodGet, - expectedStatusCode: http.StatusOK, - caModifier: func(ca *v1alpha1.CAPApplication) { - dep := v1alpha1.SubscriptionDependencyAuto - ca.Spec.BTP.Services[0].SubscriptionDependency = &dep // xsuaa: explicit Auto, still not qualified by class/credentials - }, - expectedResponse: []map[string]string{ - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"appId": "destappname!b15", "appName": "destination"}, - {"xsappname": "rtappname!b15"}, - }, - }, - { - name: "SubscriptionDependency Never - service excluded regardless of credentials", - method: http.MethodGet, - expectedStatusCode: http.StatusOK, - caModifier: func(ca *v1alpha1.CAPApplication) { - dep := v1alpha1.SubscriptionDependencyNever - ca.Spec.BTP.Services[4].SubscriptionDependency = &dep // destination: auto-qualified by class, but Never prevents inclusion - }, - expectedResponse: []map[string]string{ - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"xsappname": "rtappname!b15"}, - }, + expectedResponse: dependenciesJSON, }, } for _, testData := range tests { t.Run(testData.name, func(t *testing.T) { - ca := createCA() - if testData.caModifier != nil { - testData.caModifier(ca) + subPro := createSubscriptionProvider(SaaS) + if testData.noDependencies { + subPro.Status.Dependencies = "" } client, tokenString, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") if err != nil { t.Fatal(err.Error()) } - subHandler := setup(client, createSecrets(), ca) + subHandler := setup(client, createSecrets(), subPro) res := httptest.NewRecorder() var req *http.Request - if testData.invalidURI == true { + if testData.invalidURI { req = httptest.NewRequest(testData.method, "/callback/dependencies/providerSubaccountId/{appName}", nil) req.SetPathValue("appName", appName) } else { @@ -1718,7 +1024,7 @@ func TestGetDependencies(t *testing.T) { req.SetPathValue("appName", appName) } - if testData.invalidToken == true { + if testData.invalidToken { tokenString = "abc" //invalid token } @@ -1732,9 +1038,8 @@ func TestGetDependencies(t *testing.T) { // Get the relevant response if res.Code == http.StatusOK { resBodyStr := res.Body.String() - expectedResponseByte, _ := json.Marshal(testData.expectedResponse) - if resBodyStr != string(expectedResponseByte) { - t.Error("Unexpected error in expected response: ", res.Body) + if resBodyStr != testData.expectedResponse { + t.Error("Unexpected response: ", res.Body, " expected: ", testData.expectedResponse) } } }) @@ -1748,84 +1053,30 @@ func TestGetSMSDependencies(t *testing.T) { invalidCert bool invalidURI bool expectedStatusCode int - expectedResponse []map[string]string - caModifier func(*v1alpha1.CAPApplication) + expectedResponse string }{ { name: "Invalid get SMS dependency request - wrong method", method: http.MethodPut, expectedStatusCode: http.StatusMethodNotAllowed, - expectedResponse: nil, }, { name: "Not authorized SMS request - invalid certificate", method: http.MethodGet, invalidCert: true, expectedStatusCode: http.StatusUnauthorized, - expectedResponse: nil, }, { name: "Invalid URI", method: http.MethodGet, invalidURI: true, expectedStatusCode: http.StatusBadRequest, - expectedResponse: nil, }, { name: "Valid get SMS dependency request", method: http.MethodGet, expectedStatusCode: http.StatusOK, - expectedResponse: []map[string]string{ - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"appId": "destappname!b15", "appName": "destination"}, - {"xsappname": "rtappname!b15"}, - }, - }, - { - name: "SubscriptionDependency Always - service included regardless of class", - method: http.MethodGet, - expectedStatusCode: http.StatusOK, - caModifier: func(ca *v1alpha1.CAPApplication) { - dep := v1alpha1.SubscriptionDependencyAlways - ca.Spec.BTP.Services[0].SubscriptionDependency = &dep // xsuaa: not auto-qualified, but Always forces inclusion - }, - expectedResponse: []map[string]string{ - {"xsappname": "appname!b14"}, - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"appId": "destappname!b15", "appName": "destination"}, - {"xsappname": "rtappname!b15"}, - }, - }, - { - name: "SubscriptionDependency Auto - non-qualifying service excluded", - method: http.MethodGet, - expectedStatusCode: http.StatusOK, - caModifier: func(ca *v1alpha1.CAPApplication) { - dep := v1alpha1.SubscriptionDependencyAuto - ca.Spec.BTP.Services[0].SubscriptionDependency = &dep // xsuaa: explicit Auto, still not qualified by class/credentials - }, - expectedResponse: []map[string]string{ - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"appId": "destappname!b15", "appName": "destination"}, - {"xsappname": "rtappname!b15"}, - }, - }, - { - name: "SubscriptionDependency Never - service excluded regardless of credentials", - method: http.MethodGet, - expectedStatusCode: http.StatusOK, - caModifier: func(ca *v1alpha1.CAPApplication) { - dep := v1alpha1.SubscriptionDependencyNever - ca.Spec.BTP.Services[4].SubscriptionDependency = &dep // destination: auto-qualified by class, but Never prevents inclusion - }, - expectedResponse: []map[string]string{ - {"xsappname": "saasappname!b15"}, - {"xsappname": "smappname!b15"}, - {"xsappname": "rtappname!b15"}, - }, + expectedResponse: dependenciesJSON, }, } @@ -1835,12 +1086,8 @@ func TestGetSMSDependencies(t *testing.T) { for _, testData := range tests { t.Run(testData.name, func(t *testing.T) { - ca := createCA() - if testData.caModifier != nil { - testData.caModifier(ca) - } - secrets := append(createSmsSecret(), createSecrets()...) - subHandler := setup(nil, secrets, ca) + subPro := createSubscriptionProvider(SMS) + subHandler := setup(nil, createSmsSecret(), subPro) res := httptest.NewRecorder() var req *http.Request @@ -1867,9 +1114,8 @@ func TestGetSMSDependencies(t *testing.T) { if res.Code == http.StatusOK { resBodyStr := res.Body.String() - expectedResponseByte, _ := json.Marshal(testData.expectedResponse) - if resBodyStr != string(expectedResponseByte) { - t.Error("Unexpected error in expected response: ", res.Body) + if resBodyStr != testData.expectedResponse { + t.Error("Unexpected response: ", res.Body, " expected: ", testData.expectedResponse) } } }) diff --git a/crds/sme.sap.com_subscriptionproviders.yaml b/crds/sme.sap.com_subscriptionproviders.yaml new file mode 100644 index 00000000..d12be9e5 --- /dev/null +++ b/crds/sme.sap.com_subscriptionproviders.yaml @@ -0,0 +1,123 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: subscriptionproviders.sme.sap.com +spec: + group: sme.sap.com + names: + kind: SubscriptionProvider + listKind: SubscriptionProviderList + plural: subscriptionproviders + shortNames: + - subpro + singular: subscriptionprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.appName + name: SubscriptionProvider + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + appName: + type: string + providerSubaccountId: + type: string + subscriptionInfo: + properties: + authSecret: + type: string + subscriptionSecret: + type: string + type: + type: string + required: + - subscriptionSecret + - type + type: object + required: + - appName + - providerSubaccountId + - subscriptionInfo + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dependencies: + type: string + observedGeneration: + format: int64 + type: integer + state: + enum: + - "" + - Ready + - Error + - Processing + - Deleting + type: string + required: + - state + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/crds/sme.sap.com_subscriptions.yaml b/crds/sme.sap.com_subscriptions.yaml new file mode 100644 index 00000000..380db4a0 --- /dev/null +++ b/crds/sme.sap.com_subscriptions.yaml @@ -0,0 +1,131 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: subscriptions.sme.sap.com +spec: + group: sme.sap.com + names: + kind: Subscription + listKind: SubscriptionList + plural: subscriptions + shortNames: + - sub + singular: subscription + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.appName + name: App + type: string + - jsonPath: .spec.subscriptionGuid + name: Guid + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.url + name: Url + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + appName: + type: string + providerSubaccountId: + type: string + subdomain: + type: string + subscriptionDomain: + type: string + subscriptionGuid: + type: string + subscriptionRequestPayload: + type: string + tenantId: + type: string + required: + - appName + - providerSubaccountId + - subdomain + - subscriptionDomain + - subscriptionGuid + - subscriptionRequestPayload + - tenantId + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + format: int64 + type: integer + state: + enum: + - "" + - Ready + - Error + - Processing + - Deleting + type: string + url: + type: string + required: + - state + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/controller/common_test.go b/internal/controller/common_test.go index 3868f7d5..9f59d406 100644 --- a/internal/controller/common_test.go +++ b/internal/controller/common_test.go @@ -69,6 +69,8 @@ var gvrKindMap map[string]string = map[string]string{ "domains.sme.sap.com/v1alpha1": "Domain", "clusterdomains.sme.sap.com/v1alpha1": "ClusterDomain", "servicemonitors.monitoring.coreos.com/v1": "ServiceMonitor", + "subscriptionproviders.sme.sap.com/v1alpha1": "SubscriptionProvider", + "subscriptions.sme.sap.com/v1alpha1": "Subscription", } var createKindMap map[string]int @@ -172,6 +174,10 @@ var removeStatusTimestampHandler k8stesting.ReactionFunc = func(action k8stestin cro.Status.Conditions = adjustConditions(cro.Status.Conditions) case *v1alpha1.ClusterDomain: cro.Status.Conditions = adjustConditions(cro.Status.Conditions) + case *v1alpha1.SubscriptionProvider: + cro.Status.Conditions = adjustConditions(cro.Status.Conditions) + case *v1alpha1.Subscription: + cro.Status.Conditions = adjustConditions(cro.Status.Conditions) } } @@ -363,6 +369,10 @@ func reconcileTestItem(ctx context.Context, t *testing.T, item QueueItem, data T requeue, err = c.reconcileDomain(ctx, item, data.attempts) case ResourceClusterDomain: requeue, err = c.reconcileClusterDomain(ctx, item, data.attempts) + case ResourceSubscriptionProvider: + requeue, err = c.reconcileSubscriptionProvider(ctx, item, data.attempts) + case ResourceSubscription: + requeue, err = c.reconcileSubscription(ctx, item, data.attempts) default: t.Error("unidentified queue item for testing") } @@ -576,7 +586,7 @@ func addInitialObjectToStore(resource []byte, c *Controller) error { fakeClient.Tracker().Create(schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1", Resource: "destinationrules"}, obj, metaObj.GetNamespace()) err = c.istioInformerFactory.Networking().V1().DestinationRules().Informer().GetIndexer().Add(obj) } - case *v1alpha1.CAPApplication, *v1alpha1.CAPApplicationVersion, *v1alpha1.CAPTenant, *v1alpha1.CAPTenantOperation, *v1alpha1.Domain, *v1alpha1.ClusterDomain: + case *v1alpha1.CAPApplication, *v1alpha1.CAPApplicationVersion, *v1alpha1.CAPTenant, *v1alpha1.CAPTenantOperation, *v1alpha1.Domain, *v1alpha1.ClusterDomain, *v1alpha1.SubscriptionProvider, *v1alpha1.Subscription: fakeClient, ok := c.crdClient.(*copfake.Clientset) if !ok { return fmt.Errorf("controller is not using a fake clientset") @@ -595,6 +605,10 @@ func addInitialObjectToStore(resource []byte, c *Controller) error { err = c.crdInformerFactory.Sme().V1alpha1().Domains().Informer().GetIndexer().Add(obj) case *v1alpha1.ClusterDomain: err = c.crdInformerFactory.Sme().V1alpha1().ClusterDomains().Informer().GetIndexer().Add(obj) + case *v1alpha1.SubscriptionProvider: + err = c.crdInformerFactory.Sme().V1alpha1().SubscriptionProviders().Informer().GetIndexer().Add(obj) + case *v1alpha1.Subscription: + err = c.crdInformerFactory.Sme().V1alpha1().Subscriptions().Informer().GetIndexer().Add(obj) } case *monv1.ServiceMonitor: fakeClient, ok := c.promClient.(*promopFake.Clientset) @@ -649,7 +663,7 @@ func compareExpectedWithStore(t *testing.T, resource []byte, c *Controller) erro case *istionwv1.Gateway: actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("gateways"), mo.GetNamespace(), mo.GetName()) } - case *v1alpha1.CAPApplication, *v1alpha1.CAPApplicationVersion, *v1alpha1.CAPTenant, *v1alpha1.CAPTenantOperation, *v1alpha1.Domain, *v1alpha1.ClusterDomain: + case *v1alpha1.CAPApplication, *v1alpha1.CAPApplicationVersion, *v1alpha1.CAPTenant, *v1alpha1.CAPTenantOperation, *v1alpha1.Domain, *v1alpha1.ClusterDomain, *v1alpha1.SubscriptionProvider, *v1alpha1.Subscription: fakeClient := c.crdClient.(*copfake.Clientset) switch expected.(type) { case *v1alpha1.CAPApplication: @@ -664,6 +678,10 @@ func compareExpectedWithStore(t *testing.T, resource []byte, c *Controller) erro actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("domains"), mo.GetNamespace(), mo.GetName()) case *v1alpha1.ClusterDomain: actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("clusterdomains"), metav1.NamespaceAll, mo.GetName()) + case *v1alpha1.SubscriptionProvider: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("subscriptionproviders"), mo.GetNamespace(), mo.GetName()) + case *v1alpha1.Subscription: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("subscriptions"), mo.GetNamespace(), mo.GetName()) } case *monv1.ServiceMonitor: fakeClient := c.promClient.(*promopFake.Clientset) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 07842168..965fad8f 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -87,6 +87,8 @@ func NewController(client kubernetes.Interface, crdClient versioned.Interface, i ResourceCAPTenantOperation: workqueue.NewTypedRateLimitingQueueWithConfig(customRateLimiter(), workqueue.TypedRateLimitingQueueConfig[QueueItem]{Name: KindMap[ResourceCAPTenantOperation]}), ResourceDomain: workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[QueueItem](), workqueue.TypedRateLimitingQueueConfig[QueueItem]{Name: KindMap[ResourceDomain]}), ResourceClusterDomain: workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[QueueItem](), workqueue.TypedRateLimitingQueueConfig[QueueItem]{Name: KindMap[ResourceClusterDomain]}), + ResourceSubscriptionProvider: workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[QueueItem](), workqueue.TypedRateLimitingQueueConfig[QueueItem]{Name: KindMap[ResourceSubscriptionProvider]}), + ResourceSubscription: workqueue.NewTypedRateLimitingQueueWithConfig(customRateLimiter(), workqueue.TypedRateLimitingQueueConfig[QueueItem]{Name: KindMap[ResourceSubscription]}), } // Use 30mins as the default Resync interval for kube / proprietary resources @@ -310,6 +312,10 @@ func (c *Controller) processQueueItem(ctx context.Context, key, workerId int) er result, err = c.reconcileDomain(ctx, item, attempts) case ResourceClusterDomain: result, err = c.reconcileClusterDomain(ctx, item, attempts) + case ResourceSubscriptionProvider: + result, err = c.reconcileSubscriptionProvider(ctx, item, attempts) + case ResourceSubscription: + result, err = c.reconcileSubscription(ctx, item, attempts) default: err = errors.New("unidentified queue item") skipItem = true diff --git a/internal/controller/informers.go b/internal/controller/informers.go index 9ec4d3bb..b38e4ba1 100644 --- a/internal/controller/informers.go +++ b/internal/controller/informers.go @@ -24,6 +24,8 @@ const ( ResourceCAPTenantOperation ResourceDomain ResourceClusterDomain + ResourceSubscriptionProvider + ResourceSubscription ResourceSecret ResourceJob ResourceGateway @@ -36,6 +38,7 @@ const ( const queuing = "queuing resource for reconciliation" const defaultDependantDelay = 3 * time.Second +const defaultResourceDelay = 15 * time.Second var ( KindMap = map[int]string{ @@ -45,6 +48,8 @@ var ( ResourceCAPTenantOperation: v1alpha1.CAPTenantOperationKind, ResourceDomain: v1alpha1.DomainKind, ResourceClusterDomain: v1alpha1.ClusterDomainKind, + ResourceSubscriptionProvider: v1alpha1.SubscriptionProviderKind, + ResourceSubscription: v1alpha1.SubscriptionKind, } ) @@ -56,10 +61,12 @@ type NamespacedResourceKey struct { var QueueMapping map[int]map[int]string = map[int]map[int]string{ ResourceCAPApplication: {ResourceCAPApplication: v1alpha1.CAPApplicationKind}, ResourceCAPApplicationVersion: {ResourceCAPApplicationVersion: v1alpha1.CAPApplicationVersionKind, ResourceCAPApplication: v1alpha1.CAPApplicationKind}, - ResourceCAPTenant: {ResourceCAPTenant: v1alpha1.CAPTenantKind, ResourceCAPApplication: v1alpha1.CAPApplicationKind}, + ResourceCAPTenant: {ResourceCAPTenant: v1alpha1.CAPTenantKind, ResourceCAPApplication: v1alpha1.CAPApplicationKind, ResourceSubscription: v1alpha1.SubscriptionKind}, ResourceCAPTenantOperation: {ResourceCAPTenantOperation: v1alpha1.CAPTenantOperationKind, ResourceCAPTenant: v1alpha1.CAPTenantKind}, ResourceDomain: {ResourceDomain: v1alpha1.DomainKind}, ResourceClusterDomain: {ResourceClusterDomain: v1alpha1.ClusterDomainKind}, + ResourceSubscriptionProvider: {ResourceSubscriptionProvider: v1alpha1.SubscriptionProviderKind}, + ResourceSubscription: {ResourceSubscription: v1alpha1.SubscriptionKind}, ResourceJob: {ResourceCAPTenantOperation: v1alpha1.CAPTenantOperationKind, ResourceCAPApplicationVersion: v1alpha1.CAPApplicationVersionKind}, ResourceGateway: {ResourceDomain: v1alpha1.DomainKind, ResourceClusterDomain: v1alpha1.ClusterDomainKind}, ResourceCertificate: {ResourceDomain: v1alpha1.DomainKind, ResourceClusterDomain: v1alpha1.ClusterDomainKind}, @@ -80,23 +87,28 @@ func (c *Controller) initializeInformers() { c.registerCAPTenantOperationListeners() c.registerDomainListeners() c.registerClusterDomainListeners() + c.registerSubscriptionProviderListeners() + c.registerSubscriptionListeners() c.registerJobListeners() c.registerSecretListeners() c.registerGatewayListeners() c.registerVirtualServiceListeners() c.registerDestinationRuleListeners() + switch certificateManager() { case certManagerGardener: c.registerGardenerCertificateListeners() case certManagerCertManagerIO: c.registerCertManagerCertificateListeners() } + switch dnsManager() { case dnsManagerGardener: c.registerGardenerDNSEntrytListeners() case dnsManagerKubernetes: // no activity needed on our side so far } + klog.InfoS("informers initialized") } @@ -148,6 +160,16 @@ func (c *Controller) registerDomainListeners() { AddEventHandler(c.getEventHandlerFuncsForResource(ResourceDomain)) } +func (c *Controller) registerSubscriptionProviderListeners() { + c.crdInformerFactory.Sme().V1alpha1().SubscriptionProviders().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceSubscriptionProvider)) +} + +func (c *Controller) registerSubscriptionListeners() { + c.crdInformerFactory.Sme().V1alpha1().Subscriptions().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceSubscription)) +} + func (c *Controller) registerJobListeners() { c.kubeInformerFactory.Batch().V1().Jobs().Informer(). AddEventHandler(c.getEventHandlerFuncsForResource(ResourceJob)) diff --git a/internal/controller/reconcile-capapplication.go b/internal/controller/reconcile-capapplication.go index 784bb7e6..4f5f65fd 100644 --- a/internal/controller/reconcile-capapplication.go +++ b/internal/controller/reconcile-capapplication.go @@ -29,12 +29,14 @@ const ( CAPApplicationEventMissingIngressGatewayInfo = "MissingIngressGatewayInfo" CAPApplicationEventProviderTenantCreated = "ProviderTenantCreated" CAPApplicationEventNewCAVTriggeredTenantUpgrade = "NewCAVTriggeredTenantUpgrade" + CAPApplicationEventSubscriptionProviderCreated = "SubscriptionProviderCreated" ) const ( - EventActionProcessingSecrets = "ProcessingSecrets" - EventActionProviderTenantProcessing = "ProviderTenantProcessing" - EventActionCheckForVersion = "CheckForVersion" + EventActionProcessingSecrets = "ProcessingSecrets" + EventActionProviderTenantProcessing = "ProviderTenantProcessing" + EventActionSubscriptionProviderProcessing = "SubscriptionProviderProcessing" + EventActionCheckForVersion = "CheckForVersion" ) func (c *Controller) reconcileCAPApplication(ctx context.Context, item QueueItem, _ int) (result *ReconcileResult, err error) { @@ -79,6 +81,10 @@ func (c *Controller) reconcileCAPApplication(ctx context.Context, item QueueItem reason, message := getCAReason(genChanged) ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, reason, message) result = NewReconcileResultWithResource(ResourceCAPApplication, ca.Name, ca.Namespace, 0) + // If a SubscriptionProvider exists, reconcile it as well + if exists, _ := c.resolveSubscriptionProvider(ctx, ca, true); exists { + result.AddResource(ResourceSubscriptionProvider, ca.Name, ca.Namespace, 0) + } } else { result, err = c.handleCAPApplicationDependentResources(ctx, ca) } @@ -131,6 +137,11 @@ func (c *Controller) handleCAPApplicationDependentResources(ctx context.Context, } // We can already update LatestVersionReady to "true" at this point in time, but as this method is called several times, we do not do it here (during initial Provisioning as CA itself is may not be Consistent) + // Create SubscriptionProvider resource for non services only scenario if not already created + if _, err = c.resolveSubscriptionProvider(ctx, ca, false); err != nil { + return + } + // step 4 - validate provider tenant, create if not available if processing, err = c.reconcileCAPApplicationProviderTenant(ctx, ca, cav); err != nil || processing { return @@ -495,6 +506,69 @@ func (c *Controller) createProviderTenant(ctx context.Context, ca *v1alpha1.CAPA return } +func (c *Controller) resolveSubscriptionProvider(ctx context.Context, ca *v1alpha1.CAPApplication, skipCreate bool) (bool, error) { + if ca.IsServicesOnly() { + return false, nil + } + prov, err := c.crdInformerFactory.Sme().V1alpha1().SubscriptionProviders().Lister().SubscriptionProviders(ca.Namespace).Get(ca.Name) + if err != nil && !skipCreate { + if !k8sErrors.IsNotFound(err) { + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "SubscriptionProviderError", err.Error()) + return false, err + } + return false, c.createSubscriptionProvider(ctx, ca) + } + return prov != nil, err +} + +func (c *Controller) createSubscriptionProvider(ctx context.Context, ca *v1alpha1.CAPApplication) error { + var subscriptionInfo v1alpha1.SubscriptionInfo + for _, svc := range ca.Spec.BTP.Services { + switch svc.Class { + case "subscription-manager": + subscriptionInfo.Type = "subscription-manager" + subscriptionInfo.SubscriptionSecret = svc.Secret + case "saas-registry": + subscriptionInfo.Type = "saas-registry" + subscriptionInfo.SubscriptionSecret = svc.Secret + if xsuaaInfo := util.GetXSUAAInfo(ca.Spec.BTP.Services, ca); xsuaaInfo != nil { + subscriptionInfo.AuthSecret = xsuaaInfo.Secret + } + } + if subscriptionInfo.SubscriptionSecret != "" { + break + } + } + + labels := map[string]string{ + LabelAppIdHash: sha1Sum(ca.Spec.ProviderSubaccountId, ca.Spec.BTPAppName), + } + + util.LogInfo("Creating SubscriptionProvider", string(Processing), ca, nil) + subPro, err := c.crdClient.SmeV1alpha1().SubscriptionProviders(ca.Namespace).Create( + ctx, &v1alpha1.SubscriptionProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: ca.Name, + Namespace: ca.Namespace, + Labels: labels, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind)), + }, + }, + Spec: v1alpha1.SubscriptionProviderSpec{ + AppName: ca.Spec.BTPAppName, + ProviderSubaccountID: ca.Spec.ProviderSubaccountId, + SubscriptionInfo: subscriptionInfo, + }, + }, metav1.CreateOptions{}) + if err != nil { + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "SubscriptionProviderError", err.Error()) + return err + } + c.Event(ca, subPro, corev1.EventTypeNormal, CAPApplicationEventSubscriptionProviderCreated, EventActionSubscriptionProviderProcessing, fmt.Sprintf("created SubscriptionProvider %s.%s", subPro.Namespace, subPro.Name)) + return nil +} + func (c *Controller) handleCAPApplicationDeletion(ctx context.Context, ca *v1alpha1.CAPApplication) (*ReconcileResult, error) { var err error diff --git a/internal/controller/reconcile-captenantoperation.go b/internal/controller/reconcile-captenantoperation.go index 9e801210..4ffe4f5a 100644 --- a/internal/controller/reconcile-captenantoperation.go +++ b/internal/controller/reconcile-captenantoperation.go @@ -417,6 +417,16 @@ func (c *Controller) initiateJobForCAPTenantOperationStep(ctx context.Context, c params.providerSubdomain = relatedResources.CAPApplication.Spec.Provider.SubDomain } + // For provisioning, resolve the subscription request payload to be passed to the tenant operation job + if ctop.Spec.Operation == v1alpha1.CAPTenantOperationTypeProvisioning { + payload, err := c.getSubscriptionRequestPayload(ctop) + if err != nil { + util.LogError(err, "Failed to resolve subscription request payload", string(Processing), ctop, nil, "tenantId", ctop.Spec.TenantId, "operation", ctop.Spec.Operation) + return nil, err + } + params.subscriptionPayload = payload + } + var job *batchv1.Job if ctop.Spec.Steps[*ctop.Status.CurrentStep-1].Type == v1alpha1.JobTenantOperation { job, err = c.createTenantOperationJob(ctx, ctop, workload, params) @@ -449,6 +459,7 @@ type jobCreateParams struct { providerTenantId string providerSubdomain string tenantType string + subscriptionPayload string } func (c *Controller) createTenantOperationJob(ctx context.Context, ctop *v1alpha1.CAPTenantOperation, workload *v1alpha1.WorkloadDetails, params *jobCreateParams) (*batchv1.Job, error) { @@ -630,7 +641,13 @@ func getCTOPEnv(params *jobCreateParams, ctop *v1alpha1.CAPTenantOperation, step switch ctop.Spec.Operation { case v1alpha1.CAPTenantOperationTypeProvisioning: operation = "subscribe" - env = append(env, corev1.EnvVar{Name: EnvCAPOpSubscriptionPayload, ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: ctop.Annotations[AnnotationSubscriptionContextSecret]}, Key: SubscriptionContext}}}) + // Prefer the subscription request payload resolved from the Subscription resource. + // Fall back to the (deprecated) subscription context secret for provider tenants which have no Subscription. + if params.subscriptionPayload != "" { + env = append(env, corev1.EnvVar{Name: EnvCAPOpSubscriptionPayload, Value: params.subscriptionPayload}) + } else if ctop.Annotations[AnnotationSubscriptionContextSecret] != "" { + env = append(env, corev1.EnvVar{Name: EnvCAPOpSubscriptionPayload, ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: ctop.Annotations[AnnotationSubscriptionContextSecret]}, Key: SubscriptionContext}}}) + } case v1alpha1.CAPTenantOperationTypeUpgrade: operation = "upgrade" default: // deprovisioning @@ -642,6 +659,34 @@ func getCTOPEnv(params *jobCreateParams, ctop *v1alpha1.CAPTenantOperation, step return env } +// getSubscriptionRequestPayload resolves the subscription request payload for a tenant operation by locating the +// Subscription resource that owns the tenant (identified via the subscription-guid label) and returning its +// SubscriptionRequestPayload from the spec. Returns an empty string (no error) when no Subscription is found, +// allowing the caller to fall back to the (deprecated) subscription context secret. +func (c *Controller) getSubscriptionRequestPayload(ctop *v1alpha1.CAPTenantOperation) (string, error) { + subscriptionGUID := ctop.Labels[MetadataSubscriptionGUID] + if subscriptionGUID == "" { + return "", nil + } + + selector, err := labels.ValidatedSelectorFromSet(map[string]string{ + MetadataSubscriptionGUID: subscriptionGUID, + }) + if err != nil { + return "", err + } + + subscriptions, err := c.crdInformerFactory.Sme().V1alpha1().Subscriptions().Lister().Subscriptions(ctop.Namespace).List(selector) + if err != nil { + return "", err + } + if len(subscriptions) == 0 { + return "", nil + } + // Assume only one Subscription matches the subscriptionGUID + return subscriptions[0].Spec.SubscriptionRequestPayload, nil +} + // Collect tenant operation metrics based on the status of the tenant operation func collectTenantOperationMetrics(ctop *v1alpha1.CAPTenantOperation) { relevantAppIdHash := ctop.Labels[LabelAppIdHash] diff --git a/internal/controller/reconcile-subscription.go b/internal/controller/reconcile-subscription.go new file mode 100644 index 00000000..e524eb38 --- /dev/null +++ b/internal/controller/reconcile-subscription.go @@ -0,0 +1,295 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "fmt" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *Controller) reconcileSubscription(ctx context.Context, item QueueItem, _ int) (result *ReconcileResult, err error) { + cached, err := c.crdInformerFactory.Sme().V1alpha1().Subscriptions().Lister().Subscriptions(item.ResourceKey.Namespace).Get(item.ResourceKey.Name) + if err != nil { + return nil, handleOperatorResourceErrors(err) + } + sub := cached.DeepCopy() + + defer func() { + if statusErr := c.updateSubscriptionStatus(ctx, sub); statusErr != nil && err == nil { + err = statusErr + } + }() + + // Ensure the subscription-guid label is set on the Subscription (used to correlate it with tenant operations) + if sub.Labels[MetadataSubscriptionGUID] != sub.Spec.SubscriptionGuid { + updated, updateErr := c.updateSubscriptionLabels(ctx, sub) + if updateErr != nil { + return nil, updateErr + } + *sub = *updated + } + + // Start by ensuring the Subscription is in the Processing state before doing any work + if sub.Status.State != v1alpha1.SubscriptionStateProcessing { + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateProcessing, metav1.ConditionFalse, "Processing", "Processing subscription") + sub.SetStatusCondition(string(v1alpha1.ConditionTypeTenantReady), metav1.ConditionFalse, "Processing", "Processing subscription") + result = NewReconcileResultWithResource(ResourceSubscription, sub.Name, sub.Namespace, 0) + return + } + + // Subscription specific URL handling + // Identify the owning CAPApplication and construct the tenant-specific subscription URL (including domain validation). + ca, err := c.getSubscriptionCAPApplication(sub) + if err != nil { + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateError, metav1.ConditionFalse, "ApplicationError", err.Error()) + return + } + + appURL, err := c.getSubscriptionAppURL(sub, ca) + if err != nil { + util.LogError(err, "Error constructing subscription URL", string(Processing), sub, ca, "tenantId", sub.Spec.TenantId) + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateError, metav1.ConditionFalse, "URLError", "Error constructing subscription URL: "+err.Error()) + return + } + sub.Status.Url = appURL + + // Identify (or create) the CAPTenant owned by this Subscription + tenant, err := c.getOrCreateSubscriptionTenant(ctx, sub) + if err != nil { + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateError, metav1.ConditionFalse, "TenantError", err.Error()) + return + } + + // Wait for the owned tenant to become ready + if !isCROConditionReady(tenant.Status.GenericStatus) { + if tenant.Status.State == v1alpha1.CAPTenantStateProvisioningError || tenant.Status.State == v1alpha1.CAPTenantStateUpgradeError { + err = fmt.Errorf("tenant %s.%s in state %s", tenant.Namespace, tenant.Name, tenant.Status.State) + sub.SetStatusCondition(string(v1alpha1.ConditionTypeTenantReady), metav1.ConditionFalse, "TenantError", err.Error()) + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateError, metav1.ConditionFalse, "TenantError", err.Error()) + return + } + + msg := fmt.Sprintf("waiting for tenant %s.%s to be ready", tenant.Namespace, tenant.Name) + util.LogInfo("Waiting for tenant to be ready", string(Processing), sub, tenant, "tenantId", sub.Spec.TenantId) + sub.SetStatusCondition(string(v1alpha1.ConditionTypeTenantReady), metav1.ConditionFalse, "TenantNotReady", msg) + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateProcessing, metav1.ConditionFalse, "TenantNotReady", msg) + result = NewReconcileResultWithResource(ResourceSubscription, sub.Name, sub.Namespace, defaultResourceDelay) + return + } + + // Tenant is ready --> mark the Subscription ready + sub.SetStatusCondition(string(v1alpha1.ConditionTypeTenantReady), metav1.ConditionTrue, "TenantReady", "Tenant is ready") + sub.SetStatusWithReadyCondition(v1alpha1.SubscriptionStateReady, metav1.ConditionTrue, "Ready", "Subscription is ready") + return +} + +// getOrCreateSubscriptionTenant identifies an existing CAPTenant for the subscription (via subscriptionGuid) or creates one owned by the Subscription. +func (c *Controller) getOrCreateSubscriptionTenant(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.CAPTenant, error) { + // First check if a tenant already exists for this subscription (identified by subscriptionGuid) + tenant, err := c.getSubscriptionTenant(sub) + if err != nil { + return nil, err + } + if tenant != nil { + return tenant, nil + } + + // No tenant found; create one using the providerSubaccountId + btpAppName to identify the owning application (AppIdHash) + return c.createSubscriptionTenant(ctx, sub) +} + +// getSubscriptionTenant looks up the CAPTenant for the subscription using the subscriptionGuid (and tenantId) labels. +func (c *Controller) getSubscriptionTenant(sub *v1alpha1.Subscription) (*v1alpha1.CAPTenant, error) { + selector, err := labels.ValidatedSelectorFromSet(map[string]string{ + MetadataSubscriptionGUID: sub.Spec.SubscriptionGuid, + LabelTenantId: sub.Spec.TenantId, + }) + if err != nil { + return nil, err + } + + tenants, err := c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister().CAPTenants(sub.Namespace).List(selector) + if err != nil { + return nil, err + } + if len(tenants) == 0 { + return nil, nil + } + // Assume only one tenant matches the selector + return tenants[0], nil +} + +// createSubscriptionTenant creates a CAPTenant (owned by the Subscription) using the providerSubaccountId and btpAppName to derive the AppIdHash label/annotation. +func (c *Controller) createSubscriptionTenant(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.CAPTenant, error) { + appIdHash := sha1Sum(sub.Spec.ProviderSubaccountId, sub.Spec.AppName) + + util.LogInfo("Creating tenant for subscription", string(Processing), sub, nil, "tenantId", sub.Spec.TenantId, "subscriptionGuid", sub.Spec.SubscriptionGuid) + + tenant, err := c.crdClient.SmeV1alpha1().CAPTenants(sub.Namespace).Create(ctx, &v1alpha1.CAPTenant{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: sub.Name + "-", + Namespace: sub.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(sub, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.SubscriptionKind)), + }, + Annotations: map[string]string{ + AnnotationAppId: fmt.Sprintf("%s.%s", sub.Spec.ProviderSubaccountId, sub.Spec.AppName), + MetadataSubscriptionGUID: sub.Spec.SubscriptionGuid, + }, + Labels: map[string]string{ + LabelAppIdHash: appIdHash, + LabelTenantId: sub.Spec.TenantId, + LabelTenantType: TenantTypeConsumer, + MetadataSubscriptionGUID: sub.Spec.SubscriptionGuid, + }, + }, + Spec: v1alpha1.CAPTenantSpec{ + BTPTenantIdentification: v1alpha1.BTPTenantIdentification{ + TenantId: sub.Spec.TenantId, + SubDomain: sub.Spec.Subdomain, + }, + }, + }, metav1.CreateOptions{}) + if err != nil { + util.LogError(err, "Error creating tenant for subscription", string(Processing), sub, nil, "tenantId", sub.Spec.TenantId) + return nil, err + } + + util.LogInfo("Tenant created for subscription", string(Processing), sub, tenant, "tenantId", sub.Spec.TenantId) + return tenant, nil +} + +// updateSubscriptionLabels ensures the subscription-guid label is set on the Subscription resource itself +// (used to correlate it with the CAPTenant / CAPTenantOperation that carry the same label). +func (c *Controller) updateSubscriptionLabels(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { + if sub.Labels == nil { + sub.Labels = map[string]string{} + } + sub.Labels[MetadataSubscriptionGUID] = sub.Spec.SubscriptionGuid + return c.crdClient.SmeV1alpha1().Subscriptions(sub.Namespace).Update(ctx, sub, metav1.UpdateOptions{}) +} + +func (c *Controller) updateSubscriptionStatus(ctx context.Context, sub *v1alpha1.Subscription) error { + if isDeletionImminent(&sub.ObjectMeta) { + return nil + } + updated, err := c.crdClient.SmeV1alpha1().Subscriptions(sub.Namespace).UpdateStatus(ctx, sub, metav1.UpdateOptions{}) + if updated != nil { + *sub = *updated + } + return err +} + +// getSubscriptionCAPApplication identifies the owning CAPApplication for the subscription using the app identifier hash (derived from providerSubaccountId + btpAppName). +func (c *Controller) getSubscriptionCAPApplication(sub *v1alpha1.Subscription) (*v1alpha1.CAPApplication, error) { + selector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelAppIdHash: sha1Sum(sub.Spec.ProviderSubaccountId, sub.Spec.AppName), + }) + if err != nil { + return nil, err + } + + cas, err := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister().CAPApplications(sub.Namespace).List(selector) + if err != nil { + return nil, err + } + if len(cas) == 0 { + return nil, fmt.Errorf("no CAPApplication found for subscription %s.%s", sub.Namespace, sub.Name) + } + // Assume only one application matches the selector + return cas[0], nil +} + +// getSubscriptionAppURL constructs the tenant-specific subscription URL for the Subscription, +// mirroring the subscription server's getAppURL logic (including domain validation). +func (c *Controller) getSubscriptionAppURL(sub *v1alpha1.Subscription, ca *v1alpha1.CAPApplication) (string, error) { + needsValidation := true + var subscriptionDomain string + // Check if subscription domain is provided in the subscription spec (from the request payload). + if sub.Spec.SubscriptionDomain != "" { + subscriptionDomain = sub.Spec.SubscriptionDomain + util.LogInfo("Using subscription domain from subscription spec", string(Processing), sub, ca, "subscriptionDomain", subscriptionDomain) + } else { + // Fallback: + // First, check if subscription domain is provided in the CAPApplication annotation. If not, fallback to calculating the primary domain from the CAPApplication domain refs and use that as the subscription domain. + subscriptionDomain = ca.Annotations[AnnotationSubscriptionDomain] + if subscriptionDomain == "" { + subscriptionDomain = c.getPrimarySubscriptionDomain(sub, ca) + needsValidation = false + util.LogInfo("Using subscription domain from fallback 'primary' calculation", string(Processing), sub, ca, "subscriptionDomain", subscriptionDomain) + } else { + util.LogInfo("Using subscription domain from CAPApplication annotation", string(Processing), sub, ca, "subscriptionDomain", subscriptionDomain) + } + } + + if needsValidation { + if err := c.validateSubscriptionDomain(subscriptionDomain, ca.Namespace); err != nil { + return "", err + } + } + + return "https://" + sub.Spec.Subdomain + "." + subscriptionDomain, nil +} + +// validateSubscriptionDomain ensures the given domain is backed by a Domain (in the app's namespace) or a ClusterDomain. +func (c *Controller) validateSubscriptionDomain(domain, namespace string) error { + // First check for Domains in the app's namespace + domainsList, err := c.crdInformerFactory.Sme().V1alpha1().Domains().Lister().Domains(namespace).List(labels.Everything()) + if err != nil { + return err + } + for _, d := range domainsList { + if d.Spec.Domain == domain { + return nil + } + } + + // Check for ClusterDomains if not found in the namespace + clusterDomainsList, err := c.crdInformerFactory.Sme().V1alpha1().ClusterDomains().Lister().List(labels.Everything()) + if err != nil { + return err + } + for _, cd := range clusterDomainsList { + if cd.Spec.Domain == domain { + return nil + } + } + + return fmt.Errorf("domain %s not found in Domains or ClusterDomains", domain) +} + +// getPrimarySubscriptionDomain resolves the primary domain of the CAPApplication (first domain ref) to use as a fallback subscription domain. +func (c *Controller) getPrimarySubscriptionDomain(sub *v1alpha1.Subscription, ca *v1alpha1.CAPApplication) string { + // If no domainRefs are specified, return an empty string + if len(ca.Spec.DomainRefs) == 0 { + return "" + } + // Use the first domain ref as the primary domain + primaryDomainRef := ca.Spec.DomainRefs[0] + domain := "" + if primaryDomainRef.Kind == v1alpha1.DomainKind { + primaryDom, err := c.crdInformerFactory.Sme().V1alpha1().Domains().Lister().Domains(ca.Namespace).Get(primaryDomainRef.Name) + if err != nil { + util.LogError(err, "Error getting primary domain", string(Processing), sub, ca, "domainRef", primaryDomainRef.Name) + } else if primaryDom != nil { + domain = primaryDom.Spec.Domain + } + } else { + primaryDom, err := c.crdInformerFactory.Sme().V1alpha1().ClusterDomains().Lister().ClusterDomains(metav1.NamespaceAll).Get(primaryDomainRef.Name) + if err != nil { + util.LogError(err, "Error getting primary cluster domain", string(Processing), sub, ca, "domainRef", primaryDomainRef.Name) + } else if primaryDom != nil { + domain = primaryDom.Spec.Domain + } + } + // Return the primary domain if it exists, else return an empty string + return domain +} diff --git a/internal/controller/reconcile-subscription_test.go b/internal/controller/reconcile-subscription_test.go new file mode 100644 index 00000000..ed118b79 --- /dev/null +++ b/internal/controller/reconcile-subscription_test.go @@ -0,0 +1,210 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "testing" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +func TestSubscriptionStateTransitionToProcessing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-01"}}, + TestData{ + description: "subscription with empty state transitions to Processing", + initialResources: []string{"testdata/subscription/sub-01.initial.yaml"}, + expectedResources: "testdata/subscription/sub-01.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-01"}, + }, + }, + }, + ) +} + +func TestSubscriptionCAPApplicationNotFound(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-02"}}, + TestData{ + description: "subscription in Processing with no matching CAPApplication → ApplicationError", + initialResources: []string{"testdata/subscription/sub-02.initial.yaml"}, + expectedResources: "testdata/subscription/sub-02.expected.yaml", + expectError: true, + }, + ) +} + +func TestSubscriptionInvalidSpecDomain(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-03"}}, + TestData{ + description: "subscription with unresolvable spec.subscriptionDomain → URLError", + initialResources: []string{"testdata/subscription/sub-03.initial.yaml"}, + expectedResources: "testdata/subscription/sub-03.expected.yaml", + expectError: true, + }, + ) +} + +func TestSubscriptionCreateTenantFromSpecDomain(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-04"}}, + TestData{ + description: "subscription with valid spec.subscriptionDomain (Domain) creates CAPTenant and requeues", + initialResources: []string{"testdata/subscription/sub-04.initial.yaml"}, + expectedResources: "testdata/subscription/sub-04.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-04"}, + }, + }, + }, + ) +} + +func TestSubscriptionCreateTenantFromCAAnnotationDomain(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-05"}}, + TestData{ + description: "subscription domain from CAPApplication annotation (ClusterDomain) creates CAPTenant and requeues", + initialResources: []string{"testdata/subscription/sub-05.initial.yaml"}, + expectedResources: "testdata/subscription/sub-05.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-05"}, + }, + }, + }, + ) +} + +func TestSubscriptionCreateTenantFromPrimaryDomainRef(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-06"}}, + TestData{ + description: "subscription domain from primary DomainRef (Domain) creates CAPTenant and requeues", + initialResources: []string{"testdata/subscription/sub-06.initial.yaml"}, + expectedResources: "testdata/subscription/sub-06.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-06"}, + }, + }, + }, + ) +} + +func TestSubscriptionCreateTenantFromPrimaryClusterDomainRef(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-07"}}, + TestData{ + description: "subscription domain from primary ClusterDomainRef creates CAPTenant and requeues", + initialResources: []string{"testdata/subscription/sub-07.initial.yaml"}, + expectedResources: "testdata/subscription/sub-07.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-07"}, + }, + }, + }, + ) +} + +func TestSubscriptionWaitsForTenantProvisioning(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-08"}}, + TestData{ + description: "subscription waits for existing CAPTenant still Provisioning", + initialResources: []string{"testdata/subscription/sub-08.initial.yaml"}, + expectedResources: "testdata/subscription/sub-08.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-08"}, + }, + }, + }, + ) +} + +func TestSubscriptionTenantProvisioningError(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-09"}}, + TestData{ + description: "subscription transitions to Error when CAPTenant is in ProvisioningError", + initialResources: []string{"testdata/subscription/sub-09.initial.yaml"}, + expectedResources: "testdata/subscription/sub-09.expected.yaml", + expectError: true, + }, + ) +} + +func TestSubscriptionReadyWhenTenantReady(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-10"}}, + TestData{ + description: "subscription transitions to Ready when CAPTenant is ready", + initialResources: []string{"testdata/subscription/sub-10.initial.yaml"}, + expectedResources: "testdata/subscription/sub-10.expected.yaml", + }, + ) +} + +func TestSubscriptionNotFound(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-notfound"}}, + TestData{ + description: "subscription not found in store is handled without error", + expectResourceNotFound: true, + }, + ) +} + +func TestSubscriptionGUIDLabelSync(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-12"}}, + TestData{ + description: "subscription with missing GUID label gets label updated and transitions to Processing", + initialResources: []string{"testdata/subscription/sub-12.initial.yaml"}, + expectedResources: "testdata/subscription/sub-12.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceSubscription: { + {Namespace: "default", Name: "test-sub-12"}, + }, + }, + }, + ) +} + +func TestSubscriptionTenantUpgradeError(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscription, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-sub-13"}}, + TestData{ + description: "subscription transitions to Error when CAPTenant is in UpgradeError", + initialResources: []string{"testdata/subscription/sub-13.initial.yaml"}, + expectedResources: "testdata/subscription/sub-13.expected.yaml", + expectError: true, + }, + ) +} + +// Compile-time check that v1alpha1 is used +var _ = v1alpha1.SubscriptionStateReady diff --git a/internal/controller/reconcile-subscriptionprovider.go b/internal/controller/reconcile-subscriptionprovider.go new file mode 100644 index 00000000..5cbe9d18 --- /dev/null +++ b/internal/controller/reconcile-subscriptionprovider.go @@ -0,0 +1,156 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type serviceMetaInfo struct { + Plan string `json:"plan"` + Credentials serviceCredentials `json:"credentials"` +} + +type serviceCredentials struct { + XSAppName string `json:"xsappname"` + SaasRegistryEnabled bool `json:"saasregistryenabled"` + UAA *struct { + XSAppName string `json:"xsappname"` + } `json:"uaa"` +} + +func (c *serviceCredentials) xsAppName() string { + if c.XSAppName != "" { + return c.XSAppName + } + if c.UAA != nil && c.UAA.XSAppName != "" { + return c.UAA.XSAppName + } + return "" +} + +func (c *Controller) reconcileSubscriptionProvider(ctx context.Context, item QueueItem, _ int) (result *ReconcileResult, err error) { + cached, err := c.crdInformerFactory.Sme().V1alpha1().SubscriptionProviders().Lister().SubscriptionProviders(item.ResourceKey.Namespace).Get(item.ResourceKey.Name) + if err != nil { + return nil, handleOperatorResourceErrors(err) + } + subPro := cached.DeepCopy() + + defer func() { + if statusErr := c.updateSubscriptionProviderStatus(ctx, subPro); statusErr != nil && err == nil { + err = statusErr + } + }() + + if subPro.Status.State != v1alpha1.SubscriptionProviderStateProcessing { + subPro.SetStatusWithReadyCondition(v1alpha1.SubscriptionProviderStateProcessing, metav1.ConditionFalse, "Processing", "Processing subscription provider") + result = NewReconcileResultWithResource(ResourceSubscriptionProvider, subPro.Name, subPro.Namespace, 0) + return + } + + // Fetch owning CAPApplication + ownerRef, ok := getOwnerByKind(subPro.GetOwnerReferences(), v1alpha1.CAPApplicationKind) + if !ok { + err = fmt.Errorf("SubscriptionProvider %s.%s has no owning CAPApplication", subPro.Namespace, subPro.Name) + subPro.SetStatusWithReadyCondition(v1alpha1.SubscriptionProviderStateError, metav1.ConditionFalse, "MissingOwner", err.Error()) + return + } + ca, err := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister().CAPApplications(subPro.Namespace).Get(ownerRef.Name) + if err != nil { + subPro.SetStatusWithReadyCondition(v1alpha1.SubscriptionProviderStateError, metav1.ConditionFalse, "OwnerNotFound", err.Error()) + return + } + + dependencies, err := c.buildSubscriptionDependencies(ca) + if err != nil { + subPro.SetStatusWithReadyCondition(v1alpha1.SubscriptionProviderStateError, metav1.ConditionFalse, "DependencyResolutionFailed", err.Error()) + return + } + + subPro.Status.Dependencies = dependencies + subPro.SetStatusWithReadyCondition(v1alpha1.SubscriptionProviderStateReady, metav1.ConditionTrue, "Ready", "Subscription dependencies resolved") + return +} + +func (c *Controller) buildSubscriptionDependencies(ca *v1alpha1.CAPApplication) (string, error) { + var dependenciesArray []map[string]string + for _, service := range ca.Spec.BTP.Services { + serviceCredInfo, err := util.ReadServiceCredentialsFromSecret[serviceMetaInfo](&service, ca.Namespace, c.kubeClient, true) + if err != nil { + util.LogError(err, "Failed to read secret for service", string(Processing), ca, nil, "service", service.Name, "secret", service.Secret) + return "", err + } + + dep := getSubscriptionProviderServiceDependency(service, serviceCredInfo) + if dep != nil { + dependenciesArray = append(dependenciesArray, dep) + } + } + + if len(dependenciesArray) == 0 { + util.LogInfo("No subscription dependencies found", string(Processing), ca, nil) + return "", nil + } + + b, err := json.Marshal(dependenciesArray) + if err != nil { + return "", fmt.Errorf("failed to marshal subscription dependencies: %w", err) + } + + util.LogInfo("Subscription dependencies resolved", string(Processing), ca, nil, "count", len(dependenciesArray), "dependencies", string(b)) + return string(b), nil +} + +func getSubscriptionProviderServiceDependency(service v1alpha1.ServiceInfo, serviceCredInfo *serviceMetaInfo) map[string]string { + if isSubscriptionServiceRelevantForDependencies(service, serviceCredInfo) { + if name := serviceCredInfo.Credentials.xsAppName(); name != "" { + if isSubscriptionSpecialDependency(service, serviceCredInfo) { + return map[string]string{ + "appName": service.Class, + "appId": name, + } + } else { + return map[string]string{ + "xsappname": name, + } + } + } + } + return nil +} + +func isSubscriptionServiceRelevantForDependencies(serviceInfo v1alpha1.ServiceInfo, creds *serviceMetaInfo) bool { + if serviceInfo.GetSubscriptionDependency() == v1alpha1.SubscriptionDependencyAlways { + return true + } + if serviceInfo.GetSubscriptionDependency() == v1alpha1.SubscriptionDependencyAuto { + return isSubscriptionSpecialDependency(serviceInfo, creds) || creds.Credentials.SaasRegistryEnabled + } + return false +} + +func isSubscriptionSpecialDependency(serviceInfo v1alpha1.ServiceInfo, creds *serviceMetaInfo) bool { + return serviceInfo.Class == "destination" || + serviceInfo.Class == "connectivity" || + (serviceInfo.Class == "auditlog" && creds.Plan == "oauth2") +} + +func (c *Controller) updateSubscriptionProviderStatus(ctx context.Context, subPro *v1alpha1.SubscriptionProvider) error { + if isDeletionImminent(&subPro.ObjectMeta) { + return nil + } + updated, err := c.crdClient.SmeV1alpha1().SubscriptionProviders(subPro.Namespace).UpdateStatus(ctx, subPro, metav1.UpdateOptions{}) + if updated != nil { + *subPro = *updated + } + return err +} diff --git a/internal/controller/reconcile-subscriptionprovider_test.go b/internal/controller/reconcile-subscriptionprovider_test.go new file mode 100644 index 00000000..3af33175 --- /dev/null +++ b/internal/controller/reconcile-subscriptionprovider_test.go @@ -0,0 +1,158 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "testing" +) + +func TestSubscriptionProviderStateTransitionToProcessing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-01"}}, + TestData{ + description: "SubscriptionProvider in empty state transitions to Processing and requeues", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-01.initial.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-01.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceSubscriptionProvider: {{Namespace: "default", Name: "test-subpro-01"}}}, + }, + ) +} + +func TestSubscriptionProviderNoOwningCAPApplication(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-02"}}, + TestData{ + description: "SubscriptionProvider in Processing state with no ownerReference transitions to Error", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-02.initial.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-02.expected.yaml", + expectError: true, + }, + ) +} + +func TestSubscriptionProviderOwnerCAPApplicationNotFound(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-03"}}, + TestData{ + description: "SubscriptionProvider in Processing state with owner ref but CAPApplication missing in store transitions to Error", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-03.initial.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-03.expected.yaml", + expectError: true, + }, + ) +} + +func TestSubscriptionProviderSecretNotFound(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-04"}}, + TestData{ + description: "SubscriptionProvider in Processing state with CAPApplication that references a missing secret transitions to Error", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-04.initial.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-04.expected.yaml", + expectError: true, + }, + ) +} + +func TestSubscriptionProviderNoDependencies(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-05"}}, + TestData{ + description: "SubscriptionProvider with CAPApplication whose only service (xsuaa) is not a subscription dependency - resolves to Ready with empty dependencies", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-05.initial.yaml", + "testdata/subscriptionprovider/credential-secrets.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-05.expected.yaml", + }, + ) +} + +func TestSubscriptionProviderSaaSRegistryDependency(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-06"}}, + TestData{ + description: "SubscriptionProvider with CAPApplication using saas-registry (saasregistryenabled=true) - resolves to Ready with xsappname dependency", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-06.initial.yaml", + "testdata/subscriptionprovider/credential-secrets.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-06.expected.yaml", + }, + ) +} + +func TestSubscriptionProviderDestinationSpecialDependency(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-07"}}, + TestData{ + description: "SubscriptionProvider with CAPApplication using destination service - resolves to Ready with appName/appId special dependency format", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-07.initial.yaml", + "testdata/subscriptionprovider/credential-secrets.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-07.expected.yaml", + }, + ) +} + +func TestSubscriptionProviderMixedDependencies(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-08"}}, + TestData{ + description: "SubscriptionProvider with CAPApplication containing mixed services (xsuaa, saas-registry, destination, service-manager) - resolves to Ready with saas and destination dependencies", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-08.initial.yaml", + "testdata/subscriptionprovider/credential-secrets.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-08.expected.yaml", + }, + ) +} + +func TestSubscriptionProviderAlwaysDependency(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-09"}}, + TestData{ + description: "SubscriptionProvider with CAPApplication service marked SubscriptionDependencyAlways - included regardless of credential content", + initialResources: []string{ + "testdata/subscriptionprovider/subpro-09.initial.yaml", + "testdata/subscriptionprovider/credential-secrets.yaml", + }, + expectedResources: "testdata/subscriptionprovider/subpro-09.expected.yaml", + }, + ) +} + +func TestSubscriptionProviderNotFound(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceSubscriptionProvider, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-subpro-missing"}}, + TestData{ + description: "SubscriptionProvider not found in store - reconciliation skipped without error", + initialResources: []string{}, + expectResourceNotFound: true, + }, + ) +} diff --git a/internal/controller/reconcile.go b/internal/controller/reconcile.go index 4f829395..a3625276 100644 --- a/internal/controller/reconcile.go +++ b/internal/controller/reconcile.go @@ -50,6 +50,7 @@ const ( AnnotationGardenerDNSTarget = "dns.gardener.cloud/dnsnames" AnnotationKubernetesDNSTarget = "external-dns.alpha.kubernetes.io/hostname" AnnotationSubscriptionContextSecret = "sme.sap.com/subscription-context-secret" + AnnotationSubscriptionDomain = "sme.sap.com/subscription-domain" AnnotationGlobalAccountId = "sme.sap.com/global-account-id" AnnotationEnableCleanupMonitoring = "sme.sap.com/enable-cleanup-monitoring" AnnotationVSRouteRequestHeaderSet = "sme.sap.com/vs-route-request-header-set" // configures headers on incoming requests for Istio VirtualService route handling @@ -75,6 +76,7 @@ const ( ) const TenantTypeProvider = "provider" +const TenantTypeConsumer = "consumer" const ( EnvCAPOpAppVersion = "CAPOP_APP_VERSION" diff --git a/internal/controller/testdata/subscription/sub-01.expected.yaml b/internal/controller/testdata/subscription/sub-01.expected.yaml new file mode 100644 index 00000000..998e62b9 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-01.expected.yaml @@ -0,0 +1,33 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-01 + namespace: default + uid: aaaa-bbbb-dddd-0001 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + conditions: + - reason: Processing + message: Processing subscription + observedGeneration: 1 + status: "False" + type: Ready + - reason: Processing + message: Processing subscription + observedGeneration: 1 + status: "False" + type: TenantReady + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/subscription/sub-01.initial.yaml b/internal/controller/testdata/subscription/sub-01.initial.yaml new file mode 100644 index 00000000..8327e1f5 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-01.initial.yaml @@ -0,0 +1,21 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-01 + namespace: default + uid: aaaa-bbbb-dddd-0001 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + state: "" diff --git a/internal/controller/testdata/subscription/sub-02.expected.yaml b/internal/controller/testdata/subscription/sub-02.expected.yaml new file mode 100644 index 00000000..6915666d --- /dev/null +++ b/internal/controller/testdata/subscription/sub-02.expected.yaml @@ -0,0 +1,28 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-02 + namespace: default + uid: aaaa-bbbb-dddd-0002 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + conditions: + - reason: ApplicationError + message: 'no CAPApplication found for subscription default.test-sub-02' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/subscription/sub-02.initial.yaml b/internal/controller/testdata/subscription/sub-02.initial.yaml new file mode 100644 index 00000000..c20e227e --- /dev/null +++ b/internal/controller/testdata/subscription/sub-02.initial.yaml @@ -0,0 +1,21 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-02 + namespace: default + uid: aaaa-bbbb-dddd-0002 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + state: Processing diff --git a/internal/controller/testdata/subscription/sub-03.expected.yaml b/internal/controller/testdata/subscription/sub-03.expected.yaml new file mode 100644 index 00000000..fbdf9edf --- /dev/null +++ b/internal/controller/testdata/subscription/sub-03.expected.yaml @@ -0,0 +1,28 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-03 + namespace: default + uid: aaaa-bbbb-dddd-0003 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: invalid.domain.local + subscriptionRequestPayload: "" +status: + conditions: + - reason: URLError + message: 'Error constructing subscription URL: domain invalid.domain.local not found in Domains or ClusterDomains' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/subscription/sub-03.initial.yaml b/internal/controller/testdata/subscription/sub-03.initial.yaml new file mode 100644 index 00000000..bb5b99b0 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-03.initial.yaml @@ -0,0 +1,35 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-03 + namespace: default + uid: aaaa-bbbb-dddd-0003 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: invalid.domain.local + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] diff --git a/internal/controller/testdata/subscription/sub-04.expected.yaml b/internal/controller/testdata/subscription/sub-04.expected.yaml new file mode 100644 index 00000000..f72e0626 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-04.expected.yaml @@ -0,0 +1,59 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-04 + namespace: default + uid: aaaa-bbbb-dddd-0004 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantNotReady + message: waiting for tenant default.test-sub-04-gen to be ready + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantNotReady + message: waiting for tenant default.test-sub-04-gen to be ready + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing + url: https://test-subdomain.app-domain.test.local +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-04-gen + generateName: test-sub-04- + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: Subscription + name: test-sub-04 + controller: true + blockOwnerDeletion: true + uid: aaaa-bbbb-dddd-0004 + annotations: + sme.sap.com/app-identifier: pro-subacc-id.test-cap-01 + sme.sap.com/subscription-guid: test-guid-01 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/tenant-type: consumer + sme.sap.com/subscription-guid: test-guid-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain diff --git a/internal/controller/testdata/subscription/sub-04.initial.yaml b/internal/controller/testdata/subscription/sub-04.initial.yaml new file mode 100644 index 00000000..8f653e04 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-04.initial.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-04 + namespace: default + uid: aaaa-bbbb-dddd-0004 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: Domain +metadata: + name: test-cap-01-primary + namespace: default +spec: + dnsMode: Wildcard + domain: app-domain.test.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready diff --git a/internal/controller/testdata/subscription/sub-05.expected.yaml b/internal/controller/testdata/subscription/sub-05.expected.yaml new file mode 100644 index 00000000..5b6867cb --- /dev/null +++ b/internal/controller/testdata/subscription/sub-05.expected.yaml @@ -0,0 +1,59 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-05 + namespace: default + uid: aaaa-bbbb-dddd-0005 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantNotReady + message: waiting for tenant default.test-sub-05-gen to be ready + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantNotReady + message: waiting for tenant default.test-sub-05-gen to be ready + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing + url: https://test-subdomain.foo.bar.local +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-05-gen + generateName: test-sub-05- + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: Subscription + name: test-sub-05 + controller: true + blockOwnerDeletion: true + uid: aaaa-bbbb-dddd-0005 + annotations: + sme.sap.com/app-identifier: pro-subacc-id.test-cap-01 + sme.sap.com/subscription-guid: test-guid-01 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/tenant-type: consumer + sme.sap.com/subscription-guid: test-guid-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain diff --git a/internal/controller/testdata/subscription/sub-05.initial.yaml b/internal/controller/testdata/subscription/sub-05.initial.yaml new file mode 100644 index 00000000..eb95913f --- /dev/null +++ b/internal/controller/testdata/subscription/sub-05.initial.yaml @@ -0,0 +1,51 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-05 + namespace: default + uid: aaaa-bbbb-dddd-0005 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/subscription-domain: foo.bar.local + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: ClusterDomain +metadata: + name: test-cap-01-secondary +spec: + dnsMode: Subdomain + domain: foo.bar.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready diff --git a/internal/controller/testdata/subscription/sub-06.expected.yaml b/internal/controller/testdata/subscription/sub-06.expected.yaml new file mode 100644 index 00000000..a2e591c5 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-06.expected.yaml @@ -0,0 +1,59 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-06 + namespace: default + uid: aaaa-bbbb-dddd-0006 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantNotReady + message: waiting for tenant default.test-sub-06-gen to be ready + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantNotReady + message: waiting for tenant default.test-sub-06-gen to be ready + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing + url: https://test-subdomain.app-domain.test.local +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-06-gen + generateName: test-sub-06- + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: Subscription + name: test-sub-06 + controller: true + blockOwnerDeletion: true + uid: aaaa-bbbb-dddd-0006 + annotations: + sme.sap.com/app-identifier: pro-subacc-id.test-cap-01 + sme.sap.com/subscription-guid: test-guid-01 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/tenant-type: consumer + sme.sap.com/subscription-guid: test-guid-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain diff --git a/internal/controller/testdata/subscription/sub-06.initial.yaml b/internal/controller/testdata/subscription/sub-06.initial.yaml new file mode 100644 index 00000000..b80091f0 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-06.initial.yaml @@ -0,0 +1,54 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-06 + namespace: default + uid: aaaa-bbbb-dddd-0006 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + state: Processing +--- +# CA with DomainRef to Domain (no annotation) - uses primary DomainRef fallback +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + domainRefs: + - kind: Domain + name: test-cap-01-primary + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: Domain +metadata: + name: test-cap-01-primary + namespace: default +spec: + dnsMode: Wildcard + domain: app-domain.test.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready diff --git a/internal/controller/testdata/subscription/sub-07.expected.yaml b/internal/controller/testdata/subscription/sub-07.expected.yaml new file mode 100644 index 00000000..e8a62e50 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-07.expected.yaml @@ -0,0 +1,59 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-07 + namespace: default + uid: aaaa-bbbb-dddd-0007 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantNotReady + message: waiting for tenant default.test-sub-07-gen to be ready + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantNotReady + message: waiting for tenant default.test-sub-07-gen to be ready + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing + url: https://test-subdomain.foo.bar.local +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-07-gen + generateName: test-sub-07- + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: Subscription + name: test-sub-07 + controller: true + blockOwnerDeletion: true + uid: aaaa-bbbb-dddd-0007 + annotations: + sme.sap.com/app-identifier: pro-subacc-id.test-cap-01 + sme.sap.com/subscription-guid: test-guid-01 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/tenant-type: consumer + sme.sap.com/subscription-guid: test-guid-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain diff --git a/internal/controller/testdata/subscription/sub-07.initial.yaml b/internal/controller/testdata/subscription/sub-07.initial.yaml new file mode 100644 index 00000000..4d8de63c --- /dev/null +++ b/internal/controller/testdata/subscription/sub-07.initial.yaml @@ -0,0 +1,53 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-07 + namespace: default + uid: aaaa-bbbb-dddd-0007 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + state: Processing +--- +# CA with DomainRef to ClusterDomain (no annotation) - uses primary ClusterDomainRef fallback +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + domainRefs: + - kind: ClusterDomain + name: test-cap-01-secondary + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: ClusterDomain +metadata: + name: test-cap-01-secondary +spec: + dnsMode: Subdomain + domain: foo.bar.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready diff --git a/internal/controller/testdata/subscription/sub-08.expected.yaml b/internal/controller/testdata/subscription/sub-08.expected.yaml new file mode 100644 index 00000000..77ca89dc --- /dev/null +++ b/internal/controller/testdata/subscription/sub-08.expected.yaml @@ -0,0 +1,34 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-08 + namespace: default + uid: aaaa-bbbb-dddd-0008 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantNotReady + message: waiting for tenant default.test-sub-08-tenant to be ready + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantNotReady + message: waiting for tenant default.test-sub-08-tenant to be ready + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing + url: https://test-subdomain.app-domain.test.local diff --git a/internal/controller/testdata/subscription/sub-08.initial.yaml b/internal/controller/testdata/subscription/sub-08.initial.yaml new file mode 100644 index 00000000..bb79c16d --- /dev/null +++ b/internal/controller/testdata/subscription/sub-08.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-08 + namespace: default + uid: aaaa-bbbb-dddd-0008 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: Domain +metadata: + name: test-cap-01-primary + namespace: default +spec: + dnsMode: Wildcard + domain: app-domain.test.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready +--- +# Existing CAPTenant for this subscription - still Provisioning (not ready, not in error) +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-08-tenant + namespace: default + labels: + sme.sap.com/subscription-guid: test-guid-01 + sme.sap.com/btp-tenant-id: test-tenant-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain +status: + conditions: + - reason: Processing + message: Tenant is being provisioned + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Provisioning diff --git a/internal/controller/testdata/subscription/sub-09.expected.yaml b/internal/controller/testdata/subscription/sub-09.expected.yaml new file mode 100644 index 00000000..15e0427b --- /dev/null +++ b/internal/controller/testdata/subscription/sub-09.expected.yaml @@ -0,0 +1,34 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-09 + namespace: default + uid: aaaa-bbbb-dddd-0009 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantError + message: 'tenant default.test-sub-09-tenant in state ProvisioningError' + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantError + message: 'tenant default.test-sub-09-tenant in state ProvisioningError' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error + url: https://test-subdomain.app-domain.test.local diff --git a/internal/controller/testdata/subscription/sub-09.initial.yaml b/internal/controller/testdata/subscription/sub-09.initial.yaml new file mode 100644 index 00000000..2140b426 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-09.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-09 + namespace: default + uid: aaaa-bbbb-dddd-0009 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: Domain +metadata: + name: test-cap-01-primary + namespace: default +spec: + dnsMode: Wildcard + domain: app-domain.test.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready +--- +# Existing CAPTenant in ProvisioningError +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-09-tenant + namespace: default + labels: + sme.sap.com/subscription-guid: test-guid-01 + sme.sap.com/btp-tenant-id: test-tenant-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain +status: + conditions: + - reason: ProvisioningError + message: Tenant provisioning failed + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: ProvisioningError diff --git a/internal/controller/testdata/subscription/sub-10.expected.yaml b/internal/controller/testdata/subscription/sub-10.expected.yaml new file mode 100644 index 00000000..3e3b4893 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-10.expected.yaml @@ -0,0 +1,34 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-10 + namespace: default + uid: aaaa-bbbb-dddd-0010 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantReady + message: Tenant is ready + observedGeneration: 1 + status: "True" + type: TenantReady + - reason: Ready + message: Subscription is ready + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready + url: https://test-subdomain.app-domain.test.local diff --git a/internal/controller/testdata/subscription/sub-10.initial.yaml b/internal/controller/testdata/subscription/sub-10.initial.yaml new file mode 100644 index 00000000..8a318a94 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-10.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-10 + namespace: default + uid: aaaa-bbbb-dddd-0010 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: Domain +metadata: + name: test-cap-01-primary + namespace: default +spec: + dnsMode: Wildcard + domain: app-domain.test.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready +--- +# Existing CAPTenant - Ready +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-10-tenant + namespace: default + labels: + sme.sap.com/subscription-guid: test-guid-01 + sme.sap.com/btp-tenant-id: test-tenant-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain +status: + conditions: + - reason: Ready + message: Tenant is ready + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/subscription/sub-12.expected.yaml b/internal/controller/testdata/subscription/sub-12.expected.yaml new file mode 100644 index 00000000..bbc6737b --- /dev/null +++ b/internal/controller/testdata/subscription/sub-12.expected.yaml @@ -0,0 +1,33 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-12 + namespace: default + uid: aaaa-bbbb-dddd-0012 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + conditions: + - reason: Processing + message: Processing subscription + observedGeneration: 1 + status: "False" + type: Ready + - reason: Processing + message: Processing subscription + observedGeneration: 1 + status: "False" + type: TenantReady + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/subscription/sub-12.initial.yaml b/internal/controller/testdata/subscription/sub-12.initial.yaml new file mode 100644 index 00000000..5c9a5e50 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-12.initial.yaml @@ -0,0 +1,20 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-12 + namespace: default + uid: aaaa-bbbb-dddd-0012 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: "" + subscriptionRequestPayload: "" +status: + state: "" diff --git a/internal/controller/testdata/subscription/sub-13.expected.yaml b/internal/controller/testdata/subscription/sub-13.expected.yaml new file mode 100644 index 00000000..4068a824 --- /dev/null +++ b/internal/controller/testdata/subscription/sub-13.expected.yaml @@ -0,0 +1,34 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-13 + namespace: default + uid: aaaa-bbbb-dddd-0013 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + conditions: + - reason: TenantError + message: 'tenant default.test-sub-13-tenant in state UpgradeError' + observedGeneration: 1 + status: "False" + type: TenantReady + - reason: TenantError + message: 'tenant default.test-sub-13-tenant in state UpgradeError' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error + url: https://test-subdomain.app-domain.test.local diff --git a/internal/controller/testdata/subscription/sub-13.initial.yaml b/internal/controller/testdata/subscription/sub-13.initial.yaml new file mode 100644 index 00000000..0506250b --- /dev/null +++ b/internal/controller/testdata/subscription/sub-13.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: Subscription +metadata: + name: test-sub-13 + namespace: default + uid: aaaa-bbbb-dddd-0013 + generation: 1 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 + sme.sap.com/btp-tenant-id: test-tenant-01 + sme.sap.com/subscription-guid: test-guid-01 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + tenantId: test-tenant-01 + subdomain: test-subdomain + subscriptionGuid: test-guid-01 + subscriptionDomain: app-domain.test.local + subscriptionRequestPayload: "" +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + labels: + sme.sap.com/app-identifier-hash: 8de67d2d3734797789af01ba68df94b31dda0357 +spec: + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id + btp: + services: [] +--- +apiVersion: sme.sap.com/v1alpha1 +kind: Domain +metadata: + name: test-cap-01-primary + namespace: default +spec: + dnsMode: Wildcard + domain: app-domain.test.local + ingressSelector: + app: istio-ingressgateway + istio: ingressgateway + tlsMode: Simple +status: + state: Ready +--- +# Existing CAPTenant in UpgradeError +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-sub-13-tenant + namespace: default + labels: + sme.sap.com/subscription-guid: test-guid-01 + sme.sap.com/btp-tenant-id: test-tenant-01 +spec: + tenantId: test-tenant-01 + subDomain: test-subdomain +status: + conditions: + - reason: UpgradeError + message: Tenant upgrade failed + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: UpgradeError diff --git a/internal/controller/testdata/subscriptionprovider/credential-secrets.yaml b/internal/controller/testdata/subscriptionprovider/credential-secrets.yaml new file mode 100644 index 00000000..b48d16a4 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/credential-secrets.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Secret +metadata: + name: subpro-xsuaa-sec + namespace: default +type: Opaque +data: + credentials: eyJ4c2FwcG5hbWUiOiJ0ZXN0LXhzdWFhIWIxNCJ9 +--- +apiVersion: v1 +kind: Secret +metadata: + name: subpro-saas-sec + namespace: default +type: Opaque +data: + credentials: eyJ1YWEiOnsieHNhcHBuYW1lIjoidGVzdC1zYWFzIWIxNSJ9LCJzYWFzcmVnaXN0cnllbmFibGVkIjp0cnVlfQ== +--- +apiVersion: v1 +kind: Secret +metadata: + name: subpro-dest-sec + namespace: default +type: Opaque +data: + credentials: eyJ4c2FwcG5hbWUiOiJ0ZXN0LWRlc3QhYjE1In0= +--- +apiVersion: v1 +kind: Secret +metadata: + name: subpro-sm-sec + namespace: default +type: Opaque +data: + credentials: eyJzbV91cmwiOiJodHRwczovL3NtLnNlcnZpY2UubG9jYWwifQ== +--- +apiVersion: v1 +kind: Secret +metadata: + name: subpro-always-sec + namespace: default +type: Opaque +data: + credentials: eyJ4c2FwcG5hbWUiOiJ0ZXN0LWFsd2F5cyFiMTYifQ== diff --git a/internal/controller/testdata/subscriptionprovider/subpro-01.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-01.expected.yaml new file mode 100644 index 00000000..77e93639 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-01.expected.yaml @@ -0,0 +1,23 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-01 + namespace: default + uid: aaaa-bbbb-cccc-0001 + generation: 1 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: Processing + message: Processing subscription provider + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/subscriptionprovider/subpro-01.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-01.initial.yaml new file mode 100644 index 00000000..a281596e --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-01.initial.yaml @@ -0,0 +1,16 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-01 + namespace: default + uid: aaaa-bbbb-cccc-0001 + generation: 1 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: "" diff --git a/internal/controller/testdata/subscriptionprovider/subpro-02.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-02.expected.yaml new file mode 100644 index 00000000..57301ef0 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-02.expected.yaml @@ -0,0 +1,23 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-02 + namespace: default + uid: aaaa-bbbb-cccc-0002 + generation: 1 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: MissingOwner + message: 'SubscriptionProvider default.test-subpro-02 has no owning CAPApplication' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/subscriptionprovider/subpro-02.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-02.initial.yaml new file mode 100644 index 00000000..b0c8776b --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-02.initial.yaml @@ -0,0 +1,16 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-02 + namespace: default + uid: aaaa-bbbb-cccc-0002 + generation: 1 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing diff --git a/internal/controller/testdata/subscriptionprovider/subpro-03.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-03.expected.yaml new file mode 100644 index 00000000..113a1601 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-03.expected.yaml @@ -0,0 +1,29 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-03 + namespace: default + uid: aaaa-bbbb-cccc-0003 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: OwnerNotFound + message: 'capapplication.sme.sap.com "test-cap-01" not found' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/subscriptionprovider/subpro-03.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-03.initial.yaml new file mode 100644 index 00000000..f77d9fed --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-03.initial.yaml @@ -0,0 +1,22 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-03 + namespace: default + uid: aaaa-bbbb-cccc-0003 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing diff --git a/internal/controller/testdata/subscriptionprovider/subpro-04.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-04.expected.yaml new file mode 100644 index 00000000..622db141 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-04.expected.yaml @@ -0,0 +1,29 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-04 + namespace: default + uid: aaaa-bbbb-cccc-0004 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: DependencyResolutionFailed + message: 'secrets "missing-secret" not found' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/subscriptionprovider/subpro-04.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-04.initial.yaml new file mode 100644 index 00000000..7f70409d --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-04.initial.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-04 + namespace: default + uid: aaaa-bbbb-cccc-0004 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + generation: 1 +spec: + btp: + services: + - class: saas-registry + name: cap-saas-registry + secret: missing-secret + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id diff --git a/internal/controller/testdata/subscriptionprovider/subpro-05.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-05.expected.yaml new file mode 100644 index 00000000..a5235a13 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-05.expected.yaml @@ -0,0 +1,29 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-05 + namespace: default + uid: aaaa-bbbb-cccc-0005 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: Ready + message: Subscription dependencies resolved + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/subscriptionprovider/subpro-05.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-05.initial.yaml new file mode 100644 index 00000000..690a463a --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-05.initial.yaml @@ -0,0 +1,39 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-05 + namespace: default + uid: aaaa-bbbb-cccc-0005 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing +--- +# CA with only xsuaa - not a subscription dependency (saasregistryenabled=false, not a special class) +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + generation: 1 +spec: + btp: + services: + - class: xsuaa + name: cap-xsuaa + secret: subpro-xsuaa-sec + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id diff --git a/internal/controller/testdata/subscriptionprovider/subpro-06.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-06.expected.yaml new file mode 100644 index 00000000..4fb1d7c4 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-06.expected.yaml @@ -0,0 +1,30 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-06 + namespace: default + uid: aaaa-bbbb-cccc-0006 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: Ready + message: Subscription dependencies resolved + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready + dependencies: '[{"xsappname":"test-saas!b15"}]' diff --git a/internal/controller/testdata/subscriptionprovider/subpro-06.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-06.initial.yaml new file mode 100644 index 00000000..adb139ee --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-06.initial.yaml @@ -0,0 +1,39 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-06 + namespace: default + uid: aaaa-bbbb-cccc-0006 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing +--- +# CA with saas-registry (saasregistryenabled=true) - auto detection via credentials +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + generation: 1 +spec: + btp: + services: + - class: saas-registry + name: cap-saas-registry + secret: subpro-saas-sec + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id diff --git a/internal/controller/testdata/subscriptionprovider/subpro-07.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-07.expected.yaml new file mode 100644 index 00000000..00efaa67 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-07.expected.yaml @@ -0,0 +1,30 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-07 + namespace: default + uid: aaaa-bbbb-cccc-0007 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: Ready + message: Subscription dependencies resolved + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready + dependencies: '[{"appId":"test-dest!b15","appName":"destination"}]' diff --git a/internal/controller/testdata/subscriptionprovider/subpro-07.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-07.initial.yaml new file mode 100644 index 00000000..a410221c --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-07.initial.yaml @@ -0,0 +1,39 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-07 + namespace: default + uid: aaaa-bbbb-cccc-0007 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing +--- +# CA with destination service (special dependency by class name, uses appName/appId format) +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + generation: 1 +spec: + btp: + services: + - class: destination + name: cap-destination + secret: subpro-dest-sec + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id diff --git a/internal/controller/testdata/subscriptionprovider/subpro-08.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-08.expected.yaml new file mode 100644 index 00000000..0ba797a9 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-08.expected.yaml @@ -0,0 +1,30 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-08 + namespace: default + uid: aaaa-bbbb-cccc-0008 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: Ready + message: Subscription dependencies resolved + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready + dependencies: '[{"xsappname":"test-saas!b15"},{"appId":"test-dest!b15","appName":"destination"}]' diff --git a/internal/controller/testdata/subscriptionprovider/subpro-08.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-08.initial.yaml new file mode 100644 index 00000000..c3641436 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-08.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-08 + namespace: default + uid: aaaa-bbbb-cccc-0008 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing +--- +# CA with mixed services: xsuaa (no dep) + saas-registry (dep) + destination (special dep) +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + generation: 1 +spec: + btp: + services: + - class: xsuaa + name: cap-xsuaa + secret: subpro-xsuaa-sec + - class: saas-registry + name: cap-saas-registry + secret: subpro-saas-sec + - class: destination + name: cap-destination + secret: subpro-dest-sec + - class: service-manager + name: cap-service-manager + secret: subpro-sm-sec + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id diff --git a/internal/controller/testdata/subscriptionprovider/subpro-09.expected.yaml b/internal/controller/testdata/subscriptionprovider/subpro-09.expected.yaml new file mode 100644 index 00000000..5b05b614 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-09.expected.yaml @@ -0,0 +1,30 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-09 + namespace: default + uid: aaaa-bbbb-cccc-0009 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + conditions: + - reason: Ready + message: Subscription dependencies resolved + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready + dependencies: '[{"xsappname":"test-always!b16"}]' diff --git a/internal/controller/testdata/subscriptionprovider/subpro-09.initial.yaml b/internal/controller/testdata/subscriptionprovider/subpro-09.initial.yaml new file mode 100644 index 00000000..1fd03ab4 --- /dev/null +++ b/internal/controller/testdata/subscriptionprovider/subpro-09.initial.yaml @@ -0,0 +1,40 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: SubscriptionProvider +metadata: + name: test-subpro-09 + namespace: default + uid: aaaa-bbbb-cccc-0009 + generation: 1 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + kind: CAPApplication + name: test-cap-01 + controller: true + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + appName: test-cap-01 + providerSubaccountId: pro-subacc-id + subscriptionInfo: + type: saas-registry + subscriptionSecret: subpro-saas-sec + authSecret: subpro-xsuaa-sec +status: + state: Processing +--- +# CA with a service explicitly marked SubscriptionDependencyAlways - included regardless of credentials +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: test-cap-01 + namespace: default + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + generation: 1 +spec: + btp: + services: + - class: service-manager + name: cap-service-manager + secret: subpro-always-sec + subscriptionDependency: Always + btpAppName: test-cap-01 + providerSubaccountId: pro-subacc-id diff --git a/pkg/apis/sme.sap.com/v1alpha1/register.go b/pkg/apis/sme.sap.com/v1alpha1/register.go index 3690f750..2ed81e81 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/register.go +++ b/pkg/apis/sme.sap.com/v1alpha1/register.go @@ -68,6 +68,16 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ClusterDomain{}, &ClusterDomainList{}, ) + scheme.AddKnownTypes( + SchemeGroupVersion, + &SubscriptionProvider{}, + &SubscriptionProviderList{}, + ) + scheme.AddKnownTypes( + SchemeGroupVersion, + &Subscription{}, + &SubscriptionList{}, + ) metaV1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } diff --git a/pkg/apis/sme.sap.com/v1alpha1/types.go b/pkg/apis/sme.sap.com/v1alpha1/types.go index fd737af0..bf631cbb 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/types.go +++ b/pkg/apis/sme.sap.com/v1alpha1/types.go @@ -30,6 +30,10 @@ const ( DomainResource = "domains" ClusterDomainKind = "ClusterDomain" ClusterDomainResource = "clusterdomains" + SubscriptionProviderKind = "SubscriptionProvider" + SubscriptionProviderResource = "subscriptionproviders" + SubscriptionKind = "Subscription" + SubscriptionResource = "subscriptions" ) // +kubebuilder:resource:shortName=ca @@ -914,3 +918,139 @@ type ClusterDomainList struct { metav1.ListMeta `json:"metadata"` Items []ClusterDomain `json:"items"` } + +// +kubebuilder:resource:shortName=subpro +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="SubscriptionProvider",type="string",JSONPath=".spec.appName" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state" +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SubscriptionProvider is the schema for subscriptionproviders API +type SubscriptionProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + // SubscriptionProvider spec + Spec SubscriptionProviderSpec `json:"spec"` + // +kubebuilder:validation:Optional + // SubscriptionProvider status + Status SubscriptionProviderStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SubscriptionProviderList contains a list of SubscriptionProvider +type SubscriptionProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []SubscriptionProvider `json:"items"` +} + +type SubscriptionProviderSpec struct { + // Name of the application that is being subscribed to + AppName string `json:"appName"` + // Provider subaccount ID for the application that is being subscribed to + ProviderSubaccountID string `json:"providerSubaccountId"` + // The secret(s) containing the subscription credentials that are necessary to create a subscription for the application (Fetched from the same namespace as this resource). + SubscriptionInfo SubscriptionInfo `json:"subscriptionInfo"` +} + +type SubscriptionInfo struct { + // Type of Subscription (subscription-manager / saas-registry) + Type string `json:"type"` + // Name of the Subscription Credential + SubscriptionSecret string `json:"subscriptionSecret"` + // Name of the Auth Credential (Optional) + AuthSecret string `json:"authSecret,omitempty"` +} + +type SubscriptionProviderStatus struct { + GenericStatus `json:",inline"` + // State of the Domain + State SubscriptionProviderState `json:"state"` + // List of subscription dependencies discovered for the services specified in the spec + Dependencies string `json:"dependencies,omitempty"` +} + +// +kubebuilder:validation:Enum="";Ready;Error;Processing;Deleting +type SubscriptionProviderState string + +const ( + SubscriptionProviderStateProcessing SubscriptionProviderState = "Processing" + SubscriptionProviderStateError SubscriptionProviderState = "Error" + SubscriptionProviderStateDeleting SubscriptionProviderState = "Deleting" + SubscriptionProviderStateReady SubscriptionProviderState = "Ready" +) + +// +kubebuilder:resource:shortName=sub +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="App",type="string",JSONPath=".spec.appName" +// +kubebuilder:printcolumn:name="Guid",type="string",JSONPath=".spec.subscriptionGuid" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state" +// +kubebuilder:printcolumn:name="Url",type="string",JSONPath=".status.url" +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Subscription is the schema for subscriptions API +type Subscription struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + // SubscriptionProvider spec + Spec SubscriptionSpec `json:"spec"` + // +kubebuilder:validation:Optional + // SubscriptionProvider status + Status SubscriptionStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SubscriptionList contains a list of Subscription +type SubscriptionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []Subscription `json:"items"` +} + +type SubscriptionSpec struct { + // Name of the application that is being subscribed to + AppName string `json:"appName"` + // Provider subaccount ID of the application that is being subscribed to (used to identify the owning CAPApplication) + ProviderSubaccountId string `json:"providerSubaccountId"` + // TenantId of the consumer subaccount subscribing to the application + TenantId string `json:"tenantId"` + // Subdomain of the consumer subaccount subscribing to the app + Subdomain string `json:"subdomain"` + // SubscriptionGuid of the subscription + SubscriptionGuid string `json:"subscriptionGuid"` + // Subscrption domain of the consumer subaccount subscribing to the app + SubscriptionDomain string `json:"subscriptionDomain"` + // Payload of the subscription request (JSON string). + SubscriptionRequestPayload string `json:"subscriptionRequestPayload"` +} + +type SubscriptionStatus struct { + GenericStatus `json:",inline"` + // State of the Domain + State SubscriptionState `json:"state"` + // Tenant specific URL of the subscribed application + Url string `json:"url,omitempty"` +} + +// +kubebuilder:validation:Enum="";Ready;Error;Processing;Deleting +type SubscriptionState string + +const ( + SubscriptionStateProcessing SubscriptionState = "Processing" + SubscriptionStateError SubscriptionState = "Error" + SubscriptionStateDeleting SubscriptionState = "Deleting" + SubscriptionStateReady SubscriptionState = "Ready" +) + +type SubscriptionStatusConditionType string + +const ( + // Condition reflecting whether the owned CAPTenant is ready + ConditionTypeTenantReady SubscriptionStatusConditionType = "TenantReady" +) diff --git a/pkg/apis/sme.sap.com/v1alpha1/utils.go b/pkg/apis/sme.sap.com/v1alpha1/utils.go index eaa119a8..e93e92ac 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/utils.go +++ b/pkg/apis/sme.sap.com/v1alpha1/utils.go @@ -212,6 +212,21 @@ func (cdom *ClusterDomain) GetStatusReadyConditionMessage() string { return "" } +func (subPro *SubscriptionProvider) SetStatusWithReadyCondition(state SubscriptionProviderState, readyStatus metav1.ConditionStatus, reason string, message string) { + subPro.Status.State = state + subPro.Status.SetStatusCondition(metav1.Condition{Type: readyType, Status: readyStatus, Reason: reason, Message: message, ObservedGeneration: subPro.Generation}) +} + +func (sub *Subscription) SetStatusWithReadyCondition(state SubscriptionState, readyStatus metav1.ConditionStatus, reason string, message string) { + sub.Status.State = state + sub.SetStatusCondition(readyType, readyStatus, reason, message) +} + +// SetStatusCondition updates/sets a condition in the Status of the Subscription. +func (sub *Subscription) SetStatusCondition(conditionType string, status metav1.ConditionStatus, reason string, message string) { + sub.Status.SetStatusCondition(metav1.Condition{Type: conditionType, Status: status, Reason: reason, Message: message, ObservedGeneration: sub.Generation}) +} + func (serviceInfo ServiceInfo) GetSubscriptionDependency() SubscriptionDependency { if serviceInfo.SubscriptionDependency == nil { return SubscriptionDependencyAuto diff --git a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go index 877b1244..8387014d 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go @@ -1347,6 +1347,211 @@ func (in *StickinessHash) DeepCopy() *StickinessHash { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Subscription) DeepCopyInto(out *Subscription) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subscription. +func (in *Subscription) DeepCopy() *Subscription { + if in == nil { + return nil + } + out := new(Subscription) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Subscription) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionInfo) DeepCopyInto(out *SubscriptionInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionInfo. +func (in *SubscriptionInfo) DeepCopy() *SubscriptionInfo { + if in == nil { + return nil + } + out := new(SubscriptionInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionList) DeepCopyInto(out *SubscriptionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Subscription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionList. +func (in *SubscriptionList) DeepCopy() *SubscriptionList { + if in == nil { + return nil + } + out := new(SubscriptionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SubscriptionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionProvider) DeepCopyInto(out *SubscriptionProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionProvider. +func (in *SubscriptionProvider) DeepCopy() *SubscriptionProvider { + if in == nil { + return nil + } + out := new(SubscriptionProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SubscriptionProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionProviderList) DeepCopyInto(out *SubscriptionProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SubscriptionProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionProviderList. +func (in *SubscriptionProviderList) DeepCopy() *SubscriptionProviderList { + if in == nil { + return nil + } + out := new(SubscriptionProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SubscriptionProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionProviderSpec) DeepCopyInto(out *SubscriptionProviderSpec) { + *out = *in + out.SubscriptionInfo = in.SubscriptionInfo + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionProviderSpec. +func (in *SubscriptionProviderSpec) DeepCopy() *SubscriptionProviderSpec { + if in == nil { + return nil + } + out := new(SubscriptionProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionProviderStatus) DeepCopyInto(out *SubscriptionProviderStatus) { + *out = *in + in.GenericStatus.DeepCopyInto(&out.GenericStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionProviderStatus. +func (in *SubscriptionProviderStatus) DeepCopy() *SubscriptionProviderStatus { + if in == nil { + return nil + } + out := new(SubscriptionProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionSpec) DeepCopyInto(out *SubscriptionSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionSpec. +func (in *SubscriptionSpec) DeepCopy() *SubscriptionSpec { + if in == nil { + return nil + } + out := new(SubscriptionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubscriptionStatus) DeepCopyInto(out *SubscriptionStatus) { + *out = *in + in.GenericStatus.DeepCopyInto(&out.GenericStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubscriptionStatus. +func (in *SubscriptionStatus) DeepCopy() *SubscriptionStatus { + if in == nil { + return nil + } + out := new(SubscriptionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantOperationWorkloadReference) DeepCopyInto(out *TenantOperationWorkloadReference) { *out = *in diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscription.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscription.go new file mode 100644 index 00000000..c50e9726 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscription.go @@ -0,0 +1,236 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// SubscriptionApplyConfiguration represents a declarative configuration of the Subscription type for use +// with apply. +// +// Subscription is the schema for subscriptions API +type SubscriptionApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:""` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // SubscriptionProvider spec + Spec *SubscriptionSpecApplyConfiguration `json:"spec,omitempty"` + // SubscriptionProvider status + Status *SubscriptionStatusApplyConfiguration `json:"status,omitempty"` +} + +// Subscription constructs a declarative configuration of the Subscription type for use with +// apply. +func Subscription(name, namespace string) *SubscriptionApplyConfiguration { + b := &SubscriptionApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Subscription") + b.WithAPIVersion("sme.sap.com/v1alpha1") + return b +} + +func (b SubscriptionApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithKind(value string) *SubscriptionApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithAPIVersion(value string) *SubscriptionApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithName(value string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithGenerateName(value string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithNamespace(value string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithUID(value types.UID) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithResourceVersion(value string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithGeneration(value int64) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithCreationTimestamp(value metav1.Time) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *SubscriptionApplyConfiguration) WithLabels(entries map[string]string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *SubscriptionApplyConfiguration) WithAnnotations(entries map[string]string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *SubscriptionApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *SubscriptionApplyConfiguration) WithFinalizers(values ...string) *SubscriptionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *SubscriptionApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithSpec(value *SubscriptionSpecApplyConfiguration) *SubscriptionApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *SubscriptionApplyConfiguration) WithStatus(value *SubscriptionStatusApplyConfiguration) *SubscriptionApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *SubscriptionApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *SubscriptionApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *SubscriptionApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *SubscriptionApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptioninfo.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptioninfo.go new file mode 100644 index 00000000..a1544593 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptioninfo.go @@ -0,0 +1,49 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// SubscriptionInfoApplyConfiguration represents a declarative configuration of the SubscriptionInfo type for use +// with apply. +type SubscriptionInfoApplyConfiguration struct { + // Type of Subscription (subscription-manager / saas-registry) + Type *string `json:"type,omitempty"` + // Name of the Subscription Credential + SubscriptionSecret *string `json:"subscriptionSecret,omitempty"` + // Name of the Auth Credential (Optional) + AuthSecret *string `json:"authSecret,omitempty"` +} + +// SubscriptionInfoApplyConfiguration constructs a declarative configuration of the SubscriptionInfo type for use with +// apply. +func SubscriptionInfo() *SubscriptionInfoApplyConfiguration { + return &SubscriptionInfoApplyConfiguration{} +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *SubscriptionInfoApplyConfiguration) WithType(value string) *SubscriptionInfoApplyConfiguration { + b.Type = &value + return b +} + +// WithSubscriptionSecret sets the SubscriptionSecret field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubscriptionSecret field is set to the value of the last call. +func (b *SubscriptionInfoApplyConfiguration) WithSubscriptionSecret(value string) *SubscriptionInfoApplyConfiguration { + b.SubscriptionSecret = &value + return b +} + +// WithAuthSecret sets the AuthSecret field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuthSecret field is set to the value of the last call. +func (b *SubscriptionInfoApplyConfiguration) WithAuthSecret(value string) *SubscriptionInfoApplyConfiguration { + b.AuthSecret = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionprovider.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionprovider.go new file mode 100644 index 00000000..51400455 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionprovider.go @@ -0,0 +1,236 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// SubscriptionProviderApplyConfiguration represents a declarative configuration of the SubscriptionProvider type for use +// with apply. +// +// SubscriptionProvider is the schema for subscriptionproviders API +type SubscriptionProviderApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:""` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // SubscriptionProvider spec + Spec *SubscriptionProviderSpecApplyConfiguration `json:"spec,omitempty"` + // SubscriptionProvider status + Status *SubscriptionProviderStatusApplyConfiguration `json:"status,omitempty"` +} + +// SubscriptionProvider constructs a declarative configuration of the SubscriptionProvider type for use with +// apply. +func SubscriptionProvider(name, namespace string) *SubscriptionProviderApplyConfiguration { + b := &SubscriptionProviderApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("SubscriptionProvider") + b.WithAPIVersion("sme.sap.com/v1alpha1") + return b +} + +func (b SubscriptionProviderApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithKind(value string) *SubscriptionProviderApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithAPIVersion(value string) *SubscriptionProviderApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithName(value string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithGenerateName(value string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithNamespace(value string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithUID(value types.UID) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithResourceVersion(value string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithGeneration(value int64) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithCreationTimestamp(value metav1.Time) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *SubscriptionProviderApplyConfiguration) WithLabels(entries map[string]string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *SubscriptionProviderApplyConfiguration) WithAnnotations(entries map[string]string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *SubscriptionProviderApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *SubscriptionProviderApplyConfiguration) WithFinalizers(values ...string) *SubscriptionProviderApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *SubscriptionProviderApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithSpec(value *SubscriptionProviderSpecApplyConfiguration) *SubscriptionProviderApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *SubscriptionProviderApplyConfiguration) WithStatus(value *SubscriptionProviderStatusApplyConfiguration) *SubscriptionProviderApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *SubscriptionProviderApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *SubscriptionProviderApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *SubscriptionProviderApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *SubscriptionProviderApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionproviderspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionproviderspec.go new file mode 100644 index 00000000..9272324e --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionproviderspec.go @@ -0,0 +1,49 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// SubscriptionProviderSpecApplyConfiguration represents a declarative configuration of the SubscriptionProviderSpec type for use +// with apply. +type SubscriptionProviderSpecApplyConfiguration struct { + // Name of the application that is being subscribed to + AppName *string `json:"appName,omitempty"` + // Provider subaccount ID for the application that is being subscribed to + ProviderSubaccountID *string `json:"providerSubaccountId,omitempty"` + // The secret(s) containing the subscription credentials that are necessary to create a subscription for the application (Fetched from the same namespace as this resource). + SubscriptionInfo *SubscriptionInfoApplyConfiguration `json:"subscriptionInfo,omitempty"` +} + +// SubscriptionProviderSpecApplyConfiguration constructs a declarative configuration of the SubscriptionProviderSpec type for use with +// apply. +func SubscriptionProviderSpec() *SubscriptionProviderSpecApplyConfiguration { + return &SubscriptionProviderSpecApplyConfiguration{} +} + +// WithAppName sets the AppName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AppName field is set to the value of the last call. +func (b *SubscriptionProviderSpecApplyConfiguration) WithAppName(value string) *SubscriptionProviderSpecApplyConfiguration { + b.AppName = &value + return b +} + +// WithProviderSubaccountID sets the ProviderSubaccountID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ProviderSubaccountID field is set to the value of the last call. +func (b *SubscriptionProviderSpecApplyConfiguration) WithProviderSubaccountID(value string) *SubscriptionProviderSpecApplyConfiguration { + b.ProviderSubaccountID = &value + return b +} + +// WithSubscriptionInfo sets the SubscriptionInfo field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubscriptionInfo field is set to the value of the last call. +func (b *SubscriptionProviderSpecApplyConfiguration) WithSubscriptionInfo(value *SubscriptionInfoApplyConfiguration) *SubscriptionProviderSpecApplyConfiguration { + b.SubscriptionInfo = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionproviderstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionproviderstatus.go new file mode 100644 index 00000000..97f013d1 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionproviderstatus.go @@ -0,0 +1,66 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// SubscriptionProviderStatusApplyConfiguration represents a declarative configuration of the SubscriptionProviderStatus type for use +// with apply. +type SubscriptionProviderStatusApplyConfiguration struct { + GenericStatusApplyConfiguration `json:""` + // State of the Domain + State *smesapcomv1alpha1.SubscriptionProviderState `json:"state,omitempty"` + // List of subscription dependencies discovered for the services specified in the spec + Dependencies *string `json:"dependencies,omitempty"` +} + +// SubscriptionProviderStatusApplyConfiguration constructs a declarative configuration of the SubscriptionProviderStatus type for use with +// apply. +func SubscriptionProviderStatus() *SubscriptionProviderStatusApplyConfiguration { + return &SubscriptionProviderStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *SubscriptionProviderStatusApplyConfiguration) WithObservedGeneration(value int64) *SubscriptionProviderStatusApplyConfiguration { + b.GenericStatusApplyConfiguration.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *SubscriptionProviderStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *SubscriptionProviderStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.GenericStatusApplyConfiguration.Conditions = append(b.GenericStatusApplyConfiguration.Conditions, *values[i]) + } + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *SubscriptionProviderStatusApplyConfiguration) WithState(value smesapcomv1alpha1.SubscriptionProviderState) *SubscriptionProviderStatusApplyConfiguration { + b.State = &value + return b +} + +// WithDependencies sets the Dependencies field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Dependencies field is set to the value of the last call. +func (b *SubscriptionProviderStatusApplyConfiguration) WithDependencies(value string) *SubscriptionProviderStatusApplyConfiguration { + b.Dependencies = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionspec.go new file mode 100644 index 00000000..31059854 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionspec.go @@ -0,0 +1,89 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// SubscriptionSpecApplyConfiguration represents a declarative configuration of the SubscriptionSpec type for use +// with apply. +type SubscriptionSpecApplyConfiguration struct { + // Name of the application that is being subscribed to + AppName *string `json:"appName,omitempty"` + // Provider subaccount ID of the application that is being subscribed to (used to identify the owning CAPApplication) + ProviderSubaccountId *string `json:"providerSubaccountId,omitempty"` + // TenantId of the consumer subaccount subscribing to the application + TenantId *string `json:"tenantId,omitempty"` + // Subdomain of the consumer subaccount subscribing to the app + Subdomain *string `json:"subdomain,omitempty"` + // SubscriptionGuid of the subscription + SubscriptionGuid *string `json:"subscriptionGuid,omitempty"` + // Subscrption domain of the consumer subaccount subscribing to the app + SubscriptionDomain *string `json:"subscriptionDomain,omitempty"` + // Payload of the subscription request (JSON string). + SubscriptionRequestPayload *string `json:"subscriptionRequestPayload,omitempty"` +} + +// SubscriptionSpecApplyConfiguration constructs a declarative configuration of the SubscriptionSpec type for use with +// apply. +func SubscriptionSpec() *SubscriptionSpecApplyConfiguration { + return &SubscriptionSpecApplyConfiguration{} +} + +// WithAppName sets the AppName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AppName field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithAppName(value string) *SubscriptionSpecApplyConfiguration { + b.AppName = &value + return b +} + +// WithProviderSubaccountId sets the ProviderSubaccountId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ProviderSubaccountId field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithProviderSubaccountId(value string) *SubscriptionSpecApplyConfiguration { + b.ProviderSubaccountId = &value + return b +} + +// WithTenantId sets the TenantId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TenantId field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithTenantId(value string) *SubscriptionSpecApplyConfiguration { + b.TenantId = &value + return b +} + +// WithSubdomain sets the Subdomain field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Subdomain field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithSubdomain(value string) *SubscriptionSpecApplyConfiguration { + b.Subdomain = &value + return b +} + +// WithSubscriptionGuid sets the SubscriptionGuid field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubscriptionGuid field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithSubscriptionGuid(value string) *SubscriptionSpecApplyConfiguration { + b.SubscriptionGuid = &value + return b +} + +// WithSubscriptionDomain sets the SubscriptionDomain field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubscriptionDomain field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithSubscriptionDomain(value string) *SubscriptionSpecApplyConfiguration { + b.SubscriptionDomain = &value + return b +} + +// WithSubscriptionRequestPayload sets the SubscriptionRequestPayload field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubscriptionRequestPayload field is set to the value of the last call. +func (b *SubscriptionSpecApplyConfiguration) WithSubscriptionRequestPayload(value string) *SubscriptionSpecApplyConfiguration { + b.SubscriptionRequestPayload = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionstatus.go new file mode 100644 index 00000000..4131fb95 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/subscriptionstatus.go @@ -0,0 +1,66 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// SubscriptionStatusApplyConfiguration represents a declarative configuration of the SubscriptionStatus type for use +// with apply. +type SubscriptionStatusApplyConfiguration struct { + GenericStatusApplyConfiguration `json:""` + // State of the Domain + State *smesapcomv1alpha1.SubscriptionState `json:"state,omitempty"` + // Tenant specific URL of the subscribed application + Url *string `json:"url,omitempty"` +} + +// SubscriptionStatusApplyConfiguration constructs a declarative configuration of the SubscriptionStatus type for use with +// apply. +func SubscriptionStatus() *SubscriptionStatusApplyConfiguration { + return &SubscriptionStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *SubscriptionStatusApplyConfiguration) WithObservedGeneration(value int64) *SubscriptionStatusApplyConfiguration { + b.GenericStatusApplyConfiguration.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *SubscriptionStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *SubscriptionStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.GenericStatusApplyConfiguration.Conditions = append(b.GenericStatusApplyConfiguration.Conditions, *values[i]) + } + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *SubscriptionStatusApplyConfiguration) WithState(value smesapcomv1alpha1.SubscriptionState) *SubscriptionStatusApplyConfiguration { + b.State = &value + return b +} + +// WithUrl sets the Url field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Url field is set to the value of the last call. +func (b *SubscriptionStatusApplyConfiguration) WithUrl(value string) *SubscriptionStatusApplyConfiguration { + b.Url = &value + return b +} diff --git a/pkg/client/applyconfiguration/utils.go b/pkg/client/applyconfiguration/utils.go index 7a806870..66ba82bf 100644 --- a/pkg/client/applyconfiguration/utils.go +++ b/pkg/client/applyconfiguration/utils.go @@ -105,6 +105,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &smesapcomv1alpha1.StickinessApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("StickinessHash"): return &smesapcomv1alpha1.StickinessHashApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("Subscription"): + return &smesapcomv1alpha1.SubscriptionApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SubscriptionInfo"): + return &smesapcomv1alpha1.SubscriptionInfoApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SubscriptionProvider"): + return &smesapcomv1alpha1.SubscriptionProviderApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SubscriptionProviderSpec"): + return &smesapcomv1alpha1.SubscriptionProviderSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SubscriptionProviderStatus"): + return &smesapcomv1alpha1.SubscriptionProviderStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SubscriptionSpec"): + return &smesapcomv1alpha1.SubscriptionSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SubscriptionStatus"): + return &smesapcomv1alpha1.SubscriptionStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("TenantOperations"): return &smesapcomv1alpha1.TenantOperationsApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("TenantOperationWorkloadReference"): diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go index c716a6d8..60b06804 100644 --- a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go @@ -45,6 +45,14 @@ func (c *FakeSmeV1alpha1) Domains(namespace string) v1alpha1.DomainInterface { return newFakeDomains(c, namespace) } +func (c *FakeSmeV1alpha1) Subscriptions(namespace string) v1alpha1.SubscriptionInterface { + return newFakeSubscriptions(c, namespace) +} + +func (c *FakeSmeV1alpha1) SubscriptionProviders(namespace string) v1alpha1.SubscriptionProviderInterface { + return newFakeSubscriptionProviders(c, namespace) +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeSmeV1alpha1) RESTClient() rest.Interface { diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_subscription.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_subscription.go new file mode 100644 index 00000000..c2ce77d1 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_subscription.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + typedsmesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeSubscriptions implements SubscriptionInterface +type fakeSubscriptions struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.Subscription, *v1alpha1.SubscriptionList, *smesapcomv1alpha1.SubscriptionApplyConfiguration] + Fake *FakeSmeV1alpha1 +} + +func newFakeSubscriptions(fake *FakeSmeV1alpha1, namespace string) typedsmesapcomv1alpha1.SubscriptionInterface { + return &fakeSubscriptions{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.Subscription, *v1alpha1.SubscriptionList, *smesapcomv1alpha1.SubscriptionApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("subscriptions"), + v1alpha1.SchemeGroupVersion.WithKind("Subscription"), + func() *v1alpha1.Subscription { return &v1alpha1.Subscription{} }, + func() *v1alpha1.SubscriptionList { return &v1alpha1.SubscriptionList{} }, + func(dst, src *v1alpha1.SubscriptionList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.SubscriptionList) []*v1alpha1.Subscription { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.SubscriptionList, items []*v1alpha1.Subscription) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_subscriptionprovider.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_subscriptionprovider.go new file mode 100644 index 00000000..f3e7d914 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_subscriptionprovider.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + typedsmesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeSubscriptionProviders implements SubscriptionProviderInterface +type fakeSubscriptionProviders struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.SubscriptionProvider, *v1alpha1.SubscriptionProviderList, *smesapcomv1alpha1.SubscriptionProviderApplyConfiguration] + Fake *FakeSmeV1alpha1 +} + +func newFakeSubscriptionProviders(fake *FakeSmeV1alpha1, namespace string) typedsmesapcomv1alpha1.SubscriptionProviderInterface { + return &fakeSubscriptionProviders{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.SubscriptionProvider, *v1alpha1.SubscriptionProviderList, *smesapcomv1alpha1.SubscriptionProviderApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("subscriptionproviders"), + v1alpha1.SchemeGroupVersion.WithKind("SubscriptionProvider"), + func() *v1alpha1.SubscriptionProvider { return &v1alpha1.SubscriptionProvider{} }, + func() *v1alpha1.SubscriptionProviderList { return &v1alpha1.SubscriptionProviderList{} }, + func(dst, src *v1alpha1.SubscriptionProviderList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.SubscriptionProviderList) []*v1alpha1.SubscriptionProvider { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.SubscriptionProviderList, items []*v1alpha1.SubscriptionProvider) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go index d1371a90..8588a09e 100644 --- a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go @@ -20,3 +20,7 @@ type CAPTenantOutputExpansion interface{} type ClusterDomainExpansion interface{} type DomainExpansion interface{} + +type SubscriptionExpansion interface{} + +type SubscriptionProviderExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go index 07813cb3..4639cf56 100644 --- a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go @@ -24,6 +24,8 @@ type SmeV1alpha1Interface interface { CAPTenantOutputsGetter ClusterDomainsGetter DomainsGetter + SubscriptionsGetter + SubscriptionProvidersGetter } // SmeV1alpha1Client is used to interact with features provided by the sme.sap.com group. @@ -59,6 +61,14 @@ func (c *SmeV1alpha1Client) Domains(namespace string) DomainInterface { return newDomains(c, namespace) } +func (c *SmeV1alpha1Client) Subscriptions(namespace string) SubscriptionInterface { + return newSubscriptions(c, namespace) +} + +func (c *SmeV1alpha1Client) SubscriptionProviders(namespace string) SubscriptionProviderInterface { + return newSubscriptionProviders(c, namespace) +} + // NewForConfig creates a new SmeV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/subscription.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/subscription.go new file mode 100644 index 00000000..082a19f6 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/subscription.go @@ -0,0 +1,63 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + applyconfigurationsmesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// SubscriptionsGetter has a method to return a SubscriptionInterface. +// A group's client should implement this interface. +type SubscriptionsGetter interface { + Subscriptions(namespace string) SubscriptionInterface +} + +// SubscriptionInterface has methods to work with Subscription resources. +type SubscriptionInterface interface { + Create(ctx context.Context, subscription *smesapcomv1alpha1.Subscription, opts v1.CreateOptions) (*smesapcomv1alpha1.Subscription, error) + Update(ctx context.Context, subscription *smesapcomv1alpha1.Subscription, opts v1.UpdateOptions) (*smesapcomv1alpha1.Subscription, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, subscription *smesapcomv1alpha1.Subscription, opts v1.UpdateOptions) (*smesapcomv1alpha1.Subscription, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*smesapcomv1alpha1.Subscription, error) + List(ctx context.Context, opts v1.ListOptions) (*smesapcomv1alpha1.SubscriptionList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *smesapcomv1alpha1.Subscription, err error) + Apply(ctx context.Context, subscription *applyconfigurationsmesapcomv1alpha1.SubscriptionApplyConfiguration, opts v1.ApplyOptions) (result *smesapcomv1alpha1.Subscription, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, subscription *applyconfigurationsmesapcomv1alpha1.SubscriptionApplyConfiguration, opts v1.ApplyOptions) (result *smesapcomv1alpha1.Subscription, err error) + SubscriptionExpansion +} + +// subscriptions implements SubscriptionInterface +type subscriptions struct { + *gentype.ClientWithListAndApply[*smesapcomv1alpha1.Subscription, *smesapcomv1alpha1.SubscriptionList, *applyconfigurationsmesapcomv1alpha1.SubscriptionApplyConfiguration] +} + +// newSubscriptions returns a Subscriptions +func newSubscriptions(c *SmeV1alpha1Client, namespace string) *subscriptions { + return &subscriptions{ + gentype.NewClientWithListAndApply[*smesapcomv1alpha1.Subscription, *smesapcomv1alpha1.SubscriptionList, *applyconfigurationsmesapcomv1alpha1.SubscriptionApplyConfiguration]( + "subscriptions", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *smesapcomv1alpha1.Subscription { return &smesapcomv1alpha1.Subscription{} }, + func() *smesapcomv1alpha1.SubscriptionList { return &smesapcomv1alpha1.SubscriptionList{} }, + ), + } +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/subscriptionprovider.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/subscriptionprovider.go new file mode 100644 index 00000000..6437e765 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/subscriptionprovider.go @@ -0,0 +1,65 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + applyconfigurationsmesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// SubscriptionProvidersGetter has a method to return a SubscriptionProviderInterface. +// A group's client should implement this interface. +type SubscriptionProvidersGetter interface { + SubscriptionProviders(namespace string) SubscriptionProviderInterface +} + +// SubscriptionProviderInterface has methods to work with SubscriptionProvider resources. +type SubscriptionProviderInterface interface { + Create(ctx context.Context, subscriptionProvider *smesapcomv1alpha1.SubscriptionProvider, opts v1.CreateOptions) (*smesapcomv1alpha1.SubscriptionProvider, error) + Update(ctx context.Context, subscriptionProvider *smesapcomv1alpha1.SubscriptionProvider, opts v1.UpdateOptions) (*smesapcomv1alpha1.SubscriptionProvider, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, subscriptionProvider *smesapcomv1alpha1.SubscriptionProvider, opts v1.UpdateOptions) (*smesapcomv1alpha1.SubscriptionProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*smesapcomv1alpha1.SubscriptionProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*smesapcomv1alpha1.SubscriptionProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *smesapcomv1alpha1.SubscriptionProvider, err error) + Apply(ctx context.Context, subscriptionProvider *applyconfigurationsmesapcomv1alpha1.SubscriptionProviderApplyConfiguration, opts v1.ApplyOptions) (result *smesapcomv1alpha1.SubscriptionProvider, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, subscriptionProvider *applyconfigurationsmesapcomv1alpha1.SubscriptionProviderApplyConfiguration, opts v1.ApplyOptions) (result *smesapcomv1alpha1.SubscriptionProvider, err error) + SubscriptionProviderExpansion +} + +// subscriptionProviders implements SubscriptionProviderInterface +type subscriptionProviders struct { + *gentype.ClientWithListAndApply[*smesapcomv1alpha1.SubscriptionProvider, *smesapcomv1alpha1.SubscriptionProviderList, *applyconfigurationsmesapcomv1alpha1.SubscriptionProviderApplyConfiguration] +} + +// newSubscriptionProviders returns a SubscriptionProviders +func newSubscriptionProviders(c *SmeV1alpha1Client, namespace string) *subscriptionProviders { + return &subscriptionProviders{ + gentype.NewClientWithListAndApply[*smesapcomv1alpha1.SubscriptionProvider, *smesapcomv1alpha1.SubscriptionProviderList, *applyconfigurationsmesapcomv1alpha1.SubscriptionProviderApplyConfiguration]( + "subscriptionproviders", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *smesapcomv1alpha1.SubscriptionProvider { return &smesapcomv1alpha1.SubscriptionProvider{} }, + func() *smesapcomv1alpha1.SubscriptionProviderList { + return &smesapcomv1alpha1.SubscriptionProviderList{} + }, + ), + } +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index a4865180..eec009a9 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -56,6 +56,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().ClusterDomains().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("domains"): return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().Domains().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("subscriptions"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().Subscriptions().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("subscriptionproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().SubscriptionProviders().Informer()}, nil } diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go index a49d3d3f..077c78f5 100644 --- a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go @@ -27,6 +27,10 @@ type Interface interface { ClusterDomains() TypedClusterDomainInformer // Domains returns a DomainInformer. Domains() TypedDomainInformer + // Subscriptions returns a SubscriptionInformer. + Subscriptions() TypedSubscriptionInformer + // SubscriptionProviders returns a SubscriptionProviderInformer. + SubscriptionProviders() TypedSubscriptionProviderInformer } type version struct { @@ -74,3 +78,13 @@ func (v *version) ClusterDomains() TypedClusterDomainInformer { func (v *version) Domains() TypedDomainInformer { return &domainInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// Subscriptions returns a TypedSubscriptionInformer. +func (v *version) Subscriptions() TypedSubscriptionInformer { + return &subscriptionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// SubscriptionProviders returns a TypedSubscriptionProviderInformer. +func (v *version) SubscriptionProviders() TypedSubscriptionProviderInformer { + return &subscriptionProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/subscription.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/subscription.go new file mode 100644 index 00000000..548c9c06 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/subscription.go @@ -0,0 +1,197 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apissmesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/listers/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// SubscriptionInformer provides access to a shared informer and lister for +// Subscriptions. Prefer using the type-safe variant (see [TypedSubscriptionInformer]). +type SubscriptionInformer interface { + Informer() cache.SharedIndexInformer + Lister() smesapcomv1alpha1.SubscriptionLister +} + +// TypedSubscriptionInformer provides access to a shared informer and lister for +// Subscriptions, including the type-safe TypedInformer variant. +// It is a superset of SubscriptionInformer. +type TypedSubscriptionInformer interface { + Informer() cache.SharedIndexInformer + TypedInformer() SubscriptionIndexInformer + Lister() smesapcomv1alpha1.SubscriptionLister +} + +// SubscriptionIndexInformer is a wrapper around the underlying [cache.SharedIndexInformer] +// with type-safe variants of several methods. +type SubscriptionIndexInformer cache.TypedSharedIndexInformer[*apissmesapcomv1alpha1.Subscription] + +// SubscriptionHandlerFuncs is a specialization of [cache.TypedResourceEventHandlerFuncs] for Subscription. +type SubscriptionHandlerFuncs = cache.TypedResourceEventHandlerFuncs[*apissmesapcomv1alpha1.Subscription] + +// SubscriptionDetailedHandlerFuncs is a specialization of [cache.TypedResourceEventHandlerDetailedFuncs] for Subscription. +type SubscriptionDetailedHandlerFuncs = cache.TypedResourceEventHandlerDetailedFuncs[*apissmesapcomv1alpha1.Subscription] + +// SubscriptionFilteringHandler is a specialization of [cache.TypedFilteringResourceEventHandler] for Subscription. +type SubscriptionFilteringHandler = cache.TypedFilteringResourceEventHandler[*apissmesapcomv1alpha1.Subscription] + +// SubscriptionIndexers is a specialization of [cache.TypedIndexers] for Subscription. +type SubscriptionIndexers = cache.TypedIndexers[*apissmesapcomv1alpha1.Subscription] + +// DeletedSubscription is a specialization of [cache.DeletedObject] for Subscription. +type DeletedSubscription = cache.DeletedObject[*apissmesapcomv1alpha1.Subscription] + +type subscriptionInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewSubscriptionInformer constructs a new informer for Subscription type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedSubscriptionInformer]). +func NewSubscriptionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewSubscriptionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) +} + +// NewTypedSubscriptionInformer constructs a new informer for Subscription type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedSubscriptionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers SubscriptionIndexers) SubscriptionIndexInformer { + return NewTypedSubscriptionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.TypedIndexersToIndexers(indexers)}) +} + +// NewFilteredSubscriptionInformer constructs a new informer for Subscription type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedFilteredSubscriptionInformer]). +func NewFilteredSubscriptionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return NewTypedSubscriptionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewTypedFilteredSubscriptionInformer constructs a new informer for Subscription type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedFilteredSubscriptionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers SubscriptionIndexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) SubscriptionIndexInformer { + return NewTypedSubscriptionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.TypedIndexersToIndexers(indexers), TweakListOptions: tweakListOptions}) +} + +// NewSubscriptionInformerWithOptions constructs a new informer for Subscription type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedSubscriptionInformerWithOptions]). +func NewSubscriptionInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + return NewTypedSubscriptionInformerWithOptions(client, namespace, options) +} + +// NewTypedSubscriptionInformerWithOptions constructs a new informer for Subscription type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedSubscriptionInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) SubscriptionIndexInformer { + gvr := schema.GroupVersionResource{Group: "sme.sap.com", Version: "v1alpha1", Resource: "subscriptions"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.Subscription](cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().Subscriptions(namespace).List(context.Background(), opts) + }, + WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().Subscriptions(namespace).Watch(context.Background(), opts) + }, + ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().Subscriptions(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().Subscriptions(namespace).Watch(ctx, opts) + }, + }, client), + &apissmesapcomv1alpha1.Subscription{}, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, + )) +} + +func (f *subscriptionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewTypedSubscriptionInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) +} + +func (f *subscriptionInformer) Informer() cache.SharedIndexInformer { + return f.TypedInformer() +} + +func (f *subscriptionInformer) TypedInformer() SubscriptionIndexInformer { + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.Subscription](f.factory.InformerFor(&apissmesapcomv1alpha1.Subscription{}, f.defaultInformer)) +} + +func (f *subscriptionInformer) Lister() smesapcomv1alpha1.SubscriptionLister { + return smesapcomv1alpha1.NewSubscriptionLister(f.Informer().GetIndexer()) +} + +// ToTypedSubscriptionInformer converts an untyped informer into a TypedSubscriptionInformer. +// +// WARNING: this conversion is only safe if the informer handles objects of type +// *Subscription. If that is not the case, calling type-safe methods of the returned +// TypedSubscriptionInformer leads to runtime panics. A safer alternative is to pass +// around a TypedSubscriptionInformer instances that was obtained from a +// SharedInformerFactory. +func ToTypedSubscriptionInformer(informer SubscriptionInformer) TypedSubscriptionInformer { + if informer, ok := informer.(TypedSubscriptionInformer); ok { + return informer + } + return &subscriptionTypedInformerAdapter{informer} +} + +type subscriptionTypedInformerAdapter struct { + SubscriptionInformer +} + +func (a *subscriptionTypedInformerAdapter) TypedInformer() SubscriptionIndexInformer { + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.Subscription](a.Informer()) +} + +// ToSubscriptionIndexInformer converts an untyped informer into a SubscriptionIndexInformer. +// +// WARNING: this conversion is only safe if the informer handles objects of type +// *Subscription. If that is not the case, calling type-safe methods of the returned +// SubscriptionIndexInformer leads to runtime panics. A safer alternative is to pass +// around a SubscriptionIndexInformer instances that was obtained from a +// SharedInformerFactory. +func ToSubscriptionIndexInformer(informer cache.SharedIndexInformer) SubscriptionIndexInformer { + if informer, ok := informer.(SubscriptionIndexInformer); ok { + return informer + } + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.Subscription](informer) +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/subscriptionprovider.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/subscriptionprovider.go new file mode 100644 index 00000000..0f7e8af5 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/subscriptionprovider.go @@ -0,0 +1,197 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apissmesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/listers/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// SubscriptionProviderInformer provides access to a shared informer and lister for +// SubscriptionProviders. Prefer using the type-safe variant (see [TypedSubscriptionProviderInformer]). +type SubscriptionProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() smesapcomv1alpha1.SubscriptionProviderLister +} + +// TypedSubscriptionProviderInformer provides access to a shared informer and lister for +// SubscriptionProviders, including the type-safe TypedInformer variant. +// It is a superset of SubscriptionProviderInformer. +type TypedSubscriptionProviderInformer interface { + Informer() cache.SharedIndexInformer + TypedInformer() SubscriptionProviderIndexInformer + Lister() smesapcomv1alpha1.SubscriptionProviderLister +} + +// SubscriptionProviderIndexInformer is a wrapper around the underlying [cache.SharedIndexInformer] +// with type-safe variants of several methods. +type SubscriptionProviderIndexInformer cache.TypedSharedIndexInformer[*apissmesapcomv1alpha1.SubscriptionProvider] + +// SubscriptionProviderHandlerFuncs is a specialization of [cache.TypedResourceEventHandlerFuncs] for SubscriptionProvider. +type SubscriptionProviderHandlerFuncs = cache.TypedResourceEventHandlerFuncs[*apissmesapcomv1alpha1.SubscriptionProvider] + +// SubscriptionProviderDetailedHandlerFuncs is a specialization of [cache.TypedResourceEventHandlerDetailedFuncs] for SubscriptionProvider. +type SubscriptionProviderDetailedHandlerFuncs = cache.TypedResourceEventHandlerDetailedFuncs[*apissmesapcomv1alpha1.SubscriptionProvider] + +// SubscriptionProviderFilteringHandler is a specialization of [cache.TypedFilteringResourceEventHandler] for SubscriptionProvider. +type SubscriptionProviderFilteringHandler = cache.TypedFilteringResourceEventHandler[*apissmesapcomv1alpha1.SubscriptionProvider] + +// SubscriptionProviderIndexers is a specialization of [cache.TypedIndexers] for SubscriptionProvider. +type SubscriptionProviderIndexers = cache.TypedIndexers[*apissmesapcomv1alpha1.SubscriptionProvider] + +// DeletedSubscriptionProvider is a specialization of [cache.DeletedObject] for SubscriptionProvider. +type DeletedSubscriptionProvider = cache.DeletedObject[*apissmesapcomv1alpha1.SubscriptionProvider] + +type subscriptionProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewSubscriptionProviderInformer constructs a new informer for SubscriptionProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedSubscriptionProviderInformer]). +func NewSubscriptionProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewSubscriptionProviderInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) +} + +// NewTypedSubscriptionProviderInformer constructs a new informer for SubscriptionProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedSubscriptionProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers SubscriptionProviderIndexers) SubscriptionProviderIndexInformer { + return NewTypedSubscriptionProviderInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.TypedIndexersToIndexers(indexers)}) +} + +// NewFilteredSubscriptionProviderInformer constructs a new informer for SubscriptionProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedFilteredSubscriptionProviderInformer]). +func NewFilteredSubscriptionProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return NewTypedSubscriptionProviderInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewTypedFilteredSubscriptionProviderInformer constructs a new informer for SubscriptionProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedFilteredSubscriptionProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers SubscriptionProviderIndexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) SubscriptionProviderIndexInformer { + return NewTypedSubscriptionProviderInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.TypedIndexersToIndexers(indexers), TweakListOptions: tweakListOptions}) +} + +// NewSubscriptionProviderInformerWithOptions constructs a new informer for SubscriptionProvider type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedSubscriptionProviderInformerWithOptions]). +func NewSubscriptionProviderInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + return NewTypedSubscriptionProviderInformerWithOptions(client, namespace, options) +} + +// NewTypedSubscriptionProviderInformerWithOptions constructs a new informer for SubscriptionProvider type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedSubscriptionProviderInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) SubscriptionProviderIndexInformer { + gvr := schema.GroupVersionResource{Group: "sme.sap.com", Version: "v1alpha1", Resource: "subscriptionproviders"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.SubscriptionProvider](cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().SubscriptionProviders(namespace).List(context.Background(), opts) + }, + WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().SubscriptionProviders(namespace).Watch(context.Background(), opts) + }, + ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().SubscriptionProviders(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.SmeV1alpha1().SubscriptionProviders(namespace).Watch(ctx, opts) + }, + }, client), + &apissmesapcomv1alpha1.SubscriptionProvider{}, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, + )) +} + +func (f *subscriptionProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewTypedSubscriptionProviderInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) +} + +func (f *subscriptionProviderInformer) Informer() cache.SharedIndexInformer { + return f.TypedInformer() +} + +func (f *subscriptionProviderInformer) TypedInformer() SubscriptionProviderIndexInformer { + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.SubscriptionProvider](f.factory.InformerFor(&apissmesapcomv1alpha1.SubscriptionProvider{}, f.defaultInformer)) +} + +func (f *subscriptionProviderInformer) Lister() smesapcomv1alpha1.SubscriptionProviderLister { + return smesapcomv1alpha1.NewSubscriptionProviderLister(f.Informer().GetIndexer()) +} + +// ToTypedSubscriptionProviderInformer converts an untyped informer into a TypedSubscriptionProviderInformer. +// +// WARNING: this conversion is only safe if the informer handles objects of type +// *SubscriptionProvider. If that is not the case, calling type-safe methods of the returned +// TypedSubscriptionProviderInformer leads to runtime panics. A safer alternative is to pass +// around a TypedSubscriptionProviderInformer instances that was obtained from a +// SharedInformerFactory. +func ToTypedSubscriptionProviderInformer(informer SubscriptionProviderInformer) TypedSubscriptionProviderInformer { + if informer, ok := informer.(TypedSubscriptionProviderInformer); ok { + return informer + } + return &subscriptionProviderTypedInformerAdapter{informer} +} + +type subscriptionProviderTypedInformerAdapter struct { + SubscriptionProviderInformer +} + +func (a *subscriptionProviderTypedInformerAdapter) TypedInformer() SubscriptionProviderIndexInformer { + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.SubscriptionProvider](a.Informer()) +} + +// ToSubscriptionProviderIndexInformer converts an untyped informer into a SubscriptionProviderIndexInformer. +// +// WARNING: this conversion is only safe if the informer handles objects of type +// *SubscriptionProvider. If that is not the case, calling type-safe methods of the returned +// SubscriptionProviderIndexInformer leads to runtime panics. A safer alternative is to pass +// around a SubscriptionProviderIndexInformer instances that was obtained from a +// SharedInformerFactory. +func ToSubscriptionProviderIndexInformer(informer cache.SharedIndexInformer) SubscriptionProviderIndexInformer { + if informer, ok := informer.(SubscriptionProviderIndexInformer); ok { + return informer + } + return cache.NewTypedSharedIndexInformer[*apissmesapcomv1alpha1.SubscriptionProvider](informer) +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go b/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go index c973f8a2..9ee6f6e4 100644 --- a/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go @@ -62,3 +62,19 @@ type DomainListerExpansion interface{} // DomainNamespaceListerExpansion allows custom methods to be added to // DomainNamespaceLister. type DomainNamespaceListerExpansion interface{} + +// SubscriptionListerExpansion allows custom methods to be added to +// SubscriptionLister. +type SubscriptionListerExpansion interface{} + +// SubscriptionNamespaceListerExpansion allows custom methods to be added to +// SubscriptionNamespaceLister. +type SubscriptionNamespaceListerExpansion interface{} + +// SubscriptionProviderListerExpansion allows custom methods to be added to +// SubscriptionProviderLister. +type SubscriptionProviderListerExpansion interface{} + +// SubscriptionProviderNamespaceListerExpansion allows custom methods to be added to +// SubscriptionProviderNamespaceLister. +type SubscriptionProviderNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/subscription.go b/pkg/client/listers/sme.sap.com/v1alpha1/subscription.go new file mode 100644 index 00000000..97153df1 --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/subscription.go @@ -0,0 +1,59 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// SubscriptionLister helps list Subscriptions. +// All objects returned here must be treated as read-only. +type SubscriptionLister interface { + // List lists all Subscriptions in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*smesapcomv1alpha1.Subscription, err error) + // Subscriptions returns an object that can list and get Subscriptions. + Subscriptions(namespace string) SubscriptionNamespaceLister + SubscriptionListerExpansion +} + +// subscriptionLister implements the SubscriptionLister interface. +type subscriptionLister struct { + listers.ResourceIndexer[*smesapcomv1alpha1.Subscription] +} + +// NewSubscriptionLister returns a new SubscriptionLister. +func NewSubscriptionLister(indexer cache.Indexer) SubscriptionLister { + return &subscriptionLister{listers.New[*smesapcomv1alpha1.Subscription](indexer, smesapcomv1alpha1.Resource("subscription"))} +} + +// Subscriptions returns an object that can list and get Subscriptions. +func (s *subscriptionLister) Subscriptions(namespace string) SubscriptionNamespaceLister { + return subscriptionNamespaceLister{listers.NewNamespaced[*smesapcomv1alpha1.Subscription](s.ResourceIndexer, namespace)} +} + +// SubscriptionNamespaceLister helps list and get Subscriptions. +// All objects returned here must be treated as read-only. +type SubscriptionNamespaceLister interface { + // List lists all Subscriptions in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*smesapcomv1alpha1.Subscription, err error) + // Get retrieves the Subscription from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*smesapcomv1alpha1.Subscription, error) + SubscriptionNamespaceListerExpansion +} + +// subscriptionNamespaceLister implements the SubscriptionNamespaceLister +// interface. +type subscriptionNamespaceLister struct { + listers.ResourceIndexer[*smesapcomv1alpha1.Subscription] +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/subscriptionprovider.go b/pkg/client/listers/sme.sap.com/v1alpha1/subscriptionprovider.go new file mode 100644 index 00000000..5c16618c --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/subscriptionprovider.go @@ -0,0 +1,59 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// SubscriptionProviderLister helps list SubscriptionProviders. +// All objects returned here must be treated as read-only. +type SubscriptionProviderLister interface { + // List lists all SubscriptionProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*smesapcomv1alpha1.SubscriptionProvider, err error) + // SubscriptionProviders returns an object that can list and get SubscriptionProviders. + SubscriptionProviders(namespace string) SubscriptionProviderNamespaceLister + SubscriptionProviderListerExpansion +} + +// subscriptionProviderLister implements the SubscriptionProviderLister interface. +type subscriptionProviderLister struct { + listers.ResourceIndexer[*smesapcomv1alpha1.SubscriptionProvider] +} + +// NewSubscriptionProviderLister returns a new SubscriptionProviderLister. +func NewSubscriptionProviderLister(indexer cache.Indexer) SubscriptionProviderLister { + return &subscriptionProviderLister{listers.New[*smesapcomv1alpha1.SubscriptionProvider](indexer, smesapcomv1alpha1.Resource("subscriptionprovider"))} +} + +// SubscriptionProviders returns an object that can list and get SubscriptionProviders. +func (s *subscriptionProviderLister) SubscriptionProviders(namespace string) SubscriptionProviderNamespaceLister { + return subscriptionProviderNamespaceLister{listers.NewNamespaced[*smesapcomv1alpha1.SubscriptionProvider](s.ResourceIndexer, namespace)} +} + +// SubscriptionProviderNamespaceLister helps list and get SubscriptionProviders. +// All objects returned here must be treated as read-only. +type SubscriptionProviderNamespaceLister interface { + // List lists all SubscriptionProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*smesapcomv1alpha1.SubscriptionProvider, err error) + // Get retrieves the SubscriptionProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*smesapcomv1alpha1.SubscriptionProvider, error) + SubscriptionProviderNamespaceListerExpansion +} + +// subscriptionProviderNamespaceLister implements the SubscriptionProviderNamespaceLister +// interface. +type subscriptionProviderNamespaceLister struct { + listers.ResourceIndexer[*smesapcomv1alpha1.SubscriptionProvider] +}