Skip to content

Commit ad14e6d

Browse files
[deckhouse-cli] system: Add approve / apply-now commands for module releases (#247)
Signed-off-by: Roman Berezkin <[email protected]>
1 parent a792f55 commit ad14e6d

File tree

17 files changed

+2615
-54
lines changed

17 files changed

+2615
-54
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package applynow
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/cobra"
24+
"k8s.io/apimachinery/pkg/api/errors"
25+
"k8s.io/kubectl/pkg/util/templates"
26+
27+
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/operatemodule"
28+
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/v1alpha1"
29+
)
30+
31+
var applyNowLong = templates.LongDesc(`
32+
Apply a module release immediately, bypassing update windows.
33+
34+
This command adds the annotation 'modules.deckhouse.io/apply-now="true"'
35+
to the specified ModuleRelease resource, forcing immediate deployment
36+
regardless of configured update windows or applyAfter schedules.
37+
38+
This command is used for modules with Auto update policy that have
39+
update windows configured or releases scheduled for future deployment.
40+
41+
© Flant JSC 2025`)
42+
43+
var applyNowExample = templates.Examples(`
44+
# Apply a module release immediately
45+
d8 system module apply-now csi-hpe v0.3.10
46+
47+
# Apply without 'v' prefix (will be added automatically)
48+
d8 system module apply-now csi-hpe 0.3.10
49+
`)
50+
51+
func NewCommand() *cobra.Command {
52+
applyNowCmd := &cobra.Command{
53+
Use: "apply-now <module-name> <version>",
54+
Short: "Apply a module release immediately, bypassing update windows.",
55+
Long: applyNowLong,
56+
Example: applyNowExample,
57+
Args: cobra.ExactArgs(2),
58+
ValidArgsFunction: operatemodule.CompleteForApplyNow,
59+
SilenceErrors: true,
60+
SilenceUsage: true,
61+
RunE: applyNowRelease,
62+
}
63+
64+
return applyNowCmd
65+
}
66+
67+
func applyNowRelease(cmd *cobra.Command, args []string) error {
68+
moduleName := args[0]
69+
version := operatemodule.NormalizeVersion(args[1])
70+
71+
dynamicClient, err := operatemodule.GetDynamicClient(cmd)
72+
if err != nil {
73+
return err
74+
}
75+
76+
// Try to get the release
77+
release, err := operatemodule.GetModuleRelease(dynamicClient, moduleName, version)
78+
if err != nil {
79+
if errors.IsNotFound(err) {
80+
return operatemodule.SuggestSuitableReleasesOnNotFound(dynamicClient, moduleName, version, operatemodule.CanBeAppliedNow)
81+
}
82+
return fmt.Errorf("failed to get module release: %w", err)
83+
}
84+
85+
// Check if already has apply-now annotation
86+
if release.IsApplyNow {
87+
fmt.Fprintf(os.Stderr, "%s Module release '%s' already has apply-now annotation.\n", operatemodule.MsgWarn, release.Name)
88+
fmt.Fprintf(os.Stderr, " Phase: %s\n", release.Phase)
89+
if release.Message != "" {
90+
fmt.Fprintf(os.Stderr, " Message: %s\n", release.Message)
91+
}
92+
return nil
93+
}
94+
95+
// Check if the release is in Pending phase
96+
if release.Phase != v1alpha1.ModuleReleasePhasePending {
97+
fmt.Fprintf(os.Stderr, "%s Module release '%s' is not in Pending phase.\n", operatemodule.MsgWarn, release.Name)
98+
fmt.Fprintf(os.Stderr, " Current phase: %s\n", release.Phase)
99+
if release.Message != "" {
100+
fmt.Fprintf(os.Stderr, " Message: %s\n", release.Message)
101+
}
102+
fmt.Fprintln(os.Stderr, "\nOnly releases in 'Pending' phase can be applied.")
103+
return nil
104+
}
105+
106+
// Apply the apply-now annotation
107+
err = operatemodule.ApplyNowModuleRelease(dynamicClient, release.Name)
108+
if err != nil {
109+
return fmt.Errorf("failed to set apply-now annotation: %w", err)
110+
}
111+
fmt.Printf("%s Module release '%s' marked for immediate deployment.\n", operatemodule.MsgOK, release.Name)
112+
fmt.Println(" The release will be applied immediately, bypassing update windows.")
113+
114+
return nil
115+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package approve
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/cobra"
24+
"k8s.io/apimachinery/pkg/api/errors"
25+
"k8s.io/kubectl/pkg/util/templates"
26+
27+
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/operatemodule"
28+
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/v1alpha1"
29+
)
30+
31+
var approveLong = templates.LongDesc(`
32+
Approve a pending module release for installation.
33+
34+
This command adds the annotation 'modules.deckhouse.io/approved="true"'
35+
to the specified ModuleRelease resource, allowing it to be deployed
36+
according to the update policy.
37+
38+
This command is used for modules with Manual update policy that require
39+
explicit approval before deployment.
40+
41+
© Flant JSC 2025`)
42+
43+
var approveExample = templates.Examples(`
44+
# Approve a specific module release
45+
d8 system module approve csi-hpe v0.3.10
46+
47+
# Approve without 'v' prefix (will be added automatically)
48+
d8 system module approve csi-hpe 0.3.10
49+
`)
50+
51+
func NewCommand() *cobra.Command {
52+
approveCmd := &cobra.Command{
53+
Use: "approve <module-name> <version>",
54+
Short: "Approve a pending module release for Manual update policy.",
55+
Long: approveLong,
56+
Example: approveExample,
57+
Args: cobra.ExactArgs(2),
58+
ValidArgsFunction: operatemodule.CompleteForApprove,
59+
SilenceErrors: true,
60+
SilenceUsage: true,
61+
RunE: approveRelease,
62+
}
63+
64+
return approveCmd
65+
}
66+
67+
func approveRelease(cmd *cobra.Command, args []string) error {
68+
moduleName := args[0]
69+
version := operatemodule.NormalizeVersion(args[1])
70+
71+
dynamicClient, err := operatemodule.GetDynamicClient(cmd)
72+
if err != nil {
73+
return err
74+
}
75+
76+
// Try to get the release
77+
release, err := operatemodule.GetModuleRelease(dynamicClient, moduleName, version)
78+
if err != nil {
79+
if errors.IsNotFound(err) {
80+
return operatemodule.SuggestSuitableReleasesOnNotFound(dynamicClient, moduleName, version, operatemodule.CanBeApproved)
81+
}
82+
return fmt.Errorf("failed to get module release: %w", err)
83+
}
84+
85+
// Check if already approved
86+
if release.IsApproved {
87+
fmt.Fprintf(os.Stderr, "%s Module release '%s' is already approved.\n", operatemodule.MsgWarn, release.Name)
88+
fmt.Fprintf(os.Stderr, " Phase: %s\n", release.Phase)
89+
if release.Message != "" {
90+
fmt.Fprintf(os.Stderr, " Message: %s\n", release.Message)
91+
}
92+
return nil
93+
}
94+
95+
// Check if the release is in Pending phase
96+
if release.Phase != v1alpha1.ModuleReleasePhasePending {
97+
fmt.Fprintf(os.Stderr, "%s Module release '%s' is not in Pending phase.\n", operatemodule.MsgWarn, release.Name)
98+
fmt.Fprintf(os.Stderr, " Current phase: %s\n", release.Phase)
99+
if release.Message != "" {
100+
fmt.Fprintf(os.Stderr, " Message: %s\n", release.Message)
101+
}
102+
fmt.Fprintln(os.Stderr, "\nOnly releases in 'Pending' phase can be approved.")
103+
return nil
104+
}
105+
106+
// Apply the approved annotation
107+
err = operatemodule.ApproveModuleRelease(dynamicClient, release.Name)
108+
if err != nil {
109+
return fmt.Errorf("failed to approve module release: %w", err)
110+
}
111+
fmt.Printf("%s Module release '%s' approved.\n", operatemodule.MsgOK, release.Name)
112+
fmt.Println(" The release will be deployed according to the update policy.")
113+
114+
return nil
115+
}

