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

(Experimental) bare-metal with IPv6 #16944

Open
wants to merge 5 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
3 changes: 3 additions & 0 deletions cmd/kops/toolbox_enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ 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")
cmd.Flags().IntVar(&options.SSHPort, "ssh-port", options.SSHPort, "port for ssh")

cmd.Flags().BoolVar(&options.BuildHost, "build-host", options.BuildHost, "only build the host resource, don't apply it or enroll the node")

return cmd
}
2 changes: 2 additions & 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
51 changes: 51 additions & 0 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
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.

Loading
Loading