Skip to content

Commit

Permalink
OCM-7264 | feat: Updated to support create kubeletconfig for HCP clus…
Browse files Browse the repository at this point in the history
…ters
  • Loading branch information
robpblake committed May 13, 2024
1 parent 4534b3c commit d0090ba
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 77 deletions.
5 changes: 3 additions & 2 deletions cmd/create/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ func init() {
Cmd.AddCommand(tuningconfigs.Cmd)
Cmd.AddCommand(dnsdomains.Cmd)
Cmd.AddCommand(autoscaler.Cmd)
Cmd.AddCommand(kubeletconfig.Cmd)
kubeletConfig := kubeletconfig.NewCreateKubeletConfigCommand()
Cmd.AddCommand(kubeletConfig)
Cmd.AddCommand(externalauthprovider.Cmd)
Cmd.AddCommand(breakglasscredential.Cmd)

Expand All @@ -77,7 +78,7 @@ func init() {
userrole.Cmd, ocmrole.Cmd,
oidcprovider.Cmd, breakglasscredential.Cmd,
admin.Cmd, autoscaler.Cmd, dnsdomains.Cmd,
externalauthprovider.Cmd, idp.Cmd, kubeletconfig.Cmd, tuningconfigs.Cmd,
externalauthprovider.Cmd, idp.Cmd, kubeletConfig, tuningconfigs.Cmd,
}
arguments.MarkRegionDeprecated(Cmd, globallyAvailableCommands)
}
134 changes: 69 additions & 65 deletions cmd/create/kubeletconfig/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ limitations under the License.
package kubeletconfig

