Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ package-all: package-linux-amd64 package-linux-arm64
test:
@echo "Running tests..."
@go test -v ./...
@echo "Running local_e2e build-tag tests..."
@go test -v -tags local_e2e ./pkg/aksmachine ./pkg/cmd/daemon

.PHONY: test-coverage
test-coverage:
Expand Down
141 changes: 141 additions & 0 deletions cmd/e2ehelper/daemoncsr/daemoncsr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package daemoncsr

import (
"context"
"fmt"
"strings"
"time"

certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/manager"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

"github.com/spf13/cobra"

"github.com/Azure/unbounded/pkg/agent/daemoncred"
)

const (
defaultBootstrapGroup = "system:bootstrappers:aks-flex-node"
defaultDaemonGroup = "aks-flex-node-daemons"
e2eBootstrapLabel = "aks-flex-node/e2e-daemon-csr"
)

var (
flagKubeconfig string
flagDaemonGroup string
flagBootstrapGroup string
)

var Command = &cobra.Command{
Use: "daemon-csr-approver",
Short: "Approve daemon-controller CSRs for local e2e tests.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return run(cmd.Context())
},
}

func init() {
Command.Flags().StringVar(&flagKubeconfig, "kubeconfig", "", "Path to kubeconfig")
Command.Flags().StringVar(&flagDaemonGroup, "daemon-group", defaultDaemonGroup, "Daemon certificate group")
Command.Flags().StringVar(&flagBootstrapGroup, "bootstrap-group", defaultBootstrapGroup, "Bootstrap requester group")
}

func run(ctx context.Context) error {
cfg, err := clientcmd.BuildConfigFromFlags("", flagKubeconfig)
if err != nil {
return fmt.Errorf("build kubeconfig: %w", err)
}
kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("create Kubernetes client: %w", err)
}
approver, err := daemoncred.NewCSRApprover(daemoncred.CSRApproverOptions{
DaemonGroup: flagDaemonGroup,
BootstrapGroup: flagBootstrapGroup,
AuthorizeBootstrap: func(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, _ string) (bool, error) {
return bootstrapSecretHasE2ELabel(ctx, kubeClient, csr)
},
AuthorizeRenewal: func(context.Context, *certificatesv1.CertificateSigningRequest, string) (bool, error) {
return true, nil
},
})
if err != nil {
return fmt.Errorf("create CSR approver: %w", err)
}
mgr, err := ctrl.NewManager(cfg, manager.Options{
Metrics: metricsserver.Options{BindAddress: "0"},
})
if err != nil {
return fmt.Errorf("create controller manager: %w", err)
}
reconciler, err := daemoncred.NewCSRApproverReconciler(mgr.GetClient(), kubeClient, approver)
if err != nil {
return fmt.Errorf("create CSR approver reconciler: %w", err)
}
if err := reconciler.SetupWithManager(mgr); err != nil {
return fmt.Errorf("setup CSR approver reconciler: %w", err)
}
return mgr.Start(ctx)
}

func bootstrapSecretHasE2ELabel(ctx context.Context, kubeClient kubernetes.Interface, csr *certificatesv1.CertificateSigningRequest) (bool, error) {
tokenID, ok := strings.CutPrefix(csr.Spec.Username, daemoncred.BootstrapUserPrefix)
if !ok || tokenID == "" {
return false, nil
}
secret, err := kubeClient.CoreV1().Secrets(metav1.NamespaceSystem).Get(ctx, "bootstrap-token-"+tokenID, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("get bootstrap token secret: %w", err)
}
return isAuthorizedE2EBootstrapSecret(secret, tokenID, flagBootstrapGroup, time.Now()), nil
}

