Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: use node-ipam-controller in ipv6 metal tests #16946

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
26 changes: 26 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,29 @@ jobs:
with:
name: tests-e2e-scenarios-bare-metal
path: /tmp/artifacts/

tests-e2e-scenarios-bare-metal-ipv6:
runs-on: ubuntu-24.04
timeout-minutes: 70
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
path: ${{ env.GOPATH }}/src/k8s.io/kops

- name: Set up go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed
with:
go-version-file: '${{ env.GOPATH }}/src/k8s.io/kops/go.mod'

- name: tests/e2e/scenarios/bare-metal/run-test
working-directory: ${{ env.GOPATH }}/src/k8s.io/kops
run: |
timeout 60m tests/e2e/scenarios/bare-metal/scenario-ipv6
env:
ARTIFACTS: /tmp/artifacts
- name: Archive production artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: tests-e2e-scenarios-bare-metal-ipv6
path: /tmp/artifacts/
2 changes: 1 addition & 1 deletion cmd/kops-controller/controllers/gceipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewGCEIPAMReconciler(mgr manager.Manager) (*GCEIPAMReconciler, error) {
return r, nil
}

// GCEIPAMReconciler observes Node objects, assigning their`PodCIDRs` from the instance's `ExternalIpv6`.
// GCEIPAMReconciler observes Node objects, assigning their `PodCIDRs` from the instance's `ExternalIpv6`.
type GCEIPAMReconciler struct {
// client is the controller-runtime client
client client.Client
Expand Down
103 changes: 103 additions & 0 deletions cmd/kops-controller/controllers/metalipam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"context"
"fmt"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/klog/v2"
kopsapi "k8s.io/kops/pkg/apis/kops/v1alpha2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
)

// NewMetalIPAMReconciler is the constructor for a MetalIPAMReconciler
func NewMetalIPAMReconciler(ctx context.Context, mgr manager.Manager) (*MetalIPAMReconciler, error) {
klog.Info("starting metal ipam controller")
r := &MetalIPAMReconciler{
client: mgr.GetClient(),
log: ctrl.Log.WithName("controllers").WithName("metal_ipam"),
}

coreClient, err := corev1client.NewForConfig(mgr.GetConfig())
if err != nil {
return nil, fmt.Errorf("building corev1 client: %w", err)
}
r.coreV1Client = coreClient

return r, nil
}

// MetalIPAMReconciler observes Node objects, assigning their `PodCIDRs` from the instance's `ExternalIpv6`.
type MetalIPAMReconciler struct {
// client is the controller-runtime client
client client.Client

// log is a logr
log logr.Logger

// coreV1Client is a client-go client for patching nodes
coreV1Client *corev1client.CoreV1Client
}

