Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ dist/
build/

# Entrypoint for the application
!/cmd/d8
!/cmd/d8
159 changes: 159 additions & 0 deletions cmd/commands/cni.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

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 commands

import (
"fmt"
"log"
"strings"
"time"

"github.com/go-logr/logr"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/templates"
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"

"github.com/deckhouse/deckhouse-cli/internal/cni"
)

var (
cniSwitchLong = templates.LongDesc(`
A group of commands to switch the CNI (Container Network Interface) provider in the Deckhouse cluster.

This process is divided into several steps:

- 'd8 cni-switch prepare' - STEP 1. Prepares the cluster for CNI migration.
- 'd8 cni-switch switch' - STEP 2. Performs the actual CNI switch.
- 'd8 cni-switch cleanup' - STEP 3. Cleans up resources if the switch is aborted.
- 'd8 cni-switch rollback' - (Optional) Rollback CNI if the switch is aborted.`)

cniPrepareExample = templates.Examples(`
# Prepare to switch to Cilium CNI
d8 cni-switch prepare --to-cni cilium`)

cniSwitchExample = templates.Examples(`
# Perform the CNI switch after the prepare step is complete
d8 cni-switch switch`)

cniCleanupExample = templates.Examples(`
# Cleanup resources created by the 'prepare' command
d8 cni-switch cleanup`)

cniRollbackExample = templates.Examples(`
# Rollback changes, restore previous CNI, cleanup resources created by the 'prepare' command
d8 cni-switch rollback`)

supportedCNIs = []string{"cilium", "flannel", "simple-bridge"}
)

func NewCniSwitchCommand() *cobra.Command {
log.SetFlags(0)
ctrllog.SetLogger(logr.Discard())

cmd := &cobra.Command{
Use: "cni-switch",
Short: "A group of commands to switch CNI in the cluster",
Long: cniSwitchLong,
}
cmd.PersistentFlags().Duration("timeout", 1*time.Hour, "The timeout for the entire operation (e.g., 40m, 1h)")
cmd.AddCommand(NewCmdCniPrepare())
cmd.AddCommand(NewCmdCniSwitch())
cmd.AddCommand(NewCmdCniCleanup())
cmd.AddCommand(NewCmdCniRollback())
return cmd
}

func NewCmdCniPrepare() *cobra.Command {
cmd := &cobra.Command{
Use: "prepare",
Short: "Prepares the cluster for CNI switching",
Example: cniPrepareExample,
PreRunE: func(cmd *cobra.Command, _ []string) error {
targetCNI, _ := cmd.Flags().GetString("to-cni")
for _, supported := range supportedCNIs {
if strings.ToLower(targetCNI) == supported {
return nil
}
}
return fmt.Errorf(
"invalid --to-cni value %q. Supported values are: %s",
targetCNI,
strings.Join(supportedCNIs, ", "),
)
},

Run: func(cmd *cobra.Command, _ []string) {
targetCNI, _ := cmd.Flags().GetString("to-cni")
timeout, _ := cmd.Flags().GetDuration("timeout")
if err := cni.RunPrepare(targetCNI, timeout); err != nil {
log.Fatalf("❌ Error running prepare command: %v", err)
}
},
}
cmd.Flags().String("to-cni", "", fmt.Sprintf(
"Target CNI provider to switch to. Supported values: %s",
strings.Join(supportedCNIs, ", "),
))
_ = cmd.MarkFlagRequired("to-cni")

return cmd
}

func NewCmdCniSwitch() *cobra.Command {
cmd := &cobra.Command{
Use: "switch",
Short: "Performs the CNI switching",
Example: cniSwitchExample,
Run: func(cmd *cobra.Command, _ []string) {
timeout, _ := cmd.Flags().GetDuration("timeout")
if err := cni.RunSwitch(timeout); err != nil {
log.Fatalf("❌ Error running switch command: %v", err)
}
},
}
return cmd
}

func NewCmdCniCleanup() *cobra.Command {
cmd := &cobra.Command{
Use: "cleanup",
Short: "Cleans up resources created during CNI switching",
Example: cniCleanupExample,
Run: func(cmd *cobra.Command, _ []string) {
timeout, _ := cmd.Flags().GetDuration("timeout")
if err := cni.RunCleanup(timeout); err != nil {
log.Fatalf("❌ Error running cleanup command: %v", err)
}
},
}
return cmd
}

func NewCmdCniRollback() *cobra.Command { // TDEN It needs to be done!
cmd := &cobra.Command{
Use: "rollback",
Short: "Rollback all changes and restore previous CNI",
Example: cniRollbackExample,
Run: func(cmd *cobra.Command, _ []string) {
timeout, _ := cmd.Flags().GetDuration("timeout")
if err := cni.RunRollback(timeout); err != nil {
log.Fatalf("❌ Error running rollback command: %v", err)
}
},
}
return cmd
}
8 changes: 4 additions & 4 deletions cmd/commands/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ import (
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/component-base/logs"
kubecmd "k8s.io/kubectl/pkg/cmd"

"github.com/deckhouse/deckhouse-cli/internal/cni"
)

