Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package install
package apply

import "fmt"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package install
package apply

import (
"testing"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package install
package apply

import "fmt"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package install
package apply

import (
"testing"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package install
package apply

import "fmt"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package install
package apply

import (
"testing"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package install
package apply

import (
"bytes"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/genericclioptions"

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

func TestKarpenterHelmValues(t *testing.T) {
Expand Down Expand Up @@ -50,12 +50,10 @@ func TestDisplayForeignKarpenterMessage(t *testing.T) {
t.Setenv("PATH", "")

out := &bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(out)
cmd.SetErr(&bytes.Buffer{})
streams := genericclioptions.IOStreams{Out: out, ErrOut: &bytes.Buffer{}}

foreign := &guess.KarpenterInstallation{Namespace: "karpenter", Name: "karpenter"}
err := displayForeignKarpenterMessage(cmd, "my-cluster", foreign)
err := displayForeignKarpenterMessage(streams, "my-cluster", foreign)
require.NoError(t, err, "foreign Karpenter is a successful no-op, not an error")

rendered := out.String()
Expand Down
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 @@ -8,6 +8,7 @@ import (

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

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

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

o := newOptions(streams)
o.configFlags.AddFlags(cmd.Flags())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
"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"
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/guess"
)

// Clients holds all AWS and Kubernetes client instances needed for
Expand Down Expand Up @@ -142,6 +142,24 @@ func GetClusterNameFromKubeconfig(configFlags *genericclioptions.ConfigFlags) (s
return guess.GetClusterNameFromKubeconfig(kubeRawConfig, kubeContext), nil
}

// ResolveClusterName returns explicit when non-empty, otherwise infers the
// cluster name from the kubeconfig context. Returns an error when neither
// source provides a name so callers do not have to repeat the same fallback
// boilerplate in every cobra command.
func ResolveClusterName(configFlags *genericclioptions.ConfigFlags, explicit string) (string, error) {
if explicit != "" {
return explicit, nil
}
name, err := GetClusterNameFromKubeconfig(configFlags)
if err != nil {
return "", err
}
if name == "" {
return "", errors.New("cluster name must be specified either via --cluster-name or in the current kubeconfig context")
}
return name, nil
}

// getAccountIDFromKubeconfig attempts to extract the AWS account ID from the
// kubeconfig context. Returns an empty string if the context is not an EKS ARN.
func getAccountIDFromKubeconfig(configFlags *genericclioptions.ConfigFlags) (string, error) {
Expand Down
29 changes: 19 additions & 10 deletions cmd/kubectl-datadog/autoscaling/cluster/common/k8s/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,30 @@ import (
"log"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// CreateOrUpdate creates the object if missing, otherwise updates it. The
// Update races against any controller actively reconciling the same resource
// (Karpenter mutates its NodePool / EC2NodeClass when a NodeClaim status
// changes), so an Update can fail with a Conflict whenever the controller has
// bumped the resourceVersion since our Get. Re-fetch and retry on Conflict is
// the standard k8s pattern.
func CreateOrUpdate(ctx context.Context, cli client.Client, object client.Object) error {
resourceVersion, err := getResourceVersion(ctx, cli, object)
if err != nil {
return err
}
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
resourceVersion, err := getResourceVersion(ctx, cli, object)
if err != nil {
return err
}

if resourceVersion != "" {
object.SetResourceVersion(resourceVersion)
return update(ctx, cli, object)
} else {
return create(ctx, cli, object)
}
if resourceVersion != "" {
object.SetResourceVersion(resourceVersion)
return update(ctx, cli, object)
} else {
return create(ctx, cli, object)
}
})
}

func getResourceVersion(ctx context.Context, cli client.Client, object client.Object) (string, error) {
Expand Down
87 changes: 59 additions & 28 deletions cmd/kubectl-datadog/autoscaling/cluster/install/install.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
// Package install provides functionality to install and configure Karpenter
// autoscaling on EKS clusters, including CloudFormation stack creation,
// Helm chart deployment, and resource configuration.
// Package install provides the cobra command that installs Karpenter on an
// EKS cluster. The command is a thin wrapper around the convergence logic in
// the apply package — install binds CLI flags, validates them, and delegates
// the actual deployment work to apply.Run.
package install

import (
"context"
"errors"
"fmt"
"os/signal"
"slices"
"syscall"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/apply"
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/clients"
"github.com/DataDog/datadog-operator/pkg/plugin/common"
)

var (
clusterName string
karpenterNamespace string
karpenterVersion string
installMode = InstallModeFargate
fargateSubnets []string
createKarpenterResources = CreateKarpenterResourcesAll
inferenceMethod = InferenceMethodNodeGroups
debug bool
installExample = `
var installExample = `
# install autoscaling
%[1]s install
`
)

type options struct {
genericclioptions.IOStreams
common.Options
args []string

clusterName string
karpenterNamespace string
karpenterVersion string
installMode apply.InstallMode
fargateSubnets []string
createKarpenterResources apply.CreateKarpenterResources
inferenceMethod apply.InferenceMethod
debug bool
}

func newOptions(streams genericclioptions.IOStreams) *options {
o := &options{
IOStreams: streams,
IOStreams: streams,
installMode: apply.InstallModeFargate,
createKarpenterResources: apply.CreateKarpenterResourcesAll,
inferenceMethod: apply.InferenceMethodNodeGroups,
}
o.SetConfigFlags()
return o
}

// New returns the cobra command for `kubectl datadog autoscaling cluster install`.
func New(streams genericclioptions.IOStreams) *cobra.Command {
o := newOptions(streams)
cmd := &cobra.Command{
Expand All @@ -58,18 +67,18 @@ func New(streams genericclioptions.IOStreams) *cobra.Command {
return err
}

return o.run(c)
return o.run()
},
}

cmd.Flags().StringVar(&clusterName, "cluster-name", "", "Name of the EKS cluster")
cmd.Flags().StringVar(&karpenterNamespace, "karpenter-namespace", "dd-karpenter", "Name of the Kubernetes namespace to deploy Karpenter into")
cmd.Flags().StringVar(&karpenterVersion, "karpenter-version", "", "Version of Karpenter to install (default to latest)")
cmd.Flags().Var(&installMode, "install-mode", "How to run the Karpenter controller: fargate (on dedicated Fargate nodes, default) or existing-nodes (on existing cluster nodes)")
cmd.Flags().StringSliceVar(&fargateSubnets, "fargate-subnets", nil, "Override auto-discovery of private subnets for the Fargate profile (comma-separated subnet IDs). Only used when --install-mode=fargate.")
cmd.Flags().Var(&createKarpenterResources, "create-karpenter-resources", "Which Karpenter resources to create: none, ec2nodeclass, all (default: all)")
cmd.Flags().Var(&inferenceMethod, "inference-method", "Method to infer EC2NodeClass and NodePool properties: nodes, nodegroups")
cmd.Flags().BoolVar(&debug, "debug", false, "Enable debug logs")
cmd.Flags().StringVar(&o.clusterName, "cluster-name", "", "Name of the EKS cluster")
cmd.Flags().StringVar(&o.karpenterNamespace, "karpenter-namespace", "dd-karpenter", "Name of the Kubernetes namespace to deploy Karpenter into")
cmd.Flags().StringVar(&o.karpenterVersion, "karpenter-version", "", "Version of Karpenter to install (default to latest)")
cmd.Flags().Var(&o.installMode, "install-mode", "How to run the Karpenter controller: fargate (on dedicated Fargate nodes, default) or existing-nodes (on existing cluster nodes)")
cmd.Flags().StringSliceVar(&o.fargateSubnets, "fargate-subnets", nil, "Override auto-discovery of private subnets for the Fargate profile (comma-separated subnet IDs). Only used when --install-mode=fargate.")
cmd.Flags().Var(&o.createKarpenterResources, "create-karpenter-resources", "Which Karpenter resources to create: none, ec2nodeclass, all (default: all)")
cmd.Flags().Var(&o.inferenceMethod, "inference-method", "Method to infer EC2NodeClass and NodePool properties: nodes, nodegroups")
cmd.Flags().BoolVar(&o.debug, "debug", false, "Enable debug logs")

o.ConfigFlags.AddFlags(cmd.Flags())

Expand All @@ -88,21 +97,43 @@ func (o *options) validate() error {
return errors.New("no arguments are allowed")
}

if !slices.Contains([]InstallMode{InstallModeFargate, InstallModeExistingNodes}, installMode) {
if !slices.Contains([]apply.InstallMode{apply.InstallModeFargate, apply.InstallModeExistingNodes}, o.installMode) {
return errors.New("install-mode must be one of fargate or existing-nodes")
}

if len(fargateSubnets) > 0 && installMode != InstallModeFargate {
if len(o.fargateSubnets) > 0 && o.installMode != apply.InstallModeFargate {
return errors.New("--fargate-subnets can only be used with --install-mode=fargate")
}

if !slices.Contains([]CreateKarpenterResources{CreateKarpenterResourcesNone, CreateKarpenterResourcesEC2NodeClass, CreateKarpenterResourcesAll}, createKarpenterResources) {
if !slices.Contains([]apply.CreateKarpenterResources{apply.CreateKarpenterResourcesNone, apply.CreateKarpenterResourcesEC2NodeClass, apply.CreateKarpenterResourcesAll}, o.createKarpenterResources) {
return errors.New("create-karpenter-resources must be one of none, ec2nodeclass or all")
}

if !slices.Contains([]InferenceMethod{InferenceMethodNodes, InferenceMethodNodeGroups}, inferenceMethod) {
if !slices.Contains([]apply.InferenceMethod{apply.InferenceMethodNodes, apply.InferenceMethodNodeGroups}, o.inferenceMethod) {
return errors.New("inference-method must be one of nodes or nodegroups")
}

return nil
}

func (o *options) run() error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

clusterName, err := clients.ResolveClusterName(o.ConfigFlags, o.clusterName)
if err != nil {
return err
}

return apply.Run(ctx, o.IOStreams, o.ConfigFlags, o.Clientset, apply.RunOptions{
ClusterName: clusterName,
KarpenterNamespace: o.karpenterNamespace,
KarpenterVersion: o.karpenterVersion,
InstallMode: o.installMode,
FargateSubnets: o.fargateSubnets,
CreateKarpenterResources: o.createKarpenterResources,
InferenceMethod: o.inferenceMethod,
Debug: o.debug,
ActionLabel: "Installing",
})
}
Loading
Loading