// +kubebuilder:rbac:groups=,resources=nodes,verbs=get;list;watch;patch
// Reconcile is the main reconciler function that observes node changes.
func (r *MetalIPAMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
node := &corev1.Node{}
if err := r.client.Get(ctx, req.NamespacedName, node); err != nil {
klog.Warningf("unable to fetch node %s: %v", node.Name, err)
if apierrors.IsNotFound(err) {
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}

host := &kopsapi.Host{}
id := types.NamespacedName{
Namespace: "kops-system",
Name: node.Name,
}
if err := r.client.Get(ctx, id, host); err != nil {
klog.Warningf("unable to fetch host %s: %v", id, err)
return ctrl.Result{}, err
}

if len(node.Spec.PodCIDRs) == 0 {
if err := patchNodePodCIDRs(r.coreV1Client, ctx, node, host.Spec.PodCIDRs); err != nil {
return ctrl.Result{}, err
}
}

return ctrl.Result{}, nil
}

func (r *MetalIPAMReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("metal_ipam").
For(&corev1.Node{}).
Complete(r)
}
6 changes: 6 additions & 0 deletions cmd/kops-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,12 @@ func setupCloudIPAM(ctx context.Context, mgr manager.Manager, opt *config.Option
return fmt.Errorf("creating gce IPAM controller: %w", err)
}
controller = ipamController
case "metal":
ipamController, err := controllers.NewMetalIPAMReconciler(ctx, mgr)
if err != nil {
return fmt.Errorf("creating metal IPAM controller: %w", err)
}
controller = ipamController
default:
return fmt.Errorf("kOps IPAM controller is not supported on cloud %q", opt.Cloud)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/kops/toolbox_enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func NewCmdToolboxEnroll(f commandutils.Factory, out io.Writer) *cobra.Command {

cmd.Flags().StringVar(&options.ClusterName, "cluster", options.ClusterName, "Name of cluster to join")
cmd.Flags().StringVar(&options.InstanceGroup, "instance-group", options.InstanceGroup, "Name of instance-group to join")
cmd.Flags().StringSliceVar(&options.PodCIDRs, "pod-cidr", options.PodCIDRs, "IP Address range to use for pods that run on this node")

cmd.Flags().StringVar(&options.Host, "host", options.Host, "IP/hostname for machine to add")
cmd.Flags().StringVar(&options.SSHUser, "ssh-user", options.SSHUser, "user for ssh")
Expand Down
1 change: 1 addition & 0 deletions docs/cli/kops_toolbox_enroll.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions k8s/crds/kops.k8s.io_hosts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ spec:
properties:
instanceGroup:
type: string
podCIDRs:
description: PodCIDRs configures the IP ranges to be used for pods
on this node/host.
items:
type: string
type: array
publicKey:
type: string
type: object
Expand Down
58 changes: 54 additions & 4 deletions nodeup/pkg/model/kube_apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ package model
import (
"context"
"fmt"
"net"
"path/filepath"
"sort"
"strings"

"k8s.io/klog/v2"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/flagbuilder"
"k8s.io/kops/pkg/k8scodecs"
Expand Down Expand Up @@ -77,6 +79,55 @@ func (b *KubeAPIServerBuilder) Build(c *fi.NodeupModelBuilderContext) error {
}
}

if b.CloudProvider() == kops.CloudProviderMetal {
// Workaround for https://github.com/kubernetes/kubernetes/issues/111671
if b.IsIPv6Only() {
interfaces, err := net.Interfaces()
if err != nil {
return fmt.Errorf("getting local network interfaces: %w", err)
}
var ipv6s []net.IP
for _, intf := range interfaces {
addresses, err := intf.Addrs()
if err != nil {
return fmt.Errorf("getting addresses for network interface %q: %w", intf.Name, err)
}
for _, addr := range addresses {
ip, _, err := net.ParseCIDR(addr.String())
if ip == nil {
return fmt.Errorf("parsing ip address %q (bound to network %q): %w", addr.String(), intf.Name, err)
}
if ip.To4() != nil {
// We're only looking for ipv6
continue
}
if ip.IsLinkLocalUnicast() {
klog.V(4).Infof("ignoring link-local unicast addr %v", addr)
continue
}
if ip.IsLinkLocalMulticast() {
klog.V(4).Infof("ignoring link-local multicast addr %v", addr)
continue
}
if ip.IsLoopback() {
klog.V(4).Infof("ignoring loopback addr %v", addr)
continue
}
ipv6s = append(ipv6s, ip)
}
}
if len(ipv6s) > 1 {
klog.Warningf("found multiple ipv6s, choosing first: %v", ipv6s)
}
if len(ipv6s) == 0 {
klog.Warningf("did not find ipv6 address for kube-apiserver --advertise-address")
}
if len(ipv6s) > 0 {
kubeAPIServer.AdvertiseAddress = ipv6s[0].String()
}
}
}

b.configureOIDC(&kubeAPIServer)
if err := b.writeAuthenticationConfig(c, &kubeAPIServer); err != nil {
return err
Expand Down Expand Up @@ -697,10 +748,9 @@ func (b *KubeAPIServerBuilder) buildPod(ctx context.Context, kubeAPIServer *kops
image := b.RemapImage(kubeAPIServer.Image)

container := &v1.Container{
Name: "kube-apiserver",
Image: image,
Env: append(kubeAPIServer.Env, proxy.GetProxyEnvVars(b.NodeupConfig.Networking.EgressProxy)...),
LivenessProbe: livenessProbe,
Name: "kube-apiserver",
Image: image,
Env: append(kubeAPIServer.Env, proxy.GetProxyEnvVars(b.NodeupConfig.Networking.EgressProxy)...), LivenessProbe: livenessProbe,
ReadinessProbe: readinessProbe,
StartupProbe: startupProbe,
Ports: []v1.ContainerPort{
Expand Down
2 changes: 2 additions & 0 deletions nodeup/pkg/model/prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func (b *PrefixBuilder) Build(c *fi.NodeupModelBuilderContext) error {
})
case kops.CloudProviderGCE:
// Prefix is assigned by GCE
case kops.CloudProviderMetal:
// IPv6 must be configured externally (not by nodeup)
default:
return fmt.Errorf("kOps IPAM controller not supported on cloud %q", b.CloudProvider())
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/kops/v1alpha2/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type Host struct {
type HostSpec struct {
PublicKey string `json:"publicKey,omitempty"`
InstanceGroup string `json:"instanceGroup,omitempty"`

// PodCIDRs configures the IP ranges to be used for pods on this node/host.
PodCIDRs []string `json:"podCIDRs,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
7 changes: 6 additions & 1 deletion pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pkg/commands/toolbox_enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ type ToolboxEnrollOptions struct {

SSHUser string
SSHPort int

// PodCIDRs is the list of IP Address ranges to use for pods that run on this node
PodCIDRs []string
}

func (o *ToolboxEnrollOptions) InitDefaults() {
Expand Down Expand Up @@ -209,6 +212,7 @@ func createHostResourceInAPIServer(ctx context.Context, options *ToolboxEnrollOp
host.Name = nodeName
host.Spec.InstanceGroup = options.InstanceGroup
host.Spec.PublicKey = string(publicKey)
host.Spec.PodCIDRs = options.PodCIDRs

if err := client.Create(ctx, host); err != nil {
return fmt.Errorf("failed to create host %s/%s: %w", host.Namespace, host.Name, err)
Expand Down
16 changes: 14 additions & 2 deletions pkg/kubeconfig/create_kubecfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"crypto/x509/pkix"
"fmt"
"net"
"os/user"
"sort"
"time"
Expand All @@ -41,7 +42,7 @@ func BuildKubecfg(ctx context.Context, cluster *kops.Cluster, keyStore fi.Keysto
server = "https://" + cluster.APIInternalName()
} else {
if cluster.Spec.API.PublicName != "" {
server = "https://" + cluster.Spec.API.PublicName
server = "https://" + wrapIPv6Address(cluster.Spec.API.PublicName)
} else {
server = "https://api." + clusterName
}
Expand Down Expand Up @@ -82,7 +83,7 @@ func BuildKubecfg(ctx context.Context, cluster *kops.Cluster, keyStore fi.Keysto
if len(targets) != 1 {
klog.Warningf("Found multiple API endpoints (%v), choosing arbitrarily", targets)
}
server = "https://" + targets[0]
server = "https://" + wrapIPv6Address(targets[0])
}
}
}
Expand Down Expand Up @@ -171,3 +172,14 @@ func BuildKubecfg(ctx context.Context, cluster *kops.Cluster, keyStore fi.Keysto

return b, nil
}

// wrapIPv6Address will wrap IPv6 addresses in square brackets,
// for use in URLs; other endpoints are unchanged.
func wrapIPv6Address(endpoint string) string {
ip := net.ParseIP(endpoint)
// IPv6 addresses are wrapped in square brackets in URLs
if ip != nil && ip.To4() == nil {
return "[" + endpoint + "]"
}
return endpoint
}
2 changes: 2 additions & 0 deletions tests/e2e/scenarios/bare-metal/cleanup
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ sudo ip link del dev tap-vm0 || true
sudo ip link del dev tap-vm1 || true
sudo ip link del dev tap-vm2 || true

sudo ip link del dev br0 || true

rm -rf .build/vm0
rm -rf .build/vm1
rm -rf .build/vm2
Expand Down
Loading
Loading