import (
"context"
"fmt"
"os"

cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
"github.com/spf13/cobra"
Expand All @@ -30,88 +30,92 @@ import (
"github.com/openshift/rosa/pkg/rosa"
)

var Cmd = &cobra.Command{
Use: "kubeletconfig",
Aliases: []string{"kubelet-config"},
Short: "Create a custom kubeletconfig for a cluster",
Long: "Create a custom kubeletconfig for a cluster",
Example: ` # Create a custom kubeletconfig with a pod-pids-limit of 5000
const (
use = "kubeletconfig"
short = "Create a custom kubeletconfig for a cluster"
long = short
example = ` # Create a custom kubeletconfig with a pod-pids-limit of 5000
rosa create kubeletconfig --cluster=mycluster --pod-pids-limit=5000
`,
Run: run,
Args: cobra.NoArgs,
}
`
)

var args struct {
podPidsLimit int
}
func NewCreateKubeletConfigCommand() *cobra.Command {

options := NewKubeletConfigOptions()
cmd := &cobra.Command{
Use: use,
Aliases: []string{"kubelet-config"},
Short: short,
Long: long,
Example: example,
Run: rosa.DefaultRunner(rosa.RuntimeWithOCM(), CreateKubeletConfigRunner(options)),
Args: cobra.NoArgs,
}

func init() {
flags := Cmd.Flags()
flags.SortFlags = false
flags.IntVar(
&args.podPidsLimit,
PodPidsLimitOption,
PodPidsLimitOptionDefaultValue,
PodPidsLimitOptionUsage)

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

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

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

if cluster.Hypershift().Enabled() {
r.Reporter.Errorf("Hosted Control Plane clusters do not support custom KubeletConfig configuration.")
os.Exit(1)
}
func CreateKubeletConfigRunner(options *KubeletConfigOptions) rosa.CommandRunner {
return func(ctx context.Context, r *rosa.Runtime, command *cobra.Command, args []string) error {
clusterKey := r.GetClusterKey()
cluster, err := r.OCMClient.GetCluster(r.GetClusterKey(), r.Creator)
if err != nil {
return err
}

if cluster.State() != cmv1.ClusterStateReady {
r.Reporter.Errorf("Cluster '%s' is not yet ready. Current state is '%s'", clusterKey, cluster.State())
os.Exit(1)
}
if cluster.State() != cmv1.ClusterStateReady {
return fmt.Errorf("Cluster '%s' is not yet ready. Current state is '%s'", clusterKey, cluster.State())
}

kubeletConfig, err := r.OCMClient.GetClusterKubeletConfig(cluster.ID())
if err != nil {
r.Reporter.Errorf("Failed getting KubeletConfig for cluster '%s': %s",
cluster.ID(), err)
os.Exit(1)
}
if !cluster.Hypershift().Enabled() {
// Classic clusters can only have a single KubeletConfig
kubeletConfig, err := r.OCMClient.GetClusterKubeletConfig(cluster.ID())
if err != nil {
return fmt.Errorf("Failed getting KubeletConfig for cluster '%s': %s",
r.ClusterKey, err)
}

if kubeletConfig != nil {
return fmt.Errorf("A KubeletConfig for cluster '%s' already exists. "+
"You should edit it via 'rosa edit kubeletconfig'", clusterKey)
}
}

if kubeletConfig != nil {
r.Reporter.Errorf("A custom KubeletConfig for cluster '%s' already exists. "+
"You should edit it via 'rosa edit kubeletconfig'", clusterKey)
os.Exit(1)
}
name, err := ValidateOrPromptForName(options.Name)
if err != nil {
return nil
}

requestedPids, err := ValidateOrPromptForRequestedPidsLimit(args.podPidsLimit, clusterKey, nil, r)
if err != nil {
os.Exit(1)
}
requestedPids, err := ValidateOrPromptForRequestedPidsLimit(options.PodPidsLimit, clusterKey, nil, r)
if err != nil {
return err
}

prompt := fmt.Sprintf("Creating 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)
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) {
if !confirm.ConfirmRaw(prompt) {
r.Reporter.Infof("Creation of KubeletConfig for cluster '%s' aborted.", clusterKey)
return nil
}
}

r.Reporter.Debugf("Creating KubeletConfig for cluster '%s'", clusterKey)
kubeletConfigArgs := ocm.KubeletConfigArgs{PodPidsLimit: requestedPids}
kubeletConfigArgs := ocm.KubeletConfigArgs{PodPidsLimit: requestedPids, Name: name}

_, err = r.OCMClient.CreateKubeletConfig(cluster.ID(), kubeletConfigArgs)
if err != nil {
r.Reporter.Errorf("Failed creating custom KubeletConfig for cluster '%s': '%s'",
return fmt.Errorf("Failed creating KubeletConfig for cluster '%s': '%s'",
clusterKey, err)
os.Exit(1)
}
r.Reporter.Infof("Successfully created KubeletConfig for cluster '%s'", clusterKey)

r.Reporter.Infof("Successfully created custom KubeletConfig for cluster '%s'", clusterKey)
os.Exit(0)
return nil
}

r.Reporter.Infof("Creation of custom KubeletConfig for cluster '%s' aborted.", clusterKey)
}
187 changes: 187 additions & 0 deletions cmd/create/kubeletconfig/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package kubeletconfig

import (
"context"
"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("create kubeletconfig", func() {

It("Correctly builds the command", func() {
cmd := NewCreateKubeletConfigCommand()
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("interactive")).NotTo(BeNil())
Expect(cmd.Flags().Lookup(PodPidsLimitOption)).NotTo(BeNil())
Expect(cmd.Flags().Lookup(NameOption)).NotTo(BeNil())
})

Context("CreateKubeletConfig 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(testing.RespondWithJSON(http.StatusOK, FormatClusterList(make([]*cmv1.Cluster, 0))))
t.SetCluster("cluster", nil)

runner := CreateKubeletConfigRunner(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("Returns an error if the cluster is not ready", func() {

cluster := MockCluster(func(c *cmv1.ClusterBuilder) {
c.State(cmv1.ClusterStateInstalling)
})

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

runner := CreateKubeletConfigRunner(NewKubeletConfigOptions())
err := runner(context.Background(), t.RosaRuntime, nil, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(
Equal("Cluster 'cluster' is not yet ready. Current state is 'installing'"))

})

It("Returns an error if a kubeletconfig already exists for classic cluster", func() {
cluster := MockCluster(func(c *cmv1.ClusterBuilder) {
c.State(cmv1.ClusterStateReady)
})

config := MockKubeletConfig(func(k *cmv1.KubeletConfigBuilder) {
k.Name("test").PodPidsLimit(10000).ID("foo")
})

t.ApiServer.AppendHandlers(
testing.RespondWithJSON(
http.StatusOK, FormatClusterList([]*cmv1.Cluster{cluster})))
t.ApiServer.AppendHandlers(
testing.RespondWithJSON(http.StatusOK, FormatResource(config)))
t.SetCluster("cluster", nil)

runner := CreateKubeletConfigRunner(NewKubeletConfigOptions())
err := runner(context.Background(), t.RosaRuntime, nil, nil)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(
Equal("A KubeletConfig for cluster 'cluster' already exists." +
" You should edit it via 'rosa edit kubeletconfig'"))
})

It("Returns an error if it fails to read the kubeletconfig from OCM for classic cluster", func() {

cluster := MockCluster(func(c *cmv1.ClusterBuilder) {
c.State(cmv1.ClusterStateReady)
})

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

runner := CreateKubeletConfigRunner(NewKubeletConfigOptions())
err := runner(context.Background(), t.RosaRuntime, nil, nil)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(
ContainSubstring("Failed getting KubeletConfig for cluster 'cluster'"))
})

It("Creates the KubeletConfig 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("test-id").PodPidsLimit(10000).Name("testing")
})

t.ApiServer.AppendHandlers(
testing.RespondWithJSON(
http.StatusOK, FormatClusterList([]*cmv1.Cluster{cluster})))
t.ApiServer.AppendHandlers(
testing.RespondWithJSON(http.StatusCreated, FormatResource(kubeletConfig)))
t.SetCluster("cluster", nil)

options := NewKubeletConfigOptions()
options.PodPidsLimit = 10000

runner := CreateKubeletConfigRunner(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 created KubeletConfig for cluster 'cluster'\n"))
})

It("Returns an error if failing to create the KubeletConfig 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("test-id").PodPidsLimit(10000).Name("testing")
})

t.ApiServer.AppendHandlers(
testing.RespondWithJSON(
http.StatusOK, FormatClusterList([]*cmv1.Cluster{cluster})))
t.ApiServer.AppendHandlers(
testing.RespondWithJSON(http.StatusBadRequest, FormatResource(kubeletConfig)))
t.SetCluster("cluster", nil)

options := NewKubeletConfigOptions()
options.PodPidsLimit = 10000

runner := CreateKubeletConfigRunner(options)

err := runner(context.Background(), t.RosaRuntime, nil, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Failed creating KubeletConfig for cluster 'cluster':"))
})
})
})
13 changes: 13 additions & 0 deletions cmd/create/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 TestCreateKubeletConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Create KubeletConfig Suite")
}
Loading

0 comments on commit d0090ba

Please sign in to comment.