func isAuthorizedE2EBootstrapSecret(secret *corev1.Secret, tokenID, bootstrapGroup string, now time.Time) bool {
if secret.Type != corev1.SecretTypeBootstrapToken {
return false
}
if !strings.EqualFold(strings.TrimSpace(secret.Labels[e2eBootstrapLabel]), "true") {
return false
}
if strings.TrimSpace(string(secret.Data["token-id"])) != tokenID {
return false
}
if strings.TrimSpace(string(secret.Data["token-secret"])) == "" {
return false
}
if !strings.EqualFold(strings.TrimSpace(string(secret.Data["usage-bootstrap-authentication"])), "true") {
return false
}
if !hasBootstrapGroup(secret.Data["auth-extra-groups"], bootstrapGroup) {
return false
}
return !isExpired(secret.Data["expiration"], now)
}

func hasBootstrapGroup(raw []byte, bootstrapGroup string) bool {
for group := range strings.SplitSeq(string(raw), ",") {
if strings.TrimSpace(group) == bootstrapGroup {
return true
}
}
return false
}

func isExpired(raw []byte, now time.Time) bool {
expiresAtRaw := strings.TrimSpace(string(raw))
if expiresAtRaw == "" {
return false
}
expiresAt, err := time.Parse(time.RFC3339, expiresAtRaw)
if err != nil {
return true
}
return !now.Before(expiresAt)
}
93 changes: 93 additions & 0 deletions cmd/e2ehelper/daemoncsr/daemoncsr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package daemoncsr

import (
"testing"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestIsAuthorizedE2EBootstrapSecret(t *testing.T) {
t.Parallel()

now := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
base := func() *corev1.Secret {
return &corev1.Secret{
Type: corev1.SecretTypeBootstrapToken,
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
e2eBootstrapLabel: "true",
},
},
Data: map[string][]byte{
"token-id": []byte("abc123"),
"token-secret": []byte("secret"),
"expiration": []byte(now.Add(time.Hour).Format(time.RFC3339)),
"usage-bootstrap-authentication": []byte("true"),
"auth-extra-groups": []byte(defaultBootstrapGroup),
},
}
}

tests := map[string]struct {
mutate func(*corev1.Secret)
want bool
}{
"valid": {want: true},
"wrong type": {
mutate: func(secret *corev1.Secret) { secret.Type = corev1.SecretTypeOpaque },
},
"missing label": {
mutate: func(secret *corev1.Secret) { delete(secret.Labels, e2eBootstrapLabel) },
},
"wrong token id": {
mutate: func(secret *corev1.Secret) { secret.Data["token-id"] = []byte("other") },
},
"missing token secret": {
mutate: func(secret *corev1.Secret) { delete(secret.Data, "token-secret") },
},
"auth disabled": {
mutate: func(secret *corev1.Secret) { secret.Data["usage-bootstrap-authentication"] = []byte("false") },
},
"missing bootstrap group": {
mutate: func(secret *corev1.Secret) { secret.Data["auth-extra-groups"] = []byte("system:bootstrappers:other") },
},
"expired": {
mutate: func(secret *corev1.Secret) {
secret.Data["expiration"] = []byte(now.Add(-time.Second).Format(time.RFC3339))
},
},
"malformed expiration": {
mutate: func(secret *corev1.Secret) { secret.Data["expiration"] = []byte("not-a-time") },
},
}

for name, tt := range tests {
name, tt := name, tt
t.Run(name, func(t *testing.T) {
t.Parallel()

secret := base()
if tt.mutate != nil {
tt.mutate(secret)
}

got := isAuthorizedE2EBootstrapSecret(secret, "abc123", defaultBootstrapGroup, now)
if got != tt.want {
t.Fatalf("isAuthorizedE2EBootstrapSecret = %t, want %t", got, tt.want)
}
})
}
}

func TestHasBootstrapGroup(t *testing.T) {
t.Parallel()

if !hasBootstrapGroup([]byte("system:bootstrappers:other, system:bootstrappers:aks-flex-node"), defaultBootstrapGroup) {
t.Fatal("hasBootstrapGroup = false, want true")
}
if hasBootstrapGroup([]byte("system:bootstrappers:other"), defaultBootstrapGroup) {
t.Fatal("hasBootstrapGroup = true, want false")
}
}
85 changes: 65 additions & 20 deletions cmd/e2ehelper/localmachine/localmachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ package localmachine
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/spf13/cobra"

