mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-25 00:21:10 +02:00
This PR moves to using v1alpha1 as the inital node config version, so we can graduate these configs a little more cleanly later on. Signed-off-by: Spencer Smith <robertspencersmith@gmail.com>
380 lines
10 KiB
Go
380 lines
10 KiB
Go
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package generate
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/rand"
|
|
stdlibx509 "crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
|
|
"github.com/talos-systems/talos/pkg/constants"
|
|
"github.com/talos-systems/talos/pkg/crypto/x509"
|
|
tnet "github.com/talos-systems/talos/pkg/net"
|
|
)
|
|
|
|
// DefaultIPv4PodNet is the network to be used for kubernetes Pods when using IPv4-based master nodes
|
|
const DefaultIPv4PodNet = "10.244.0.0/16"
|
|
|
|
// DefaultIPv4ServiceNet is the network to be used for kubernetes Services when using IPv4-based master nodes
|
|
const DefaultIPv4ServiceNet = "10.96.0.0/12"
|
|
|
|
// DefaultIPv6PodNet is the network to be used for kubernetes Pods when using IPv6-based master nodes
|
|
const DefaultIPv6PodNet = "fc00:db8:10::/56"
|
|
|
|
// DefaultIPv6ServiceNet is the network to be used for kubernetes Services when using IPv6-based master nodes
|
|
const DefaultIPv6ServiceNet = "fc00:db8:20::/112"
|
|
|
|
// CertStrings holds the string representation of a certificate and key.
|
|
type CertStrings struct {
|
|
Crt string
|
|
Key string
|
|
}
|
|
|
|
// Input holds info about certs, ips, and node type.
|
|
type Input struct {
|
|
Certs *Certs
|
|
|
|
// ControlplaneEndpoint is the canonical address of the kubernetes control
|
|
// plane. It can be a DNS name, the IP address of a load balancer, or
|
|
// (default) the IP address of the first master node. It is NOT
|
|
// multi-valued. It may optionally specify the port.
|
|
ControlPlaneEndpoint string
|
|
|
|
MasterIPs []string
|
|
AdditionalSubjectAltNames []string
|
|
|
|
ClusterName string
|
|
ServiceDomain string
|
|
PodNet []string
|
|
ServiceNet []string
|
|
KubernetesVersion string
|
|
KubeadmTokens *KubeadmTokens
|
|
TrustdInfo *TrustdInfo
|
|
|
|
//
|
|
// Runtime variables
|
|
//
|
|
|
|
// Index is the index of the current master
|
|
Index int
|
|
|
|
// IP is the IP address of the current master
|
|
IP net.IP
|
|
}
|
|
|
|
// Endpoints returns the formatted set of Master IP addresses
|
|
func (i *Input) Endpoints() (out string) {
|
|
if i == nil || len(i.MasterIPs) < 1 {
|
|
panic("cannot Endpoints without any Master IPs")
|
|
}
|
|
for index, addr := range i.MasterIPs {
|
|
if index > 0 {
|
|
out += ", "
|
|
}
|
|
out += fmt.Sprintf(`"%s"`, addr)
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetControlPlaneEndpoint returns the formatted host:port of the canonical controlplane address, defaulting to the first master IP
|
|
func (i *Input) GetControlPlaneEndpoint() string {
|
|
|
|
if i == nil || (len(i.MasterIPs) < 1 && i.ControlPlaneEndpoint == "") {
|
|
panic("cannot GetControlPlaneEndpoint without any Master IPs")
|
|
}
|
|
|
|
if i.ControlPlaneEndpoint != "" {
|
|
return i.ControlPlaneEndpoint
|
|
}
|
|
|
|
return tnet.FormatAddress(i.MasterIPs[0])
|
|
}
|
|
|
|
// GetAPIServerEndpoint returns the formatted host:port of the API server endpoint
|
|
func (i *Input) GetAPIServerEndpoint(port string) string {
|
|
|
|
if i == nil || len(i.MasterIPs) < 1 {
|
|
panic("cannot GetControlPlaneEndpoint without any Master IPs")
|
|
}
|
|
|
|
// Each master after the first should reference the next-lower master index.
|
|
// Thus, master-2 references master-1 and master-3 references master-2.
|
|
refMaster := 0
|
|
if i.Index > 1 {
|
|
refMaster = i.Index - 1
|
|
}
|
|
|
|
if port == "" {
|
|
return tnet.FormatAddress(i.MasterIPs[refMaster])
|
|
}
|
|
return net.JoinHostPort(i.MasterIPs[refMaster], port)
|
|
}
|
|
|
|
// GetAPIServerSANs returns the formatted list of Subject Alt Name addresses for the API Server
|
|
func (i *Input) GetAPIServerSANs() []string {
|
|
|
|
var list = []string{"127.0.0.1", "::1"}
|
|
list = append(list, i.MasterIPs...)
|
|
list = append(list, i.AdditionalSubjectAltNames...)
|
|
|
|
return list
|
|
}
|
|
|
|
// Certs holds the base64 encoded keys and certificates.
|
|
type Certs struct {
|
|
AdminCert string
|
|
AdminKey string
|
|
OsCert string
|
|
OsKey string
|
|
K8sCert string
|
|
K8sKey string
|
|
}
|
|
|
|
// KubeadmTokens holds the senesitve kubeadm data.
|
|
type KubeadmTokens struct {
|
|
BootstrapToken string
|
|
CertKey string
|
|
}
|
|
|
|
// TrustdInfo holds the trustd credentials.
|
|
type TrustdInfo struct {
|
|
Token string
|
|
}
|
|
|
|
// randBytes returns a random string consisting of the characters in
|
|
// validBootstrapTokenChars, with the length customized by the parameter
|
|
func randBytes(length int) (string, error) {
|
|
|
|
// validBootstrapTokenChars defines the characters a bootstrap token can consist of
|
|
const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
|
|
// len("0123456789abcdefghijklmnopqrstuvwxyz") = 36 which doesn't evenly divide
|
|
// the possible values of a byte: 256 mod 36 = 4. Discard any random bytes we
|
|
// read that are >= 252 so the bytes we evenly divide the character set.
|
|
const maxByteValue = 252
|
|
|
|
var (
|
|
b byte
|
|
err error
|
|
token = make([]byte, length)
|
|
)
|
|
|
|
reader := bufio.NewReaderSize(rand.Reader, length*2)
|
|
for i := range token {
|
|
for {
|
|
if b, err = reader.ReadByte(); err != nil {
|
|
return "", err
|
|
}
|
|
if b < maxByteValue {
|
|
break
|
|
}
|
|
}
|
|
token[i] = validBootstrapTokenChars[int(b)%len(validBootstrapTokenChars)]
|
|
}
|
|
|
|
return string(token), err
|
|
}
|
|
|
|
//genToken will generate a token of the format abc.123 (like kubeadm/trustd), where the length of the first string (before the dot)
|
|
//and length of the second string (after dot) are specified as inputs
|
|
func genToken(lenFirst int, lenSecond int) (string, error) {
|
|
|
|
var err error
|
|
var tokenTemp = make([]string, 2)
|
|
|
|
tokenTemp[0], err = randBytes(lenFirst)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tokenTemp[1], err = randBytes(lenSecond)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return tokenTemp[0] + "." + tokenTemp[1], nil
|
|
}
|
|
|
|
func isIPv6(addrs ...string) bool {
|
|
for _, a := range addrs {
|
|
if ip := net.ParseIP(a); ip != nil {
|
|
if ip.To4() == nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NewInput generates the sensitive data required to generate all userdata
|
|
// types.
|
|
// nolint: dupl,gocyclo
|
|
func NewInput(clustername string, masterIPs []string) (input *Input, err error) {
|
|
|
|
var loopbackIP, podNet, serviceNet string
|
|
|
|
if isIPv6(masterIPs...) {
|
|
loopbackIP = "::1"
|
|
podNet = DefaultIPv6PodNet
|
|
serviceNet = DefaultIPv6ServiceNet
|
|
} else {
|
|
loopbackIP = "127.0.0.1"
|
|
podNet = DefaultIPv4PodNet
|
|
serviceNet = DefaultIPv4ServiceNet
|
|
}
|
|
|
|
//Gen trustd token strings
|
|
kubeadmBootstrapToken, err := genToken(6, 16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//TODO: Can be dropped
|
|
//Gen kubeadm cert key
|
|
kubeadmCertKey, err := randBytes(26)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//Gen trustd token strings
|
|
trustdToken, err := genToken(6, 16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
kubeadmTokens := &KubeadmTokens{
|
|
BootstrapToken: kubeadmBootstrapToken,
|
|
CertKey: kubeadmCertKey,
|
|
}
|
|
|
|
trustdInfo := &TrustdInfo{
|
|
Token: trustdToken,
|
|
}
|
|
|
|
// Generate Kubernetes CA.
|
|
opts := []x509.Option{x509.RSA(true), x509.Organization("talos-k8s")}
|
|
k8sCert, err := x509.NewSelfSignedCertificateAuthority(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate Talos CA.
|
|
opts = []x509.Option{x509.RSA(false), x509.Organization("talos-os")}
|
|
osCert, err := x509.NewSelfSignedCertificateAuthority(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate the admin talosconfig.
|
|
adminKey, err := x509.NewKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pemBlock, _ := pem.Decode(adminKey.KeyPEM)
|
|
if pemBlock == nil {
|
|
return nil, errors.New("failed to decode admin key pem")
|
|
}
|
|
adminKeyEC, err := stdlibx509.ParseECPrivateKey(pemBlock.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ips := []net.IP{net.ParseIP(loopbackIP)}
|
|
opts = []x509.Option{x509.IPAddresses(ips)}
|
|
csr, err := x509.NewCertificateSigningRequest(adminKeyEC, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
csrPemBlock, _ := pem.Decode(csr.X509CertificateRequestPEM)
|
|
if csrPemBlock == nil {
|
|
return nil, errors.New("failed to decode csr pem")
|
|
}
|
|
ccsr, err := stdlibx509.ParseCertificateRequest(csrPemBlock.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
caPemBlock, _ := pem.Decode(osCert.CrtPEM)
|
|
if caPemBlock == nil {
|
|
return nil, errors.New("failed to decode ca cert pem")
|
|
}
|
|
caCrt, err := stdlibx509.ParseCertificate(caPemBlock.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
caKeyPemBlock, _ := pem.Decode(osCert.KeyPEM)
|
|
if caKeyPemBlock == nil {
|
|
return nil, errors.New("failed to decode ca key pem")
|
|
}
|
|
caKey, err := stdlibx509.ParseECPrivateKey(caKeyPemBlock.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
adminCrt, err := x509.NewCertificateFromCSR(caCrt, caKey, ccsr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certs := &Certs{
|
|
AdminCert: base64.StdEncoding.EncodeToString(adminCrt.X509CertificatePEM),
|
|
AdminKey: base64.StdEncoding.EncodeToString(adminKey.KeyPEM),
|
|
OsCert: base64.StdEncoding.EncodeToString(osCert.CrtPEM),
|
|
OsKey: base64.StdEncoding.EncodeToString(osCert.KeyPEM),
|
|
K8sCert: base64.StdEncoding.EncodeToString(k8sCert.CrtPEM),
|
|
K8sKey: base64.StdEncoding.EncodeToString(k8sCert.KeyPEM),
|
|
}
|
|
|
|
input = &Input{
|
|
Certs: certs,
|
|
MasterIPs: masterIPs,
|
|
PodNet: []string{podNet},
|
|
ServiceNet: []string{serviceNet},
|
|
ServiceDomain: "cluster.local",
|
|
ClusterName: clustername,
|
|
KubernetesVersion: constants.KubernetesVersion,
|
|
KubeadmTokens: kubeadmTokens,
|
|
TrustdInfo: trustdInfo,
|
|
}
|
|
|
|
return input, nil
|
|
}
|
|
|
|
// Type represents a userdata type.
|
|
type Type int
|
|
|
|
const (
|
|
// TypeInit indicates a userdata type should correspond to the kubeadm
|
|
// InitConfiguration type.
|
|
TypeInit Type = iota
|
|
// TypeControlPlane indicates a userdata type should correspond to the
|
|
// kubeadm JoinConfiguration type that has the ControlPlane field
|
|
// defined.
|
|
TypeControlPlane
|
|
// TypeJoin indicates a userdata type should correspond to the kubeadm
|
|
// JoinConfiguration type.
|
|
TypeJoin
|
|
)
|
|
|
|
// Sring returns the string representation of Type.
|
|
func (t Type) String() string {
|
|
return [...]string{"Init", "ControlPlane", "Join"}[t]
|
|
}
|
|
|
|
// Userdata returns the talos userdata for a given node type.
|
|
func Userdata(t Type, in *Input) (string, error) {
|
|
switch t {
|
|
case TypeInit:
|
|
return initUd(in)
|
|
case TypeControlPlane:
|
|
return controlPlaneUd(in)
|
|
case TypeJoin:
|
|
return workerUd(in)
|
|
default:
|
|
}
|
|
return "", errors.New("failed to determine userdata type to generate")
|
|
}
|