const (
cmNamespace = "d8-system"
cmName = "debug-container"
cmImageKey = "image"
cmImageKey = "debug-container-image"
)

var d8CommandRegex = regexp.MustCompile("([\"'`])d8 (\\w+)")
Expand Down Expand Up @@ -112,7 +112,7 @@ func getDebugImage(cmd *cobra.Command) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

configMap, err := kubeCl.CoreV1().ConfigMaps(cmNamespace).Get(ctx, cmName, v1.GetOptions{})
configMap, err := kubeCl.CoreV1().ConfigMaps(cni.CMDataNameSpace).Get(ctx, cni.CMDataName, v1.GetOptions{})
if err != nil {
return "", ErrGenericImageFetch
}
Expand Down
1 change: 1 addition & 0 deletions cmd/d8/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func (r *RootCommand) registerCommands() {
r.cmd.AddCommand(commands.NewKubectlCommand())
r.cmd.AddCommand(commands.NewLoginCommand())
r.cmd.AddCommand(commands.NewStrongholdCommand())
r.cmd.AddCommand(commands.NewCniSwitchCommand())
r.cmd.AddCommand(commands.NewHelpJSONCommand(r.cmd))

r.cmd.AddCommand(plugins.NewPluginsCommand(r.logger.Named("plugins-command")))
Expand Down
75 changes: 75 additions & 0 deletions internal/cni/api/v1alpha1/cni_migration_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

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 v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:openapi-gen=true

// CNIMigration is the schema for the CNIMigration API.
// It is a cluster-level resource that serves as the "single source of truth"
// for the entire migration process. It defines the goal (targetCNI)
// and tracks the overall progress across all nodes.
type CNIMigration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// Spec defines the desired state of CNIMigration.
Spec CNIMigrationSpec `json:"spec,omitempty"`
// Status defines the observed state of CNIMigration.
Status CNIMigrationStatus `json:"status,omitempty"`
}

// CNIMigrationSpec defines the desired state of CNIMigration.
// +k8s:deepcopy-gen=true
type CNIMigrationSpec struct {
// TargetCNI is the CNI to switch to.
// Set by the d8 cli utility when starting Phase 1.
TargetCNI string `json:"targetCNI"`
// Phase is the phase controlled by the d8 cli to command the agents.
// Possible values: Prepare, Migrate, Cleanup, Abort.
Phase string `json:"phase"`
}

// CNIMigrationStatus defines the observed state of CNIMigration.
// +k8s:deepcopy-gen=true
type CNIMigrationStatus struct {
// CurrentCNI is the detected CNI from which the switch is being made.
CurrentCNI string `json:"currentCNI,omitempty"`
// NodesTotal is the total number of nodes involved in the migration.
NodesTotal int `json:"nodesTotal,omitempty"`
// NodesSucceeded is the number of nodes that have successfully completed the current phase.
NodesSucceeded int `json:"nodesSucceeded,omitempty"`
// NodesFailed is the number of nodes where an error occurred.
NodesFailed int `json:"nodesFailed,omitempty"`
// Conditions reflect the state of the migration as a whole.
// The d8 cli aggregates statuses from all CNINodeMigrations here.
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// CNIMigrationList contains a list of CNIMigration.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type CNIMigrationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CNIMigration `json:"items"`
}
63 changes: 63 additions & 0 deletions internal/cni/api/v1alpha1/cni_node_migration_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

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 v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:openapi-gen=true

// CNINodeMigration is the schema for the CNINodeMigration API.
// This resource is created for each node in the cluster. The Helper
// agent running on the node updates this resource to report its local progress.
// The d8 cli reads these resources to display detailed status.
type CNINodeMigration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// Spec can be empty, as all configuration is taken from the parent CNIMigration resource.
Spec CNINodeMigrationSpec `json:"spec,omitempty"`
// Status defines the observed state of CNINodeMigration.
Status CNINodeMigrationStatus `json:"status,omitempty"`
}

// CNINodeMigrationSpec defines the desired state of CNINodeMigration.
// +k8s:deepcopy-gen=true
type CNINodeMigrationSpec struct {
// The spec can be empty, as all configuration is taken from the parent CNIMigration resource.
}

// CNINodeMigrationStatus defines the observed state of CNINodeMigration.
// +k8s:deepcopy-gen=true
type CNINodeMigrationStatus struct {
// Phase is the phase of this particular node.
Phase string `json:"phase,omitempty"`
// Conditions are the detailed conditions reflecting the steps performed on the node.
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// CNINodeMigrationList contains a list of CNINodeMigration.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type CNINodeMigrationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CNINodeMigration `json:"items"`
}
50 changes: 50 additions & 0 deletions internal/cni/api/v1alpha1/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

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 v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

const (
APIGroup = "network.deckhouse.io"
APIVersion = "v1alpha1"
)

// SchemeGroupVersion is group version used to register these objects
var (
SchemeGroupVersion = schema.GroupVersion{
Group: APIGroup,
Version: APIVersion,
}
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)

// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&CNIMigration{},
&CNIMigrationList{},
&CNINodeMigration{},
&CNINodeMigrationList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
Loading