"github.com/Azure/AKSFlexNode/pkg/aksmachine"
"github.com/Azure/AKSFlexNode/pkg/aksmachine/local"
)

const fileMode = 0o600

const localResourceID = "local-test-machine"

var (
flagPath string
flagKubernetesVersion string
Expand Down Expand Up @@ -78,36 +83,45 @@ func init() {
Command.AddCommand(createCmd, getCmd, statusCmd, deleteCmd)
}

func client() (*local.Client, error) {
return local.NewClient(flagPath)
}

func runCreate(ctx context.Context, out io.Writer) error {
c, err := client()
if err != nil {
return err
select {
case <-ctx.Done():
return ctx.Err()
default:
}
machine, err := c.Create(ctx, aksmachine.GoalState{KubernetesVersion: flagKubernetesVersion, SettingsVersion: flagSettingsVersion})
if err != nil {
machine := &aksmachine.Machine{
ID: localResourceID,
Goal: aksmachine.GoalState{
KubernetesVersion: flagKubernetesVersion,
SettingsVersion: flagSettingsVersion,
},
}
if err := writeLocalMachine(flagPath, machine); err != nil {
return err
}
return writeMachine(out, machine)
}

func runGet(ctx context.Context, out io.Writer) error {
c, err := client()
if err != nil {
return err
select {
case <-ctx.Done():
return ctx.Err()
default:
}
machine, err := c.Get(ctx)
machine, err := readLocalMachine(flagPath)
if err != nil {
return err
}
return writeMachine(out, machine)
}

func runStatus(ctx context.Context, out io.Writer) error {
c, err := client()
select {
case <-ctx.Done():
return ctx.Err()
default:
}
machine, err := readLocalMachine(flagPath)
if err != nil {
return err
}
Expand All @@ -116,11 +130,8 @@ func runStatus(ctx context.Context, out io.Writer) error {
ObservedSettingsVersion: flagObservedSettingsVersion,
Message: flagMessage,
}
if err := c.PatchStatus(ctx, status); err != nil {
return err
}
machine, err := c.Get(ctx)
if err != nil {
machine.Status = status
if err := writeLocalMachine(flagPath, machine); err != nil {
return err
}
return writeMachine(out, machine)
Expand All @@ -139,3 +150,37 @@ func writeMachine(out io.Writer, machine *aksmachine.Machine) error {
enc.SetIndent("", " ")
return enc.Encode(machine)
}

func readLocalMachine(path string) (*aksmachine.Machine, error) {
data, err := os.ReadFile(filepath.Clean(path))
if errors.Is(err, os.ErrNotExist) {
return nil, &aksmachine.NotFoundError{Resource: path}
}
if err != nil {
return nil, fmt.Errorf("read machine file %s: %w", path, err)
}

var machine aksmachine.Machine
if err := json.Unmarshal(data, &machine); err != nil {
return nil, fmt.Errorf("decode machine file %s: %w", path, err)
}
return &machine, nil
}

func writeLocalMachine(path string, machine *aksmachine.Machine) error {
if machine == nil {
return fmt.Errorf("machine is nil")
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("create local machine file directory: %w", err)
}
data, err := json.MarshalIndent(machine, "", " ")
if err != nil {
return fmt.Errorf("marshal machine: %w", err)
}
data = append(data, '\n')
if err := os.WriteFile(filepath.Clean(path), data, fileMode); err != nil {
return fmt.Errorf("write machine file %s: %w", path, err)
}
return nil
}
3 changes: 2 additions & 1 deletion cmd/e2ehelper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/spf13/cobra"

"github.com/Azure/AKSFlexNode/cmd/e2ehelper/daemoncsr"
"github.com/Azure/AKSFlexNode/cmd/e2ehelper/localmachine"
)

Expand All @@ -18,7 +19,7 @@ func main() {
Use: "e2ehelper",
Short: "AKS Flex Node E2E helper",
}
rootCmd.AddCommand(localmachine.Command)
rootCmd.AddCommand(localmachine.Command, daemoncsr.Command)

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
Expand Down
Loading
Loading