Skip to content

Commit

Permalink
OCM-7266 | feat: Added support for delete of KubeletConfig for HCP cl…
Browse files Browse the repository at this point in the history
…usters
  • Loading branch information
robpblake committed May 13, 2024
1 parent 5b3abac commit fb55c25
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 60 deletions.
9 changes: 2 additions & 7 deletions cmd/create/kubeletconfig/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"github.com/spf13/cobra"

"github.com/openshift/rosa/pkg/interactive"
"github.com/openshift/rosa/pkg/interactive/confirm"
. "github.com/openshift/rosa/pkg/kubeletconfig"
"github.com/openshift/rosa/pkg/ocm"
"github.com/openshift/rosa/pkg/rosa"
Expand Down Expand Up @@ -52,7 +51,7 @@ func NewCreateKubeletConfigCommand() *cobra.Command {
Args: cobra.NoArgs,
}

options.AddFlagsToCommand(cmd)
options.AddAllFlags(cmd)
ocm.AddClusterFlag(cmd)
interactive.AddFlag(cmd.Flags())
return cmd
Expand Down Expand Up @@ -97,11 +96,7 @@ func CreateKubeletConfigRunner(options *KubeletConfigOptions) rosa.CommandRunner
if !cluster.Hypershift().Enabled() {
// Creating a KubeletConfig for a classic cluster must prompt the user, as the changes apply
// immediately and cause reboots of the worker nodes in their cluster
prompt := fmt.Sprintf("Creating a KubeletConfig for cluster '%s' will cause all non-Control Plane "+
"nodes to reboot. This may cause outages to your applications. Do you wish to continue?", clusterKey)

if !confirm.ConfirmRaw(prompt) {
r.Reporter.Infof("Creation of KubeletConfig for cluster '%s' aborted.", clusterKey)
if !PromptUserToAcceptWorkerNodeReboot(OperationCreate, r) {
return nil
}
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/dlt/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ func init() {
Cmd.AddCommand(tuningconfigs.Cmd)
Cmd.AddCommand(dnsdomains.Cmd)
Cmd.AddCommand(autoscaler.Cmd)
Cmd.AddCommand(kubeletconfig.Cmd)
kubeletconfig := kubeletconfig.NewDeleteKubeletConfigCommand()
Cmd.AddCommand(kubeletconfig)
Cmd.AddCommand(externalauthprovider.Cmd)

flags := Cmd.PersistentFlags()
Expand All @@ -80,7 +81,7 @@ func init() {
oidcprovider.Cmd, upgrade.Cmd, admin.Cmd,
service.Cmd, autoscaler.Cmd, idp.Cmd,
cluster.Cmd, dnsdomains.Cmd, externalauthprovider.Cmd,
kubeletconfig.Cmd, machinepool.Cmd, tuningconfigs.Cmd,
kubeletconfig, machinepool.Cmd, tuningconfigs.Cmd,
}
arguments.MarkRegionDeprecated(Cmd, globallyAvailableCommands)
}
89 changes: 57 additions & 32 deletions cmd/dlt/kubeletconfig/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,80 @@ limitations under the License.
package kubeletconfig

import (
"context"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/openshift/rosa/pkg/interactive/confirm"
. "github.com/openshift/rosa/pkg/kubeletconfig"
"github.com/openshift/rosa/pkg/ocm"
"github.com/openshift/rosa/pkg/rosa"
)

var Cmd = &cobra.Command{
Use: "kubeletconfig",
Aliases: []string{"kubelet-config"},
Short: "Delete the custom kubeletconfig for a cluster",
Long: "Delete the custom kubeletconfig for a cluster",
Example: ` # Delete the custom kubeletconfig for cluster 'foo'
rosa delete kubeletconfig --cluster foo`,
Run: run,
Args: cobra.NoArgs,
}
const (
use = "kubeletconfig"
short = "Delete a kubeletconfig from a cluster"
long = short
example = ` # Delete the KubeletConfig for ROSA Classic cluster 'foo'
rosa delete kubeletconfig --cluster foo
# Delete the KubeletConfig named 'bar' from cluster 'foo'
rosa delete kubeletconfig --cluster foo --name bar
`
)

func init() {
ocm.AddClusterFlag(Cmd)
confirm.AddFlag(Cmd.Flags())
}
var aliases = []string{"kubelet-config"}

func run(_ *cobra.Command, _ []string) {
r := rosa.NewRuntime().WithOCM()
defer r.Cleanup()
func NewDeleteKubeletConfigCommand() *cobra.Command {

clusterKey := r.GetClusterKey()
cluster := r.FetchCluster()
options := NewKubeletConfigOptions()

r.Reporter.Debugf("Deleting KubeletConfig for cluster '%s'", clusterKey)
var cmd = &cobra.Command{
Use: use,
Aliases: aliases,
Short: short,
Long: long,
Example: example,
Run: rosa.DefaultRunner(rosa.RuntimeWithOCM(), DeleteKubeletConfigRunner(options)),
Args: cobra.NoArgs,
}
ocm.AddClusterFlag(cmd)
confirm.AddFlag(cmd.Flags())
options.AddNameFlag(cmd)
return cmd
}

prompt := fmt.Sprintf("Deleting the custom KubeletConfig for cluster '%s' will cause all non-Control Plane "+
"nodes to reboot. This may cause outages to your applications. Do you wish to continue?", clusterKey)
func DeleteKubeletConfigRunner(options *KubeletConfigOptions) rosa.CommandRunner {
return func(ctx context.Context, r *rosa.Runtime, command *cobra.Command, args []string) error {

if confirm.ConfirmRaw(prompt) {
cluster, err := r.OCMClient.GetCluster(r.GetClusterKey(), r.Creator)
if err != nil {
return err
}

if !cluster.Hypershift().Enabled() {
if !PromptUserToAcceptWorkerNodeReboot(OperationDelete, r) {
return nil
}
}

if cluster.Hypershift().Enabled() {
options.Name, err = PromptForName(options.Name)
if err != nil {
return err
}
options.ValidateForHypershift()
err = r.OCMClient.DeleteKubeletConfigByName(ctx, cluster.ID(), options.Name)
} else {
err = r.OCMClient.DeleteKubeletConfig(ctx, cluster.ID())
}

err := r.OCMClient.DeleteKubeletConfig(cluster.ID())
if err != nil {
r.Reporter.Errorf("Failed to delete custom KubeletConfig for cluster '%s': '%s'",
clusterKey, err)
os.Exit(1)
return fmt.Errorf("Failed to delete KubeletConfig for cluster '%s': '%s'",
r.GetClusterKey(), err)
}
r.Reporter.Infof("Successfully deleted custom KubeletConfig for cluster '%s'", clusterKey)
os.Exit(0)
}

r.Reporter.Infof("Delete of custom KubeletConfig for cluster '%s' aborted.", clusterKey)
r.Reporter.Infof("Successfully deleted KubeletConfig for cluster '%s'", r.GetClusterKey())
return nil
}
}
120 changes: 120 additions & 0 deletions cmd/dlt/kubeletconfig/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package kubeletconfig

import (
"context"
"fmt"
"net/http"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
. "github.com/openshift-online/ocm-sdk-go/testing"

. "github.com/openshift/rosa/pkg/kubeletconfig"
"github.com/openshift/rosa/pkg/output"
. "github.com/openshift/rosa/pkg/test"
)

var _ = Describe("delete kubeletconfig", func() {

It("Correctly builds the command", func() {
cmd := NewDeleteKubeletConfigCommand()
Expect(cmd).NotTo(BeNil())

Expect(cmd.Use).To(Equal(use))
Expect(cmd.Short).To(Equal(short))
Expect(cmd.Long).To(Equal(long))
Expect(cmd.Args).NotTo(BeNil())
Expect(cmd.Run).NotTo(BeNil())

Expect(cmd.Flags().Lookup("cluster")).NotTo(BeNil())
Expect(cmd.Flags().Lookup(PodPidsLimitOption)).To(BeNil())
Expect(cmd.Flags().Lookup(NameOption)).NotTo(BeNil())
})

Context("Delete KubeletConfig Runner", func() {

var t *TestingRuntime

BeforeEach(func() {
t = NewTestRuntime()
output.SetOutput("")
})

AfterEach(func() {
output.SetOutput("")
})

It("Returns an error if the cluster does not exist", func() {
t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, FormatClusterList(make([]*cmv1.Cluster, 0))))
t.SetCluster("cluster", nil)

runner := DeleteKubeletConfigRunner(NewKubeletConfigOptions())
err := runner(context.Background(), t.RosaRuntime, nil, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(
Equal("There is no cluster with identifier or name 'cluster'"))
})

It("Deletes KubeletConfig by name for HCP Clusters", func() {

cluster := MockCluster(func(c *cmv1.ClusterBuilder) {
c.State(cmv1.ClusterStateReady)
b := cmv1.HypershiftBuilder{}
b.Enabled(true)
c.Hypershift(&b)
})

kubeletConfig := MockKubeletConfig(func(k *cmv1.KubeletConfigBuilder) {
k.ID("testing").PodPidsLimit(5000).Name("testing")
})

t.ApiServer.AppendHandlers(
RespondWithJSON(
http.StatusOK, FormatClusterList([]*cmv1.Cluster{cluster})))
t.ApiServer.AppendHandlers(
RespondWithJSON(http.StatusOK, FormatKubeletConfigList([]*cmv1.KubeletConfig{kubeletConfig})))
t.ApiServer.RouteToHandler(http.MethodDelete,
fmt.Sprintf("/api/clusters_mgmt/v1/clusters/%s/kubelet_configs/%s", cluster.ID(), kubeletConfig.ID()),
RespondWithJSON(http.StatusOK, FormatResource(kubeletConfig)))
t.SetCluster("cluster", cluster)

options := NewKubeletConfigOptions()
options.Name = "testing"

runner := DeleteKubeletConfigRunner(options)
t.StdOutReader.Record()

err := runner(context.Background(), t.RosaRuntime, nil, nil)
Expect(err).NotTo(HaveOccurred())

stdOut, _ := t.StdOutReader.Read()
Expect(stdOut).To(Equal("INFO: Successfully deleted KubeletConfig for cluster 'cluster'\n"))
})

It("Fails to delete KubeletConfig by name for HCP Clusters if the KubeletConfig does not exist", func() {
cluster := MockCluster(func(c *cmv1.ClusterBuilder) {
c.State(cmv1.ClusterStateReady)
b := cmv1.HypershiftBuilder{}
b.Enabled(true)
c.Hypershift(&b)
})

t.ApiServer.AppendHandlers(
RespondWithJSON(
http.StatusOK, FormatClusterList([]*cmv1.Cluster{cluster})))
t.ApiServer.AppendHandlers(
RespondWithJSON(http.StatusOK, FormatKubeletConfigList([]*cmv1.KubeletConfig{})))
t.SetCluster("cluster", cluster)

options := NewKubeletConfigOptions()
options.Name = "testing"

runner := DeleteKubeletConfigRunner(options)

err := runner(context.Background(), t.RosaRuntime, nil, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Failed to delete KubeletConfig for cluster 'cluster'"))
})
})
})
13 changes: 13 additions & 0 deletions cmd/dlt/kubeletconfig/kubeletconfig_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package kubeletconfig

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestDeleteKubeletConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Delete KubeletConfig Suite")
}
9 changes: 2 additions & 7 deletions cmd/edit/kubeletconfig/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"github.com/spf13/cobra"

"github.com/openshift/rosa/pkg/interactive"
"github.com/openshift/rosa/pkg/interactive/confirm"
. "github.com/openshift/rosa/pkg/kubeletconfig"
"github.com/openshift/rosa/pkg/ocm"
"github.com/openshift/rosa/pkg/rosa"
Expand Down Expand Up @@ -63,7 +62,7 @@ func NewEditKubeletConfigCommand() *cobra.Command {

ocm.AddClusterFlag(cmd)
interactive.AddFlag(flags)
options.AddFlagsToCommand(cmd)
options.AddAllFlags(cmd)
return cmd
}

Expand Down Expand Up @@ -109,11 +108,7 @@ func EditKubeletConfigRunner(options *KubeletConfigOptions) rosa.CommandRunner {

if !cluster.Hypershift().Enabled() {
// Classic clusters must prompt the user as edit will cause all worker nodes to reboot
prompt := fmt.Sprintf("Updating the custom KubeletConfig for cluster '%s' will cause all non-Control Plane "+
"nodes to reboot. This may cause outages to your applications. Do you wish to continue?", r.GetClusterKey())

if !confirm.ConfirmRaw(prompt) {
r.Reporter.Infof("Update of custom KubeletConfig for cluster '%s' aborted.", r.GetClusterKey())
if !PromptUserToAcceptWorkerNodeReboot(OperationEdit, r) {
return nil
}
}
Expand Down
49 changes: 49 additions & 0 deletions pkg/kubeletconfig/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kubeletconfig

import (
"fmt"

"github.com/openshift/rosa/pkg/interactive/confirm"
"github.com/openshift/rosa/pkg/rosa"
)

const (
promptMessage = "%s the KubeletConfig for cluster '%s' will cause all non-Control Plane " +
"nodes to reboot. This may cause outages to your applications. Do you wish to continue?"
abortMessage = "%s of KubeletConfig for cluster '%s' aborted."
OperationDelete KubeletOperation = "delete"
OperationEdit KubeletOperation = "edit"
OperationCreate KubeletOperation = "create"
)

type KubeletOperation string

var (
singularTense = map[KubeletOperation]string{
OperationEdit: "Edit",
OperationDelete: "Delete",
OperationCreate: "Create",
}

futureTense = map[KubeletOperation]string{
OperationEdit: "Editing",
OperationDelete: "Deleting",
OperationCreate: "Creating",
}
)

func PromptUserToAcceptWorkerNodeReboot(operation KubeletOperation, r *rosa.Runtime) bool {
if !confirm.ConfirmRaw(buildPromptMessage(operation, r.GetClusterKey())) {
r.Reporter.Infof(buildAbortMessage(operation, r.GetClusterKey()))
return false
}
return true
}

func buildAbortMessage(operation KubeletOperation, clusterKey string) string {
return fmt.Sprintf(abortMessage, singularTense[operation], clusterKey)
}

func buildPromptMessage(operation KubeletOperation, clusterKey string) string {
return fmt.Sprintf(promptMessage, futureTense[operation], clusterKey)
}
19 changes: 19 additions & 0 deletions pkg/kubeletconfig/input_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kubeletconfig

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("KubeletConfig Input", func() {
It("Generates the abort message", func() {
msg := buildAbortMessage(OperationCreate, "foo")
Expect(msg).To(Equal("Create of KubeletConfig for cluster 'foo' aborted."))
})

It("Generates the prompt message", func() {
msg := buildPromptMessage(OperationCreate, "foo")
Expect(msg).To(Equal("Creating the KubeletConfig for cluster 'foo' will cause all non-Control Plane " +
"nodes to reboot. This may cause outages to your applications. Do you wish to continue?"))
})
})
Loading

0 comments on commit fb55c25

Please sign in to comment.