diff --git a/pkg/cmds/mssql.go b/pkg/cmds/mssql.go new file mode 100644 index 000000000..7a5d740b3 --- /dev/null +++ b/pkg/cmds/mssql.go @@ -0,0 +1,180 @@ +/* +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" + "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" + appApi "kmodules.xyz/custom-resources/apis/appcatalog/v1alpha1" + "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) + + 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, 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, 0o644); 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 +} + +func generateMSSQLDAGConfig(ctx context.Context, opts *common.MSSQLOpts) ([]byte, error) { + 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) + + secret, err := opts.Client.CoreV1().Secrets(opts.DB.Namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + 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")...) + } + + 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 +} + +// 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, + } +} + +// 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/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..7a98078fe --- /dev/null +++ b/pkg/common/mssql.go @@ -0,0 +1,85 @@ +/* +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 ( + "context" + "fmt" + + "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 *dboldapi.MSSQLServer + Config *rest.Config + Client *kubernetes.Clientset + DBClient *cs.Clientset + AppcatClient *as.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 + } + + 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 + } + + // IMPORTANT VALIDATION: Check if the database is in a state + // where it has generated the necessary DAG secrets. + 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{ + DB: mssql, + Config: config, + Client: client, + DBClient: dbClient, + AppcatClient: appcatClient, + }, nil +}