internal/system/cmd/module/disable/disable.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ import (
2020
"fmt"
2121

2222
"github.com/spf13/cobra"
23-
"k8s.io/client-go/dynamic"
2423
"k8s.io/kubectl/pkg/util/templates"
2524

2625
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/operatemodule"
27-
"github.com/deckhouse/deckhouse-cli/internal/utilk8s"
2826
)
2927

3028
var disableLong = templates.LongDesc(`
@@ -51,24 +49,9 @@ func disableModule(cmd *cobra.Command, args []string) error {
5149
}
5250
moduleName := args[0]
5351

54-
kubeconfigPath, err := cmd.Flags().GetString("kubeconfig")
52+
dynamicClient, err := operatemodule.GetDynamicClient(cmd)
5553
if err != nil {
56-
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
57-
}
58-
59-
contextName, err := cmd.Flags().GetString("context")
60-
if err != nil {
61-
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
62-
}
63-
64-
config, _, err := utilk8s.SetupK8sClientSet(kubeconfigPath, contextName)
65-
if err != nil {
66-
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
67-
}
68-
69-
dynamicClient, err := dynamic.NewForConfig(config)
70-
if err != nil {
71-
return fmt.Errorf("Failed to create dynamic client: %v", err)
54+
return err
7255
}
7356

7457
err = operatemodule.OperateModule(dynamicClient, moduleName, operatemodule.ModuleDisabled)

internal/system/cmd/module/enable/enable.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ import (
2020
"fmt"
2121

2222
"github.com/spf13/cobra"
23-
"k8s.io/client-go/dynamic"
2423
"k8s.io/kubectl/pkg/util/templates"
2524

2625
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/operatemodule"
27-
"github.com/deckhouse/deckhouse-cli/internal/utilk8s"
2826
)
2927

3028
var enableLong = templates.LongDesc(`
@@ -51,24 +49,9 @@ func enableModule(cmd *cobra.Command, args []string) error {
5149
}
5250
moduleName := args[0]
5351

54-
kubeconfigPath, err := cmd.Flags().GetString("kubeconfig")
52+
dynamicClient, err := operatemodule.GetDynamicClient(cmd)
5553
if err != nil {
56-
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
57-
}
58-
59-
contextName, err := cmd.Flags().GetString("context")
60-
if err != nil {
61-
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
62-
}
63-
64-
config, _, err := utilk8s.SetupK8sClientSet(kubeconfigPath, contextName)
65-
if err != nil {
66-
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
67-
}
68-
69-
dynamicClient, err := dynamic.NewForConfig(config)
70-
if err != nil {
71-
return fmt.Errorf("Failed to create dynamic client: %v", err)
54+
return err
7255
}
7356

7457
err = operatemodule.OperateModule(dynamicClient, moduleName, operatemodule.ModuleEnabled)

internal/system/cmd/module/module.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"github.com/spf13/cobra"
2121
"k8s.io/kubectl/pkg/util/templates"
2222

23+
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/applynow"
24+
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/approve"
2325
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/disable"
2426
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/enable"
2527
"github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/list"
@@ -39,8 +41,15 @@ func NewCommand() *cobra.Command {
3941
}
4042

4143
moduleCmd.AddCommand(
44+
// Release management (ModuleRelease)
45+
approve.NewCommand(),
46+
applynow.NewCommand(),
47+
48+
// Module state (ModuleConfig)
4249
enable.NewCommand(),
4350
disable.NewCommand(),
51+
52+
// Inspection
4453
list.NewCommand(),
4554
values.NewCommand(),
4655
snapshots.NewCommand(),

0 commit comments

Comments
 (0)