Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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: 2 additions & 0 deletions cmd/kubectl-datadog/autoscaling/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/install"
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/uninstall"
)

// options provides information required by cluster command
Expand All @@ -31,6 +32,7 @@ func New(streams genericclioptions.IOStreams) *cobra.Command {
}

cmd.AddCommand(install.New(streams))
cmd.AddCommand(uninstall.New(streams))

o := newOptions(streams)
o.configFlags.AddFlags(cmd.Flags())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,50 @@ func EnsureAwsAuthRole(ctx context.Context, clientset kubernetes.Interface, role

return nil
}

func RemoveAwsAuthRole(ctx context.Context, clientset kubernetes.Interface, roleArn string) error {
cm, err := clientset.CoreV1().ConfigMaps("kube-system").Get(ctx, "aws-auth", metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get aws-auth ConfigMap: %w", err)
}

var roles []RoleMapping
if mapRoles, ok := cm.Data["mapRoles"]; ok {
if err = yaml.Unmarshal([]byte(mapRoles), &roles); err != nil {
return fmt.Errorf("failed to parse mapRoles: %w", err)
}
} else {
log.Printf("No mapRoles found in aws-auth ConfigMap, skipping role removal.")
return nil
}

found := false
updatedRoles := make([]RoleMapping, 0, len(roles))
for _, role := range roles {
if role.RoleArn == roleArn {
found = true
continue
}
updatedRoles = append(updatedRoles, role)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could use slices.DeleteFunc:

oldLen := len(roles)
roles = slices.DeleteFunc(roles, func(role RoleMapping) bool { return role.RoleArn == roleArn })
found := oldLen != len(roles)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if !found {
log.Printf("Role %s not found in aws-auth ConfigMap, skipping removal.", roleArn)
return nil
}

updated, err := yaml.Marshal(updatedRoles)
if err != nil {
return fmt.Errorf("failed to marshal updated mapRoles: %w", err)
}

cm.Data["mapRoles"] = string(updated)

if _, err := clientset.CoreV1().ConfigMaps("kube-system").Update(ctx, cm, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("failed to update aws-auth ConfigMap: %w", err)
}

log.Printf("Removed role %s from aws-auth ConfigMap.", roleArn)

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
)

func CreateOrUpdateStack(ctx context.Context, client *cloudformation.Client, stackName string, templateBody string, params map[string]string) error {
exist, err := doesStackExist(ctx, client, stackName)
exist, err := DoesStackExist(ctx, client, stackName)
if err != nil {
return err
}
Expand All @@ -43,7 +43,8 @@ func CreateOrUpdateStack(ctx context.Context, client *cloudformation.Client, sta
}
}

func doesStackExist(ctx context.Context, client *cloudformation.Client, stackName string) (bool, error) {
// DoesStackExist checks if a CloudFormation stack exists.
func DoesStackExist(ctx context.Context, client *cloudformation.Client, stackName string) (bool, error) {
_, err := client.DescribeStacks(
ctx,
&cloudformation.DescribeStacksInput{
Expand Down Expand Up @@ -165,6 +166,48 @@ func updateStack(ctx context.Context, client *cloudformation.Client, stackName s
return nil
}

func DeleteStack(ctx context.Context, client *cloudformation.Client, stackName string) error {
exist, err := DoesStackExist(ctx, client, stackName)
if err != nil {
return err
}

if !exist {
log.Printf("Stack %s does not exist, skipping deletion.", stackName)
return nil
}

log.Printf("Deleting stack %s…", stackName)

_, err = client.DeleteStack(
ctx,
&cloudformation.DeleteStackInput{
StackName: aws.String(stackName),
},
)
if err != nil {
return fmt.Errorf("failed to delete stack %s: %w", stackName, err)
}

waiter := cloudformation.NewStackDeleteCompleteWaiter(client)
if err := waiter.Wait(
ctx,
&cloudformation.DescribeStacksInput{
StackName: aws.String(stackName),
},
maxWaitDuration,
); err != nil {
log.Printf("Failed to delete stack %s.", stackName)
describeStack(ctx, client, stackName)

return fmt.Errorf("failed to wait for stack %s deletion: %w", stackName, err)
}

log.Printf("Deleted stack %s.", stackName)

return nil
}

func describeStack(ctx context.Context, client *cloudformation.Client, stackName string) error {
out, err := client.DescribeStacks(
ctx,
Expand Down
119 changes: 119 additions & 0 deletions cmd/kubectl-datadog/autoscaling/cluster/common/clients/clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package clients provides shared AWS and Kubernetes client initialization
// for the Karpenter install and uninstall commands.
package clients

import (
"context"
"fmt"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/eks"
"github.com/aws/aws-sdk-go-v2/service/sts"
karpawsv1 "github.com/aws/karpenter-provider-aws/pkg/apis/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1"

"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/install/guess"
)

// Clients holds all AWS and Kubernetes client instances needed for
// Karpenter installation and uninstallation operations.
type Clients struct {
// AWS clients
Config awssdk.Config
CloudFormation *cloudformation.Client
EC2 *ec2.Client
EKS *eks.Client
STS *sts.Client

// Kubernetes clients
K8sClient client.Client // controller-runtime client
K8sClientset *kubernetes.Clientset // typed Kubernetes client
}

// Build creates AWS and Kubernetes clients for Karpenter operations.
func Build(ctx context.Context, configFlags *genericclioptions.ConfigFlags, k8sClientset *kubernetes.Clientset) (*Clients, error) {
awsConfig, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load AWS config: %w", err)
}

sch := runtime.NewScheme()

if err = scheme.AddToScheme(sch); err != nil {
return nil, fmt.Errorf("failed to add base scheme: %w", err)
}

sch.AddKnownTypes(
schema.GroupVersion{Group: "karpenter.sh", Version: "v1"},
&karpv1.NodePool{},
&karpv1.NodePoolList{},
)
metav1.AddToGroupVersion(sch, schema.GroupVersion{Group: "karpenter.sh", Version: "v1"})

sch.AddKnownTypes(
schema.GroupVersion{Group: "karpenter.k8s.aws", Version: "v1"},
&karpawsv1.EC2NodeClass{},
&karpawsv1.EC2NodeClassList{},
)
metav1.AddToGroupVersion(sch, schema.GroupVersion{Group: "karpenter.k8s.aws", Version: "v1"})

restConfig, err := configFlags.ToRESTConfig()
if err != nil {
return nil, fmt.Errorf("failed to get REST config: %w", err)
}

httpClient, err := rest.HTTPClientFor(restConfig)
if err != nil {
return nil, fmt.Errorf("unable to create http client: %w", err)
}

mapper, err := apiutil.NewDynamicRESTMapper(restConfig, httpClient)
if err != nil {
return nil, fmt.Errorf("unable to instantiate mapper: %w", err)
}

k8sClient, err := client.New(restConfig, client.Options{
Scheme: sch,
Mapper: mapper,
})
if err != nil {
return nil, fmt.Errorf("failed to create Karpenter client: %w", err)
}

return &Clients{
Config: awsConfig,
CloudFormation: cloudformation.NewFromConfig(awsConfig),
EC2: ec2.NewFromConfig(awsConfig),
EKS: eks.NewFromConfig(awsConfig),
STS: sts.NewFromConfig(awsConfig),
K8sClient: k8sClient,
K8sClientset: k8sClientset,
}, nil
}

// GetClusterNameFromKubeconfig extracts the EKS cluster name from the current kubeconfig context.
func GetClusterNameFromKubeconfig(ctx context.Context, configFlags *genericclioptions.ConfigFlags) (string, error) {
kubeRawConfig, err := configFlags.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return "", fmt.Errorf("failed to get raw kubeconfig: %w", err)
}

kubeContext := ""
if configFlags.Context != nil {
kubeContext = *configFlags.Context
}

return guess.GetClusterNameFromKubeconfig(ctx, kubeRawConfig, kubeContext), nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Package display provides utilities for formatting text output
// in the kubectl-datadog CLI tool.
package display

import (
"fmt"
"io"
"regexp"
"strings"

"github.com/mattn/go-runewidth"
"github.com/samber/lo"
)

// ansiEscapeRegex matches ANSI escape sequences (e.g., color codes).
var ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)

// stripANSI removes ANSI escape sequences from a string.
func stripANSI(s string) string {
return ansiEscapeRegex.ReplaceAllString(s, "")
}

// visualWidth returns the display width of a string in terminal columns,
// correctly handling ANSI escape sequences and wide Unicode characters.
func visualWidth(s string) int {
return runewidth.StringWidth(stripANSI(s))
}

// PrintBox prints text inside a Unicode box to the given writer.
// For multiple lines, all lines are padded to the width of the longest line.
// This function correctly handles ANSI escape sequences and wide Unicode
// characters (such as emojis and East Asian characters).
func PrintBox(w io.Writer, lines ...string) {
maxWidth := lo.Max(lo.Map(lines, func(line string, _ int) int {
return visualWidth(line)
}))

fmt.Fprintln(w, "╭─"+strings.Repeat("─", maxWidth)+"─╮")
for _, line := range lines {
padding := maxWidth - visualWidth(line)
fmt.Fprintf(w, "│ %s%s │\n", line, strings.Repeat(" ", padding))
}
fmt.Fprintln(w, "╰─"+strings.Repeat("─", maxWidth)+"─╯")
}
Loading
Loading