From 9e06c9e24dce02678394a176acf482e1cd641d0d Mon Sep 17 00:00:00 2001 From: Neaj Morshad Date: Tue, 1 Jul 2025 03:55:36 +0600 Subject: [PATCH 1/6] Add mssql server dag config command Signed-off-by: Neaj Morshad --- pkg/cmds/mssql.go | 151 ++++++++++++++++++++++++++++++++++++++++++++ pkg/cmds/root.go | 6 ++ pkg/common/mssql.go | 61 ++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 pkg/cmds/mssql.go create mode 100644 pkg/common/mssql.go diff --git a/pkg/cmds/mssql.go b/pkg/cmds/mssql.go new file mode 100644 index 000000000..c04cdce09 --- /dev/null +++ b/pkg/cmds/mssql.go @@ -0,0 +1,151 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the AppsCode Community License 1.0.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://github.com/appscode/licenses/raw/1.0.0/AppsCode-Community-1.0.0.md + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmds + +import ( + "context" + "fmt" + "kubedb.dev/cli/pkg/common" + "os" + + "github.com/spf13/cobra" + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + _ "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" + "sigs.k8s.io/yaml" +) + +// NewCmdMSSQL creates the parent `mssql` command +func NewCmdMSSQL(f cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "mssql", + Short: "MSSQLServer database commands", + Long: "Commands for managing KubeDB MSSQLServer instances.", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + cmd.AddCommand(NewCmdDAGConfig(f)) + return cmd +} + +// NewCmdDAGConfig creates the `kubectl dba mssql dag-config` command. +func NewCmdDAGConfig(f cmdutil.Factory) *cobra.Command { + var ( + namespace string + outputDir string + desLong = `Generates a YAML file with the necessary secrets for setting up a MSSQLServer Distributed Availability Group (DAG) remote replica.` + exampleStr = ` # Generate DAG configuration secrets from MSSQLServer 'ag1' in namespace 'demo' + kubectl dba mssql dag-config ag1 -n demo` + ) + + cmd := &cobra.Command{ + Use: "dag-config [mssqlserver-name]", + Short: "Generate Distributed Availability Group configuration from a source MSSQLServer", + Long: desLong, + Example: exampleStr, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + mssqlServerName := args[0] + // Pass the command's context for cancellation handling + cmdutil.CheckErr(runDAGConfig(cmd.Context(), f, namespace, outputDir, mssqlServerName)) + }, + } + + cmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "Namespace of the source MSSQLServer") + cmd.Flags().StringVar(&outputDir, "output-dir", ".", "Directory where the configuration YAML file will be saved") + return cmd +} + +// runDAGConfig is now much simpler. It just orchestrates the steps. +func runDAGConfig(ctx context.Context, f cmdutil.Factory, namespace, outputDir, mssqlServerName string) error { + fmt.Printf("🔎 Generating DAG configuration for MSSQLServer '%s' in namespace '%s'...\n", mssqlServerName, namespace) + + // Use the new common constructor to get a validated options object + opts, err := common.NewMSSQLOpts(f, mssqlServerName, namespace) + if err != nil { + return err // The error from NewMSSQLOpts will be very informative + } + + // Generate the YAML buffer using the opts object + yamlBuffer, err := generateMSSQLDAGConfig(ctx, opts) + if err != nil { + return err + } + + // Write the buffer to a file + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory '%s': %w", outputDir, err) + } + + outputFile := fmt.Sprintf("%s/%s-dag-config.yaml", outputDir, mssqlServerName) + if err := os.WriteFile(outputFile, yamlBuffer, 0644); err != nil { + return fmt.Errorf("failed to write DAG config to file '%s': %w", outputFile, err) + } + + fmt.Printf("Successfully generated DAG configuration.\n") + fmt.Printf("Apply this file in your remote cluster: kubectl apply -f %s\n", outputFile) + + return nil +} + +// generateMSSQLDAGConfig now takes the opts object and is much more robust. +func generateMSSQLDAGConfig(ctx context.Context, opts *common.MSSQLOpts) ([]byte, error) { + // IMPROVEMENT: Get secret names directly from the CR status, not by guessing. + secretNames := []string{ + opts.DB.DbmLoginSecretName(), + opts.DB.MasterKeySecretName(), + opts.DB.EndpointCertSecretName(), + } + + var finalYAML []byte + for _, secretName := range secretNames { + fmt.Printf(" - Fetching secret '%s'...\n", secretName) + // Use the client from the opts object to fetch the secret + secret, err := opts.Client.CoreV1().Secrets(opts.DB.Namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + // No special error handling needed; if it's not found, something is wrong. + return nil, fmt.Errorf("failed to get required secret '%s': %w", secretName, err) + } + + cleanedSecret := cleanupSecretForExport(secret) + secretYAML, err := yaml.Marshal(cleanedSecret) + if err != nil { + return nil, fmt.Errorf("failed to marshal secret '%s' to YAML: %w", secretName, err) + } + finalYAML = append(finalYAML, secretYAML...) + finalYAML = append(finalYAML, []byte("---\n")...) + } + return finalYAML, nil +} + +// cleanupSecretForExport creates a clean, portable version of a Secret. +func cleanupSecretForExport(secret *core.Secret) *core.Secret { + return &core.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name, + Namespace: secret.Namespace, + }, + Data: secret.Data, + Type: secret.Type, + } +} diff --git a/pkg/cmds/root.go b/pkg/cmds/root.go index c8c04997a..a222925bb 100644 --- a/pkg/cmds/root.go +++ b/pkg/cmds/root.go @@ -108,6 +108,12 @@ func NewKubeDBCommand(in io.Reader, out, err io.Writer) *cobra.Command { NewCmdGenApb(f), }, }, + { + Message: "MSSQLServer specific commands", + Commands: []*cobra.Command{ + NewCmdMSSQL(f), + }, + }, { Message: "Metric related CMDs", Commands: []*cobra.Command{ diff --git a/pkg/common/mssql.go b/pkg/common/mssql.go new file mode 100644 index 000000000..2ad0ea3f1 --- /dev/null +++ b/pkg/common/mssql.go @@ -0,0 +1,61 @@ +package common + +import ( + "context" + "fmt" + + // Import the correct MSSQLServer API version + dbapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" + cs "kubedb.dev/apimachinery/client/clientset/versioned" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + cmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +// MSSQLOpts holds clients and the fetched MSSQLServer object for a command. +type MSSQLOpts struct { + DB *dbapi.MSSQLServer + Config *rest.Config + Client *kubernetes.Clientset + DBClient *cs.Clientset +} + +// NewMSSQLOpts creates a new MSSQLOpts instance, fetches the MSSQLServer CR, +// and performs initial validation. +func NewMSSQLOpts(f cmdutil.Factory, dbName, namespace string) (*MSSQLOpts, error) { + config, err := f.ToRESTConfig() + if err != nil { + return nil, err + } + + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + dbClient, err := cs.NewForConfig(config) + if err != nil { + return nil, err + } + + // Fetch the source MSSQLServer custom resource + mssql, err := dbClient.KubedbV1alpha2().MSSQLServers(namespace).Get(context.TODO(), dbName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + // IMPORTANT VALIDATION: Check if the database is in a state + // where it has generated the necessary DAG secrets. + if mssql.Status.Phase != dbapi.DatabasePhaseReady { + return nil, fmt.Errorf("source MSSQLServer %s/%s is not ready (current phase: %s)", namespace, dbName, mssql.Status.Phase) + } + + return &MSSQLOpts{ + DB: mssql, + Config: config, + Client: client, + DBClient: dbClient, + }, nil +} From f8f9697c2f1410420a81d296bc1457c421efef7d Mon Sep 17 00:00:00 2001 From: Neaj Morshad Date: Tue, 1 Jul 2025 04:34:05 +0600 Subject: [PATCH 2/6] Make fmt Signed-off-by: Neaj Morshad --- pkg/cmds/mssql.go | 9 +++++---- pkg/common/mssql.go | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/cmds/mssql.go b/pkg/cmds/mssql.go index c04cdce09..a30ce48ae 100644 --- a/pkg/cmds/mssql.go +++ b/pkg/cmds/mssql.go @@ -19,14 +19,15 @@ package cmds import ( "context" "fmt" - "kubedb.dev/cli/pkg/common" "os" + _ "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" + "kubedb.dev/cli/pkg/common" + "github.com/spf13/cobra" core "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" - _ "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" "sigs.k8s.io/yaml" ) @@ -89,12 +90,12 @@ func runDAGConfig(ctx context.Context, f cmdutil.Factory, namespace, outputDir, } // Write the buffer to a file - if err := os.MkdirAll(outputDir, 0755); err != nil { + if err := os.MkdirAll(outputDir, 0o755); err != nil { return fmt.Errorf("failed to create output directory '%s': %w", outputDir, err) } outputFile := fmt.Sprintf("%s/%s-dag-config.yaml", outputDir, mssqlServerName) - if err := os.WriteFile(outputFile, yamlBuffer, 0644); err != nil { + if err := os.WriteFile(outputFile, yamlBuffer, 0o644); err != nil { return fmt.Errorf("failed to write DAG config to file '%s': %w", outputFile, err) } diff --git a/pkg/common/mssql.go b/pkg/common/mssql.go index 2ad0ea3f1..7a44db7c1 100644 --- a/pkg/common/mssql.go +++ b/pkg/common/mssql.go @@ -5,6 +5,7 @@ import ( "fmt" // Import the correct MSSQLServer API version + dbapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" cs "kubedb.dev/apimachinery/client/clientset/versioned" From f5bd4ba41e14b346a30b3b636c032fa59dd454dc Mon Sep 17 00:00:00 2001 From: Neaj Morshad Date: Tue, 1 Jul 2025 04:36:13 +0600 Subject: [PATCH 3/6] Add license Signed-off-by: Neaj Morshad --- pkg/common/mssql.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/common/mssql.go b/pkg/common/mssql.go index 7a44db7c1..e73135ed9 100644 --- a/pkg/common/mssql.go +++ b/pkg/common/mssql.go @@ -1,3 +1,19 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the AppsCode Community License 1.0.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://github.com/appscode/licenses/raw/1.0.0/AppsCode-Community-1.0.0.md + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package common import ( From 550498d3c0093007335ae67b665e129f88d511b1 Mon Sep 17 00:00:00 2001 From: Neaj Morshad Date: Tue, 1 Jul 2025 17:08:07 +0600 Subject: [PATCH 4/6] Cleanup Signed-off-by: Neaj Morshad --- pkg/cmds/mssql.go | 8 ++------ pkg/common/mssql.go | 3 --- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pkg/cmds/mssql.go b/pkg/cmds/mssql.go index a30ce48ae..efa9c4ea6 100644 --- a/pkg/cmds/mssql.go +++ b/pkg/cmds/mssql.go @@ -75,9 +75,8 @@ func NewCmdDAGConfig(f cmdutil.Factory) *cobra.Command { // runDAGConfig is now much simpler. It just orchestrates the steps. func runDAGConfig(ctx context.Context, f cmdutil.Factory, namespace, outputDir, mssqlServerName string) error { - fmt.Printf("🔎 Generating DAG configuration for MSSQLServer '%s' in namespace '%s'...\n", mssqlServerName, namespace) + fmt.Printf("Generating DAG configuration for MSSQLServer '%s' in namespace '%s'...\n", mssqlServerName, namespace) - // Use the new common constructor to get a validated options object opts, err := common.NewMSSQLOpts(f, mssqlServerName, namespace) if err != nil { return err // The error from NewMSSQLOpts will be very informative @@ -105,9 +104,7 @@ func runDAGConfig(ctx context.Context, f cmdutil.Factory, namespace, outputDir, return nil } -// generateMSSQLDAGConfig now takes the opts object and is much more robust. func generateMSSQLDAGConfig(ctx context.Context, opts *common.MSSQLOpts) ([]byte, error) { - // IMPROVEMENT: Get secret names directly from the CR status, not by guessing. secretNames := []string{ opts.DB.DbmLoginSecretName(), opts.DB.MasterKeySecretName(), @@ -117,10 +114,9 @@ func generateMSSQLDAGConfig(ctx context.Context, opts *common.MSSQLOpts) ([]byte var finalYAML []byte for _, secretName := range secretNames { fmt.Printf(" - Fetching secret '%s'...\n", secretName) - // Use the client from the opts object to fetch the secret + secret, err := opts.Client.CoreV1().Secrets(opts.DB.Namespace).Get(ctx, secretName, metav1.GetOptions{}) if err != nil { - // No special error handling needed; if it's not found, something is wrong. return nil, fmt.Errorf("failed to get required secret '%s': %w", secretName, err) } diff --git a/pkg/common/mssql.go b/pkg/common/mssql.go index e73135ed9..34f86c18d 100644 --- a/pkg/common/mssql.go +++ b/pkg/common/mssql.go @@ -20,8 +20,6 @@ import ( "context" "fmt" - // Import the correct MSSQLServer API version - dbapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" cs "kubedb.dev/apimachinery/client/clientset/versioned" @@ -57,7 +55,6 @@ func NewMSSQLOpts(f cmdutil.Factory, dbName, namespace string) (*MSSQLOpts, erro return nil, err } - // Fetch the source MSSQLServer custom resource mssql, err := dbClient.KubedbV1alpha2().MSSQLServers(namespace).Get(context.TODO(), dbName, metav1.GetOptions{}) if err != nil { return nil, err From 61736f8f9bf36915626795a3709c20ee30beb32a Mon Sep 17 00:00:00 2001 From: Neaj Morshad Date: Tue, 1 Jul 2025 20:02:40 +0600 Subject: [PATCH 5/6] Make appbinding Signed-off-by: Neaj Morshad --- pkg/cmds/mssql.go | 32 ++++++++++++++++++++++++++++++++ pkg/common/mssql.go | 24 ++++++++++++++++-------- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/pkg/cmds/mssql.go b/pkg/cmds/mssql.go index efa9c4ea6..7a5d740b3 100644 --- a/pkg/cmds/mssql.go +++ b/pkg/cmds/mssql.go @@ -28,6 +28,7 @@ import ( core "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" + appApi "kmodules.xyz/custom-resources/apis/appcatalog/v1alpha1" "sigs.k8s.io/yaml" ) @@ -128,6 +129,22 @@ func generateMSSQLDAGConfig(ctx context.Context, opts *common.MSSQLOpts) ([]byte finalYAML = append(finalYAML, secretYAML...) finalYAML = append(finalYAML, []byte("---\n")...) } + + appBindingName := opts.DB.Name + fmt.Printf(" - Fetching AppBinding '%s'...\n", appBindingName) + appBinding, err := opts.AppcatClient.AppcatalogV1alpha1().AppBindings(opts.DB.Namespace).Get(ctx, appBindingName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get AppBinding '%s': %w", appBindingName, err) + } + + cleanedAppBinding := cleanupAppBindingForExport(appBinding) + appBindingYAML, err := yaml.Marshal(cleanedAppBinding) + if err != nil { + return nil, fmt.Errorf("failed to marshal AppBinding '%s' to YAML: %w", appBindingName, err) + } + finalYAML = append(finalYAML, appBindingYAML...) + finalYAML = append(finalYAML, []byte("---\n")...) + return finalYAML, nil } @@ -146,3 +163,18 @@ func cleanupSecretForExport(secret *core.Secret) *core.Secret { Type: secret.Type, } } + +// creates a clean, portable version of an AppBinding. +func cleanupAppBindingForExport(appBinding *appApi.AppBinding) *appApi.AppBinding { + return &appApi.AppBinding{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appApi.SchemeGroupVersion.String(), + Kind: appApi.ResourceKindApp, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: appBinding.Name, + Namespace: appBinding.Namespace, + }, + Spec: appBinding.Spec, + } +} diff --git a/pkg/common/mssql.go b/pkg/common/mssql.go index 34f86c18d..b20272ba8 100644 --- a/pkg/common/mssql.go +++ b/pkg/common/mssql.go @@ -27,14 +27,16 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" cmdutil "k8s.io/kubectl/pkg/cmd/util" + as "kmodules.xyz/custom-resources/client/clientset/versioned" ) // MSSQLOpts holds clients and the fetched MSSQLServer object for a command. type MSSQLOpts struct { - DB *dbapi.MSSQLServer - Config *rest.Config - Client *kubernetes.Clientset - DBClient *cs.Clientset + DB *dbapi.MSSQLServer + Config *rest.Config + Client *kubernetes.Clientset + DBClient *cs.Clientset + AppcatClient *as.Clientset } // NewMSSQLOpts creates a new MSSQLOpts instance, fetches the MSSQLServer CR, @@ -55,6 +57,11 @@ func NewMSSQLOpts(f cmdutil.Factory, dbName, namespace string) (*MSSQLOpts, erro return nil, err } + appcatClient, err := as.NewForConfig(config) + if err != nil { + return nil, err + } + mssql, err := dbClient.KubedbV1alpha2().MSSQLServers(namespace).Get(context.TODO(), dbName, metav1.GetOptions{}) if err != nil { return nil, err @@ -67,9 +74,10 @@ func NewMSSQLOpts(f cmdutil.Factory, dbName, namespace string) (*MSSQLOpts, erro } return &MSSQLOpts{ - DB: mssql, - Config: config, - Client: client, - DBClient: dbClient, + DB: mssql, + Config: config, + Client: client, + DBClient: dbClient, + AppcatClient: appcatClient, }, nil } From 20b935d41bf5d859da437fbb1c9712efb2dd6d09 Mon Sep 17 00:00:00 2001 From: Neaj Morshad Date: Tue, 1 Jul 2025 20:27:10 +0600 Subject: [PATCH 6/6] Only ensure provisioned, no need to be ready Signed-off-by: Neaj Morshad --- pkg/common/mssql.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/common/mssql.go b/pkg/common/mssql.go index b20272ba8..7a98078fe 100644 --- a/pkg/common/mssql.go +++ b/pkg/common/mssql.go @@ -20,19 +20,21 @@ import ( "context" "fmt" - dbapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" + "kubedb.dev/apimachinery/apis/kubedb" + dboldapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2" cs "kubedb.dev/apimachinery/client/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" cmdutil "k8s.io/kubectl/pkg/cmd/util" + cutil "kmodules.xyz/client-go/conditions" as "kmodules.xyz/custom-resources/client/clientset/versioned" ) // MSSQLOpts holds clients and the fetched MSSQLServer object for a command. type MSSQLOpts struct { - DB *dbapi.MSSQLServer + DB *dboldapi.MSSQLServer Config *rest.Config Client *kubernetes.Clientset DBClient *cs.Clientset @@ -69,8 +71,8 @@ func NewMSSQLOpts(f cmdutil.Factory, dbName, namespace string) (*MSSQLOpts, erro // IMPORTANT VALIDATION: Check if the database is in a state // where it has generated the necessary DAG secrets. - if mssql.Status.Phase != dbapi.DatabasePhaseReady { - return nil, fmt.Errorf("source MSSQLServer %s/%s is not ready (current phase: %s)", namespace, dbName, mssql.Status.Phase) + if !cutil.IsConditionTrue(mssql.Status.Conditions, kubedb.DatabaseProvisioned) { + return nil, fmt.Errorf("source MSSQLServer %s/%s has not been successfully provisioned yet. Please wait for the 'Provisioned' condition to be 'True'", namespace, dbName) } return &MSSQLOpts{