diff --git a/apis/apps/v1alpha1/cluster_types.go b/apis/apps/v1alpha1/cluster_types.go index 795aaa23fb1..769287da0ec 100644 --- a/apis/apps/v1alpha1/cluster_types.go +++ b/apis/apps/v1alpha1/cluster_types.go @@ -76,6 +76,11 @@ type ClusterSpec struct { // +optional Services []ClusterService `json:"services,omitempty"` + // credentials defines the credentials used to access a cluster. + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + Credentials []ClusterCredential `json:"credentials,omitempty"` + // tenancy describes how pods are distributed across node. // SharedNode means multiple pods may share the same node. // DedicatedNode means each pod runs on their own dedicated node. @@ -647,6 +652,35 @@ type ClusterComponentService struct { Annotations map[string]string `json:"annotations,omitempty"` } +type ClusterCredential struct { + // The name of the ConnectionCredential. + // Cannot be updated. + // +required + Name string `json:"name"` + + // ServiceName specifies the name of service to use for accessing the cluster. + // Cannot be updated. + // +optional + ServiceName string `json:"serviceName,omitempty"` + + // PortName specifies the name of the port to access the service. + // If the service has multiple ports, a specific port must be specified to use here. + // Otherwise, the unique port of the service will be used. + // Cannot be updated. + // +optional + PortName string `json:"portName,omitempty"` + + // Cannot be updated. + // +optional + ComponentName string `json:"componentName,omitempty"` + + // AccountName specifies the account used to access the component service. + // If specified, the account must be defined in @SystemAccounts. + // Cannot be updated. + // +optional + AccountName string `json:"accountName,omitempty"` +} + type ClassDefRef struct { // Name refers to the name of the ComponentClassDefinition. // +kubebuilder:validation:MaxLength=63 diff --git a/apis/apps/v1alpha1/zz_generated.deepcopy.go b/apis/apps/v1alpha1/zz_generated.deepcopy.go index c2e83a84926..411a994bcc0 100644 --- a/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -707,6 +707,21 @@ func (in *ClusterComponentVolumeClaimTemplate) DeepCopy() *ClusterComponentVolum return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterCredential) DeepCopyInto(out *ClusterCredential) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCredential. +func (in *ClusterCredential) DeepCopy() *ClusterCredential { + if in == nil { + return nil + } + out := new(ClusterCredential) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterDefinition) DeepCopyInto(out *ClusterDefinition) { *out = *in @@ -1029,6 +1044,11 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = make([]ClusterCredential, len(*in)) + copy(*out, *in) + } if in.Affinity != nil { in, out := &in.Affinity, &out.Affinity *out = new(Affinity) diff --git a/config/crd/bases/apps.kubeblocks.io_clusters.yaml b/config/crd/bases/apps.kubeblocks.io_clusters.yaml index 73e594adf0a..5ac4d490a33 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusters.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusters.yaml @@ -681,6 +681,38 @@ spec: rule: self.all(x, oldSelf.exists(y, y.name == x.name)) - message: component can not be removed dynamically rule: oldSelf.all(x, self.exists(y, y.name == x.name)) + credentials: + description: credentials defines the credentials used to access a + cluster. + items: + properties: + accountName: + description: AccountName specifies the account used to access + the component service. If specified, the account must be defined + in @SystemAccounts. Cannot be updated. + type: string + componentName: + description: Cannot be updated. + type: string + name: + description: The name of the ConnectionCredential. Cannot be + updated. + type: string + portName: + description: PortName specifies the name of the port to access + the service. If the service has multiple ports, a specific + port must be specified to use here. Otherwise, the unique + port of the service will be used. Cannot be updated. + type: string + serviceName: + description: ServiceName specifies the name of service to use + for accessing the cluster. Cannot be updated. + type: string + required: + - name + type: object + type: array + x-kubernetes-preserve-unknown-fields: true monitor: description: monitor specifies the configuration of monitor properties: diff --git a/controllers/apps/cluster_controller.go b/controllers/apps/cluster_controller.go index 011771ad7dc..832ca3fcf5a 100644 --- a/controllers/apps/cluster_controller.go +++ b/controllers/apps/cluster_controller.go @@ -176,8 +176,8 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct &ClusterAPINormalizationTransformer{}, // handle cluster services &ClusterServiceTransformer{}, - // TODO(component): create default cluster connection credential secret object - &ClusterCredentialTransformer{Client: r.Client}, + // create default cluster connection credential secret object + &ClusterCredentialTransformer{}, // TODO(component): handle restore before ClusterComponentTransformer &RestoreTransformer{Client: r.Client}, // create all cluster components objects diff --git a/controllers/apps/transformer_cluster_credential.go b/controllers/apps/transformer_cluster_credential.go index 5b1dff183e5..4ebddd2c480 100644 --- a/controllers/apps/transformer_cluster_credential.go +++ b/controllers/apps/transformer_cluster_credential.go @@ -20,67 +20,233 @@ along with this program. If not, see . package apps import ( + "fmt" + "reflect" + + "golang.org/x/exp/maps" corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/constant" "github.com/apecloud/kubeblocks/pkg/controller/component" "github.com/apecloud/kubeblocks/pkg/controller/factory" "github.com/apecloud/kubeblocks/pkg/controller/graph" "github.com/apecloud/kubeblocks/pkg/controller/model" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) // ClusterCredentialTransformer creates the connection credential secret -type ClusterCredentialTransformer struct { - client.Client -} +type ClusterCredentialTransformer struct{} var _ graph.Transformer = &ClusterCredentialTransformer{} -// Transform creates the connection credential secret -// TODO(xingran): ClusterCredentialTransformer needs to be refactored. It should not depend on clusterDefinition anymore. -func (c *ClusterCredentialTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { +func (t *ClusterCredentialTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { transCtx, _ := ctx.(*clusterTransformContext) - cluster := transCtx.Cluster + if model.IsObjectDeleting(transCtx.OrigCluster) { + return nil + } + + if t.isLegacyCluster(transCtx) { + return t.transformClusterCredentialLegacy(transCtx, dag) + } + return t.transformClusterCredential(transCtx, dag) +} + +func (t *ClusterCredentialTransformer) isLegacyCluster(transCtx *clusterTransformContext) bool { + for _, comp := range transCtx.ComponentSpecs { + compDef, ok := transCtx.ComponentDefs[comp.ComponentDef] + if ok && (len(compDef.UID) > 0 || !compDef.CreationTimestamp.IsZero()) { + return false + } + } + return true +} + +func (t *ClusterCredentialTransformer) transformClusterCredentialLegacy(transCtx *clusterTransformContext, dag *graph.DAG) error { graphCli, _ := transCtx.Client.(model.GraphClient) + synthesizedComponent := t.buildSynthesizedComponentLegacy(transCtx) + if synthesizedComponent != nil { + secret := factory.BuildConnCredential(transCtx.ClusterDef, transCtx.Cluster, synthesizedComponent) + if secret != nil { + graphCli.Create(dag, secret) + } + } + return nil +} - var ( - synthesizedComponent *component.SynthesizedComponent - err error - ) - compSpecMap := cluster.Spec.GetDefNameMappingComponents() +func (t *ClusterCredentialTransformer) buildSynthesizedComponentLegacy(transCtx *clusterTransformContext) *component.SynthesizedComponent { for _, compDef := range transCtx.ClusterDef.Spec.ComponentDefs { if compDef.Service == nil { continue } - reqCtx := intctrlutil.RequestCtx{ - Ctx: transCtx.Context, - Log: log.Log.WithName("cluster"), + for _, compSpec := range transCtx.ComponentSpecs { + if compDef.Name != compSpec.ComponentDefRef { + continue + } + return &component.SynthesizedComponent{ + Name: compSpec.Name, + Services: []corev1.Service{{Spec: compDef.Service.ToSVCSpec()}}, + } + } + } + return nil +} + +func (t *ClusterCredentialTransformer) transformClusterCredential(transCtx *clusterTransformContext, dag *graph.DAG) error { + graphCli, _ := transCtx.Client.(model.GraphClient) + for _, credential := range transCtx.Cluster.Spec.Credentials { + secret, err := t.buildClusterCredential(transCtx, credential) + if err != nil { + return err + } + if err = t.createOrUpdate(transCtx, dag, graphCli, secret); err != nil { + return err + } + } + return nil +} + +func (t *ClusterCredentialTransformer) buildClusterCredential(transCtx *clusterTransformContext, credential appsv1alpha1.ClusterCredential) (*corev1.Secret, error) { + cluster := transCtx.Cluster + secret := factory.BuildConnCredential4Cluster(cluster, credential.Name) + + var compDef *appsv1alpha1.ComponentDefinition + if len(credential.ComponentName) > 0 { + // TODO(component): lookup comp def + } + + data := make(map[string]string) + if len(credential.ServiceName) > 0 { + if err := t.buildServiceEndpoint(cluster, compDef, credential, &data); err != nil { + return nil, err + } + } + if len(credential.ComponentName) > 0 && len(credential.AccountName) > 0 { + if err := t.buildCredential(transCtx, cluster.Namespace, credential, &data); err != nil { + return nil, err + } + } + // TODO(component): define the format of conn-credential secret + secret.StringData = data + + return secret, nil +} + +func (t *ClusterCredentialTransformer) buildServiceEndpoint(cluster *appsv1alpha1.Cluster, compDef *appsv1alpha1.ComponentDefinition, + credential appsv1alpha1.ClusterCredential, data *map[string]string) error { + clusterSvc, compSvc, ports := t.lookupMatchedService(cluster, compDef, credential) + if clusterSvc == nil && compSvc == nil { + return fmt.Errorf("cluster credential references a service which is not definied: %s-%s", cluster.Name, credential.Name) + } + if len(ports) == 0 { + return fmt.Errorf("cluster credential references a service which doesn't define any ports: %s-%s", cluster.Name, credential.Name) + } + if len(credential.PortName) == 0 && len(ports) > 1 { + return fmt.Errorf("cluster credential should specify which port to use for the referenced service: %s-%s", cluster.Name, credential.Name) + } + + if clusterSvc != nil { + t.buildEndpointFromClusterService(credential, clusterSvc, data) + } else { + t.buildEndpointFromComponentService(cluster, credential, compSvc, data) + } + return nil +} + +func (t *ClusterCredentialTransformer) lookupMatchedService(cluster *appsv1alpha1.Cluster, + compDef *appsv1alpha1.ComponentDefinition, credential appsv1alpha1.ClusterCredential) (*appsv1alpha1.ClusterService, *appsv1alpha1.ComponentService, []corev1.ServicePort) { + for i, svc := range cluster.Spec.Services { + if svc.Name == credential.ServiceName { + return &cluster.Spec.Services[i], nil, cluster.Spec.Services[i].Service.Spec.Ports } - comps := compSpecMap[compDef.Name] - if len(comps) > 0 { - synthesizedComponent = &component.SynthesizedComponent{ - Name: comps[0].Name, + } + if len(credential.ComponentName) > 0 && compDef != nil { + for i, svc := range compDef.Spec.Services { + if svc.Name == credential.ServiceName { + return nil, &compDef.Spec.Services[i], compDef.Spec.Services[i].Ports } - } else { - synthesizedComponent, err = component.BuildSynthesizedComponentWrapper(reqCtx, c.Client, cluster, nil) - if err != nil { - return err + } + } + return nil, nil, nil +} + +func (t *ClusterCredentialTransformer) buildEndpointFromClusterService(credential appsv1alpha1.ClusterCredential, + service *appsv1alpha1.ClusterService, data *map[string]string) { + port := int32(0) + if len(credential.PortName) == 0 { + port = service.Service.Spec.Ports[0].Port + } else { + for _, servicePort := range service.Service.Spec.Ports { + if servicePort.Name == credential.PortName { + port = servicePort.Port + break } } - if synthesizedComponent != nil { - synthesizedComponent.Services = []corev1.Service{ - {Spec: compDef.Service.ToSVCSpec()}, + } + // TODO(component): define the service and port pattern + (*data)["service"] = service.Name + (*data)["port"] = fmt.Sprintf("%d", port) +} + +func (t *ClusterCredentialTransformer) buildEndpointFromComponentService(cluster *appsv1alpha1.Cluster, + credential appsv1alpha1.ClusterCredential, service *appsv1alpha1.ComponentService, data *map[string]string) { + // TODO(component): service.ServiceName + serviceName := constant.GenerateComponentServiceEndpoint(cluster.Name, credential.ComponentName, + string(service.ServiceName), cluster.Namespace) + + port := int32(0) + if len(credential.PortName) == 0 { + port = service.Ports[0].Port + } else { + for _, servicePort := range service.Ports { + if servicePort.Name == credential.PortName { + port = servicePort.Port + break } - break } } - if synthesizedComponent != nil { - secret := factory.BuildConnCredential(transCtx.ClusterDef, cluster, synthesizedComponent) - if secret != nil { + // TODO(component): define the service and port pattern + (*data)["service"] = serviceName + (*data)["port"] = fmt.Sprintf("%d", port) +} + +func (t *ClusterCredentialTransformer) buildCredential(ctx graph.TransformContext, namespace string, + credential appsv1alpha1.ClusterCredential, data *map[string]string) error { + key := types.NamespacedName{ + Namespace: namespace, + Name: credential.AccountName, // TODO(component): secret name + } + secret := &corev1.Secret{} + if err := ctx.GetClient().Get(ctx.GetContext(), key, secret); err != nil { + return err + } + // TODO: which field should to use from accounts? + maps.Copy(*data, secret.StringData) + return nil +} + +func (t *ClusterCredentialTransformer) createOrUpdate(ctx graph.TransformContext, + dag *graph.DAG, graphCli model.GraphClient, secret *corev1.Secret) error { + key := types.NamespacedName{ + Namespace: secret.Namespace, + Name: secret.Name, + } + obj := &corev1.Secret{} + if err := ctx.GetClient().Get(ctx.GetContext(), key, obj); err != nil { + if apierrors.IsNotFound(err) { graphCli.Create(dag, secret) + return nil } + return err + } + objCopy := obj.DeepCopy() + objCopy.Immutable = secret.Immutable + objCopy.Data = secret.Data + objCopy.StringData = secret.StringData + objCopy.Type = secret.Type + if !reflect.DeepEqual(obj, objCopy) { + graphCli.Update(dag, obj, objCopy) } return nil } diff --git a/controllers/apps/transformer_component_credential.go b/controllers/apps/transformer_component_credential.go index 1977ae507ce..4b53fbbe594 100644 --- a/controllers/apps/transformer_component_credential.go +++ b/controllers/apps/transformer_component_credential.go @@ -113,7 +113,7 @@ func (t *ComponentCredentialTransformer) buildFromServiceAndAccount(ctx graph.Tr return err } } - // TODO: define the format of conn-credential secret + // TODO(component): define the format of conn-credential secret secret.StringData = data return nil } @@ -146,7 +146,7 @@ func (t *ComponentCredentialTransformer) buildEndpoint(synthesizeComp *component func (t *ComponentCredentialTransformer) buildEndpointFromService(synthesizeComp *component.SynthesizedComponent, credential appsv1alpha1.ConnectionCredential, service *appsv1alpha1.ComponentService, data *map[string]string) { - // TODO: service.ServiceName + // TODO(component): service.ServiceName serviceName := constant.GenerateComponentServiceEndpoint(synthesizeComp.ClusterName, synthesizeComp.Name, string(service.ServiceName), synthesizeComp.Namespace) @@ -162,7 +162,7 @@ func (t *ComponentCredentialTransformer) buildEndpointFromService(synthesizeComp } } - // TODO: define the service and port pattern + // TODO(component): define the service and port pattern (*data)["service"] = serviceName (*data)["port"] = fmt.Sprintf("%d", port) } @@ -171,13 +171,13 @@ func (t *ComponentCredentialTransformer) buildCredential(ctx graph.TransformCont namespace, accountName string, data *map[string]string) error { key := types.NamespacedName{ Namespace: namespace, - Name: accountName, + Name: accountName, // TODO(component): secret name } secret := &corev1.Secret{} if err := ctx.GetClient().Get(ctx.GetContext(), key, secret); err != nil { return err } - // TODO: which field should to use from accounts? + // TODO(component): which field should to use from accounts? maps.Copy(*data, secret.StringData) return nil } diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml index 73e594adf0a..5ac4d490a33 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml @@ -681,6 +681,38 @@ spec: rule: self.all(x, oldSelf.exists(y, y.name == x.name)) - message: component can not be removed dynamically rule: oldSelf.all(x, self.exists(y, y.name == x.name)) + credentials: + description: credentials defines the credentials used to access a + cluster. + items: + properties: + accountName: + description: AccountName specifies the account used to access + the component service. If specified, the account must be defined + in @SystemAccounts. Cannot be updated. + type: string + componentName: + description: Cannot be updated. + type: string + name: + description: The name of the ConnectionCredential. Cannot be + updated. + type: string + portName: + description: PortName specifies the name of the port to access + the service. If the service has multiple ports, a specific + port must be specified to use here. Otherwise, the unique + port of the service will be used. Cannot be updated. + type: string + serviceName: + description: ServiceName specifies the name of service to use + for accessing the cluster. Cannot be updated. + type: string + required: + - name + type: object + type: array + x-kubernetes-preserve-unknown-fields: true monitor: description: monitor specifies the configuration of monitor properties: diff --git a/pkg/constant/pattern.go b/pkg/constant/pattern.go index 7c0fa5e6aa7..9ad30360128 100644 --- a/pkg/constant/pattern.go +++ b/pkg/constant/pattern.go @@ -25,7 +25,15 @@ import ( // GenerateDefaultConnCredential generates default connection credential name of cluster func GenerateDefaultConnCredential(clusterName string) string { - return fmt.Sprintf("%s-conn-credential", clusterName) + return GenerateClusterConnCredential(clusterName, "") +} + +// GenerateClusterConnCredential generates connection credential name of cluster +func GenerateClusterConnCredential(clusterName, name string) string { + if len(name) == 0 { + name = "conn-credential" + } + return fmt.Sprintf("%s-%s", clusterName, name) } // GenerateComponentConnCredential generates connection credential name of component diff --git a/pkg/controller/factory/builder.go b/pkg/controller/factory/builder.go index 3ff5984ecde..0860ea3f3b8 100644 --- a/pkg/controller/factory/builder.go +++ b/pkg/controller/factory/builder.go @@ -511,6 +511,12 @@ func BuildConnCredential(clusterDefinition *appsv1alpha1.ClusterDefinition, clus return connCredential } +func BuildConnCredential4Cluster(cluster *appsv1alpha1.Cluster, name string) *corev1.Secret { + secretName := constant.GenerateClusterConnCredential(cluster.Name, name) + labels := constant.GetClusterWellKnownLabels(cluster.Name) + return builder.NewSecretBuilder(cluster.Namespace, secretName).AddLabelsInMap(labels).GetObject() +} + func BuildConnCredential4Component(comp *component.SynthesizedComponent, name string) *corev1.Secret { secretName := constant.GenerateComponentConnCredential(comp.ClusterName, comp.Name, name) labels := constant.GetKBWellKnownLabels(comp.ClusterDefName, comp.ClusterName, comp.Name)