feat: generate and use v1 machine configs

This PR will implement the v1 machine config proposal. This will allow
for a streamlined config for talos nodes.

Signed-off-by: Spencer Smith <robertspencersmith@gmail.com>
This commit is contained in:
Spencer Smith
2019-08-22 19:01:05 -04:00
committed by Spencer Smith
parent 15cfd42168
commit f85750cdca
30 changed files with 1736 additions and 179 deletions

View File

@ -24,7 +24,7 @@ import (
"github.com/talos-systems/talos/cmd/osctl/cmd/cluster/pkg/node"
"github.com/talos-systems/talos/cmd/osctl/pkg/client/config"
"github.com/talos-systems/talos/cmd/osctl/pkg/helpers"
"github.com/talos-systems/talos/pkg/userdata/generate"
"github.com/talos-systems/talos/pkg/userdata/v1/generate"
"github.com/talos-systems/talos/pkg/version"
)

View File

@ -15,7 +15,7 @@ import (
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/talos-systems/talos/pkg/userdata/generate"
"github.com/talos-systems/talos/pkg/userdata/v1/generate"
)
// Request represents the set of options available for configuring a node.

View File

@ -17,11 +17,16 @@ import (
"github.com/spf13/cobra"
"github.com/talos-systems/talos/cmd/osctl/pkg/client/config"
"github.com/talos-systems/talos/cmd/osctl/pkg/helpers"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/generate"
udv0 "github.com/talos-systems/talos/pkg/userdata"
udgenv0 "github.com/talos-systems/talos/pkg/userdata/generate"
"github.com/talos-systems/talos/pkg/userdata/translate"
udgenv1 "github.com/talos-systems/talos/pkg/userdata/v1/generate"
"gopkg.in/yaml.v2"
)
var genVersion string
// configCmd represents the config command.
var configCmd = &cobra.Command{
Use: "config",
@ -127,57 +132,66 @@ var configGenerateCmd = &cobra.Command{
if len(args) != 2 {
log.Fatal("expected a cluster name and comma delimited list of IP addresses")
}
input, err := generate.NewInput(args[0], strings.Split(args[1], ","))
if err != nil {
helpers.Fatalf("failed to generate PKI and tokens: %v", err)
switch genVersion {
case "v0":
genV0Userdata(args)
case "v1":
genV1Userdata(args)
}
input.AdditionalSubjectAltNames = additionalSANs
workingDir, err := os.Getwd()
if err != nil {
helpers.Fatalf("failed to fetch current working dir: %v", err)
}
var udType generate.Type
for idx, master := range strings.Split(args[1], ",") {
input.Index = idx
input.IP = net.ParseIP(master)
if input.Index == 0 {
udType = generate.TypeInit
} else {
udType = generate.TypeControlPlane
}
if err = writeUserdata(input, udType, "master-"+strconv.Itoa(idx+1)); err != nil {
helpers.Fatalf("failed to generate userdata for %s: %v", "master-"+strconv.Itoa(idx+1), err)
}
fmt.Println("created file", workingDir+"/master-"+strconv.Itoa(idx+1)+".yaml")
}
input.IP = nil
if err = writeUserdata(input, generate.TypeJoin, "worker"); err != nil {
helpers.Fatalf("failed to generate userdata for %s: %v", "worker", err)
}
fmt.Println("created file", workingDir+"/worker.yaml")
data, err := generate.Talosconfig(input)
if err != nil {
helpers.Fatalf("failed to generate talosconfig: %v", err)
}
if err = ioutil.WriteFile("talosconfig", []byte(data), 0644); err != nil {
helpers.Fatalf("%v", err)
}
fmt.Println("created file", workingDir+"/talosconfig")
},
}
func writeUserdata(input *generate.Input, t generate.Type, name string) (err error) {
func genV0Userdata(args []string) {
input, err := udgenv0.NewInput(args[0], strings.Split(args[1], ","))
if err != nil {
helpers.Fatalf("failed to generate PKI and tokens: %v", err)
}
input.AdditionalSubjectAltNames = additionalSANs
workingDir, err := os.Getwd()
if err != nil {
helpers.Fatalf("failed to fetch current working dir: %v", err)
}
var udType udgenv0.Type
for idx, master := range strings.Split(args[1], ",") {
input.Index = idx
input.IP = net.ParseIP(master)
if input.Index == 0 {
udType = udgenv0.TypeInit
} else {
udType = udgenv0.TypeControlPlane
}
if err = writeV0Userdata(input, udType, "master-"+strconv.Itoa(idx+1)); err != nil {
helpers.Fatalf("failed to generate userdata for %s: %v", "master-"+strconv.Itoa(idx+1), err)
}
fmt.Println("created file", workingDir+"/master-"+strconv.Itoa(idx+1)+".yaml")
}
input.IP = nil
if err = writeV0Userdata(input, udgenv0.TypeJoin, "worker"); err != nil {
helpers.Fatalf("failed to generate userdata for %s: %v", "worker", err)
}
fmt.Println("created file", workingDir+"/worker.yaml")
data, err := udgenv0.Talosconfig(input)
if err != nil {
helpers.Fatalf("failed to generate talosconfig: %v", err)
}
if err = ioutil.WriteFile("talosconfig", []byte(data), 0644); err != nil {
helpers.Fatalf("%v", err)
}
fmt.Println("created file", workingDir+"/talosconfig")
}
func writeV0Userdata(input *udgenv0.Input, t udgenv0.Type, name string) (err error) {
var data string
data, err = generate.Userdata(t, input)
data, err = udgenv0.Userdata(t, input)
if err != nil {
return err
}
ud := &userdata.UserData{}
ud := &udv0.UserData{}
if err = yaml.Unmarshal([]byte(data), ud); err != nil {
return err
}
@ -191,12 +205,84 @@ func writeUserdata(input *generate.Input, t generate.Type, name string) (err err
return nil
}
func genV1Userdata(args []string) {
input, err := udgenv1.NewInput(args[0], strings.Split(args[1], ","))
if err != nil {
helpers.Fatalf("failed to generate PKI and tokens: %v", err)
}
input.AdditionalSubjectAltNames = additionalSANs
workingDir, err := os.Getwd()
if err != nil {
helpers.Fatalf("failed to fetch current working dir: %v", err)
}
var udType udgenv1.Type
for idx, master := range strings.Split(args[1], ",") {
input.Index = idx
input.IP = net.ParseIP(master)
if input.Index == 0 {
udType = udgenv1.TypeInit
} else {
udType = udgenv1.TypeControlPlane
}
if err = writeV1Userdata(input, udType, "master-"+strconv.Itoa(idx+1)); err != nil {
helpers.Fatalf("failed to generate userdata for %s: %v", "master-"+strconv.Itoa(idx+1), err)
}
fmt.Println("created file", workingDir+"/master-"+strconv.Itoa(idx+1)+".yaml")
}
input.IP = nil
if err = writeV1Userdata(input, udgenv1.TypeJoin, "worker"); err != nil {
helpers.Fatalf("failed to generate userdata for %s: %v", "worker", err)
}
fmt.Println("created file", workingDir+"/worker.yaml")
data, err := udgenv1.Talosconfig(input)
if err != nil {
helpers.Fatalf("failed to generate talosconfig: %v", err)
}
if err = ioutil.WriteFile("talosconfig", []byte(data), 0644); err != nil {
helpers.Fatalf("%v", err)
}
fmt.Println("created file", workingDir+"/talosconfig")
}
func writeV1Userdata(input *udgenv1.Input, t udgenv1.Type, name string) (err error) {
var data string
data, err = udgenv1.Userdata(t, input)
if err != nil {
return err
}
trans, err := translate.NewTranslator("v1", data)
if err != nil {
return err
}
ud, err := trans.Translate()
if err != nil {
return err
}
if err = ud.Validate(); err != nil {
return err
}
if err = ioutil.WriteFile(strings.ToLower(name)+".yaml", []byte(data), 0644); err != nil {
return err
}
return nil
}
func init() {
configCmd.AddCommand(configContextCmd, configTargetCmd, configAddCmd, configGenerateCmd)
configAddCmd.Flags().StringVar(&ca, "ca", "", "the path to the CA certificate")
configAddCmd.Flags().StringVar(&crt, "crt", "", "the path to the certificate")
configAddCmd.Flags().StringVar(&key, "key", "", "the path to the key")
configGenerateCmd.Flags().StringSliceVar(&additionalSANs, "additionalSANs", []string{}, "additional Subject-Alt-Names for the APIServer certificate")
configGenerateCmd.Flags().StringVar(&genVersion, "genversion", "v1", "desired machine configs to generate")
helpers.Should(configAddCmd.MarkFlagRequired("ca"))
helpers.Should(configAddCmd.MarkFlagRequired("crt"))
helpers.Should(configAddCmd.MarkFlagRequired("key"))

View File

@ -8,6 +8,7 @@ import (
"strings"
ud "github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/download"
)
// UserData provides an abstraction to call the appropriate method to
@ -15,7 +16,7 @@ import (
// TODO: Merge this in to internal/pkg/userdata
func UserData(location string) (userData *ud.UserData, err error) {
if strings.HasPrefix(location, "http") {
userData, err = ud.Download(location, nil)
userData, err = download.Download(location, nil)
} else {
userData, err = ud.Open(location)
}

View File

@ -15,6 +15,7 @@ import (
"github.com/fullsailor/pkcs7"
"github.com/talos-systems/talos/internal/app/machined/internal/runtime"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/download"
)
const (
@ -118,7 +119,7 @@ func (a *AWS) Name() string {
// UserData implements the platform.Platform interface.
func (a *AWS) UserData() (*userdata.UserData, error) {
return userdata.Download(AWSUserDataEndpoint)
return download.Download(AWSUserDataEndpoint)
}
// Mode implements the platform.Platform interface.

View File

@ -11,6 +11,7 @@ import (
"github.com/talos-systems/talos/internal/app/machined/internal/runtime"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/download"
)
const (
@ -39,7 +40,7 @@ func (a *Azure) UserData() (*userdata.UserData, error) {
return nil, err
}
return userdata.Download(AzureUserDataEndpoint, userdata.WithHeaders(map[string]string{"Metadata": "true"}), userdata.WithFormat("base64"))
return download.Download(AzureUserDataEndpoint, download.WithHeaders(map[string]string{"Metadata": "true"}), download.WithFormat("base64"))
}
// Mode implements the platform.Platform interface.

View File

@ -11,8 +11,7 @@ import (
"github.com/pkg/errors"
"github.com/talos-systems/talos/internal/app/machined/internal/runtime"
"github.com/talos-systems/talos/pkg/userdata"
"gopkg.in/yaml.v2"
"github.com/talos-systems/talos/pkg/userdata/translate"
)
// Container is a platform for installing Talos via an Container image.
@ -33,11 +32,14 @@ func (c *Container) UserData() (data *userdata.UserData, err error) {
if decoded, err = base64.StdEncoding.DecodeString(s); err != nil {
return nil, err
}
data = &userdata.UserData{}
if err = yaml.Unmarshal(decoded, data); err != nil {
trans, err := translate.NewTranslator("v1", string(decoded))
if err != nil {
return nil, err
}
data, err = trans.Translate()
if err != nil {
return nil, err
}
return data, nil
}

View File

@ -7,6 +7,7 @@ package gcp
import (
"github.com/talos-systems/talos/internal/app/machined/internal/runtime"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/download"
)
const (
@ -24,7 +25,7 @@ func (gc *GCP) Name() string {
// UserData implements the platform.Platform interface.
func (gc *GCP) UserData() (data *userdata.UserData, err error) {
return userdata.Download(GCUserDataEndpoint, userdata.WithHeaders(map[string]string{"Metadata-Flavor": "Google"}))
return download.Download(GCUserDataEndpoint, download.WithHeaders(map[string]string{"Metadata-Flavor": "Google"}))
}
// Mode implements the platform.Platform interface.

View File

@ -15,6 +15,7 @@ import (
"github.com/talos-systems/talos/pkg/blockdevice/probe"
"github.com/talos-systems/talos/pkg/constants"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/download"
"golang.org/x/sys/unix"
@ -67,7 +68,7 @@ func (b *Metal) UserData() (data *userdata.UserData, err error) {
return data, nil
}
return userdata.Download(*option)
return download.Download(*option)
}
// Mode implements the platform.Platform interface.

View File

@ -7,6 +7,7 @@ package packet
import (
"github.com/talos-systems/talos/internal/app/machined/internal/runtime"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/download"
)
const (
@ -24,7 +25,7 @@ func (p *Packet) Name() string {
// UserData implements the platform.Platform interface.
func (p *Packet) UserData() (data *userdata.UserData, err error) {
return userdata.Download(PacketUserDataEndpoint)
return download.Download(PacketUserDataEndpoint)
}
// Mode implements the platform.Platform interface.

View File

@ -2,7 +2,7 @@
* 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 userdata
package download
import (
"encoding/base64"
@ -14,6 +14,8 @@ import (
"net/url"
"time"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/translate"
yaml "gopkg.in/yaml.v2"
)
@ -26,6 +28,10 @@ type downloadOptions struct {
Wait float64
}
type version struct {
Version string `yaml:"version"`
}
// Option configures the download options
type Option func(*downloadOptions)
@ -76,7 +82,7 @@ func WithMaxWait(wait float64) Option {
// Download initializes a UserData struct from a remote URL.
// nolint: gocyclo
func Download(udURL string, opts ...Option) (data *UserData, err error) {
func Download(udURL string, opts ...Option) (data *userdata.UserData, err error) {
u, err := url.Parse(udURL)
if err != nil {
return data, err
@ -98,6 +104,7 @@ func Download(udURL string, opts ...Option) (data *UserData, err error) {
var dataBytes []byte
for attempt := 0; attempt < dlOpts.Retries; attempt++ {
dataBytes, err = download(req)
if err != nil {
log.Printf("download failed: %+v", err)
@ -117,11 +124,30 @@ func Download(udURL string, opts ...Option) (data *UserData, err error) {
dataBytes = baseBytes
}
data = &UserData{}
version := &version{}
if err = yaml.Unmarshal(dataBytes, version); err != nil {
return data, fmt.Errorf("failed to parse version: %s", err.Error())
}
data = &userdata.UserData{}
if version.Version != "" {
trans, err := translate.NewTranslator(version.Version, string(dataBytes))
if err != nil {
return data, err
}
data, err = trans.Translate()
if err != nil {
return data, err
}
return data, data.Validate()
}
// No version specified, just unmarshal and return
if err := yaml.Unmarshal(dataBytes, data); err != nil {
return data, fmt.Errorf("unmarshal user data: %s", err.Error())
return data, fmt.Errorf("unmarshal v0 user data: %s", err.Error())
}
return data, data.Validate()
}
return data, fmt.Errorf("failed to download userdata from: %s", u.String())

View File

@ -0,0 +1,200 @@
/* 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 download
import (
"encoding/base64"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/suite"
)
type downloadSuite struct {
suite.Suite
}
func TestDownloadSuite(t *testing.T) {
suite.Run(t, new(downloadSuite))
}
// nolint: dupl
func (suite *downloadSuite) TestV0Download() {
// Disable logging for test
log.SetOutput(ioutil.Discard)
ts := testUDServer()
defer ts.Close()
var err error
// Download plain-text string
_, err = Download(ts.URL, WithMaxWait(0.1), WithHeaders(map[string]string{"configVersion": "v0"}))
suite.Require().NoError(err)
// Download b64 string
_, err = Download(
ts.URL,
WithFormat(b64),
WithRetries(1),
WithHeaders(map[string]string{"Metadata": "true", "format": b64, "configVersion": "v0"}),
)
suite.Require().NoError(err)
log.SetOutput(os.Stderr)
}
// nolint: dupl
func (suite *downloadSuite) TestV1Download() {
// Disable logging for test
log.SetOutput(ioutil.Discard)
ts := testUDServer()
defer ts.Close()
var err error
_, err = Download(ts.URL, WithMaxWait(0.1), WithHeaders(map[string]string{"configVersion": "v1"}))
suite.Require().NoError(err)
_, err = Download(
ts.URL,
WithFormat(b64),
WithRetries(1),
WithHeaders(map[string]string{"Metadata": "true", "format": b64, "configVersion": "v1"}),
)
suite.Require().NoError(err)
log.SetOutput(os.Stderr)
}
func testUDServer() *httptest.Server {
var count int
testMap := map[string]string{
"v0": testV0Config,
"v1": testV1Config,
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count++
log.Printf("Request %d\n", count)
if count < 2 {
w.WriteHeader(http.StatusInternalServerError)
}
if r.Header.Get("format") == b64 {
// nolint: errcheck
w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(testMap[r.Header.Get("configVersion")]))))
} else {
// nolint: errcheck
w.Write([]byte(testMap[r.Header.Get("configVersion")]))
}
}))
return ts
}
// nolint: lll
const testV1Config = `version: v1
machine:
type: init
token: 57dn7x.k5jc6dum97cotlqb
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
kubelet: {}
network: {}
install: {}
cluster:
controlPlane:
ips:
- 10.254.0.10
clusterName: spencer-test
network:
dnsDomain: cluster.local
podSubnets:
- 10.244.0.0/16
serviceSubnets:
- 10.96.0.0/12
token: 4iysc6.t3bsjbrd74v91wpv
initToken: 22c11be4-c413-11e9-b8e8-309c23e4bd47
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
apiServer: {}
controllerManager: {}
scheduler: {}
etcd: {}
`
// nolint: lll
const testV0Config = `version: ""
security:
os:
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
identity:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
kubernetes:
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
sa:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
frontproxy:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
etcd:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
networking:
os: {}
kubernetes: {}
services:
init:
cni: flannel
kubeadm:
initToken: 528d1ad6-3485-49ad-94cd-0f44a35877ac
certificateKey: 'test'
configuration: |
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
localAPIEndpoint:
bindPort: 6443
bootstrapTokens:
- token: '1qbsj9.3oz5hsk6grdfp98b'
ttl: 0s
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
clusterName: test
kubernetesVersion: v1.16.0-alpha.3
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
ipvs:
scheduler: lc
trustd:
username: 'test'
password: 'test'
endpoints: [ "1.2.3.4" ]
certSANs: []
install:
wipe: true
force: true
boot:
force: true
device: /dev/sda
size: 1024000000
ephemeral:
force: true
device: /dev/sda
size: 1024000000
`

View File

@ -34,7 +34,7 @@ type Kubeadm struct {
CertificateKey string `yaml:"certificateKey,omitempty"`
IgnorePreflightErrors []string `yaml:"ignorePreflightErrors,omitempty"`
Token *token.Token `yaml:"initToken,omitempty"`
controlPlane bool
ControlPlane bool
}
// MarshalYAML implements the yaml.Marshaler interface.
@ -112,7 +112,7 @@ func (kdm *Kubeadm) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
kdm.InitConfiguration = cfg
kdm.controlPlane = true
kdm.ControlPlane = true
case kubeadmutil.GroupVersionKindsHasKind(gvks, "JoinConfiguration"):
cfg, err := kubeadmutil.UnmarshalFromYamlForCodecs(config, kubeadmv1beta2.SchemeGroupVersion, kubeadmscheme.Codecs)
if err != nil {
@ -123,7 +123,7 @@ func (kdm *Kubeadm) UnmarshalYAML(unmarshal func(interface{}) error) error {
return errors.New("expected JoinConfiguration")
}
if joinCfg.ControlPlane != nil {
kdm.controlPlane = true
kdm.ControlPlane = true
}
kdm.JoinConfiguration = cfg
case kubeadmutil.GroupVersionKindsHasKind(gvks, "ClusterConfiguration"):
@ -161,7 +161,7 @@ func (kdm *Kubeadm) UnmarshalYAML(unmarshal func(interface{}) error) error {
// IsControlPlane indicates if the current kubeadm configuration is a worker
// acting as a master.
func (kdm *Kubeadm) IsControlPlane() bool {
return kdm.controlPlane
return kdm.ControlPlane
}
// IsBootstrap indicates if the current kubeadm configuration is a master init

View File

@ -2,6 +2,8 @@
* 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 userdata provides internal representation of machine configs
// nolint: dupl
package userdata
import (

View File

@ -0,0 +1,26 @@
/* 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 translate
import (
"errors"
"github.com/talos-systems/talos/pkg/userdata"
)
// Translator is the interface that will be implemented by all future machine config versions
type Translator interface {
Translate() (*userdata.UserData, error)
}
// NewTranslator returns an instance of the translator depending on version
func NewTranslator(apiVersion string, nodeConfig string) (Translator, error) {
switch apiVersion {
case "v1":
return &V1Translator{nodeConfig: nodeConfig}, nil
default:
return nil, errors.New("unknown translator")
}
}

View File

@ -0,0 +1,64 @@
/* 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 translate
import (
"testing"
"github.com/stretchr/testify/suite"
)
type translatorSuite struct {
suite.Suite
}
func TestTranslatorSuite(t *testing.T) {
suite.Run(t, new(translatorSuite))
}
func (suite *translatorSuite) TestTranslation() {
tv1, err := NewTranslator("v1", testV1Config)
suite.Require().NoError(err)
ud, err := tv1.Translate()
suite.Require().NoError(err)
suite.Assert().Equal(string(ud.Version), "v1")
err = ud.Validate()
suite.Require().NoError(err)
}
// nolint: lll
const testV1Config = `version: v1
machine:
type: init
token: 57dn7x.k5jc6dum97cotlqb
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
kubelet: {}
network: {}
install: {}
cluster:
controlPlane:
ips:
- 10.254.0.10
clusterName: spencer-test
network:
dnsDomain: cluster.local
podSubnets:
- 10.244.0.0/16
serviceSubnets:
- 10.96.0.0/12
token: 4iysc6.t3bsjbrd74v91wpv
initToken: 22c11be4-c413-11e9-b8e8-309c23e4bd47
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
apiServer: {}
controllerManager: {}
scheduler: {}
etcd: {}
`

View File

@ -0,0 +1,312 @@
/* 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 translate
import (
"encoding/base64"
"strings"
"time"
"github.com/talos-systems/talos/pkg/constants"
"github.com/talos-systems/talos/pkg/crypto/x509"
"github.com/talos-systems/talos/pkg/userdata"
v1 "github.com/talos-systems/talos/pkg/userdata/v1"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeproxyconfig "k8s.io/kube-proxy/config/v1alpha1"
kubeletconfig "k8s.io/kubelet/config/v1beta1"
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
)
// V1Translator holds info about a v1 machine config translation layer
type V1Translator struct {
nodeConfig string
}
// Translate takes a v1 NodeConfig and translates it to a UserData struct
func (tv1 *V1Translator) Translate() (*userdata.UserData, error) {
nc := &v1.NodeConfig{}
err := yaml.Unmarshal([]byte(tv1.nodeConfig), nc)
if err != nil {
return nil, err
}
// Lay down the absolute minimum for all node types
ud := &userdata.UserData{
Version: "v1",
Security: &userdata.Security{},
Services: &userdata.Services{
Init: &userdata.Init{
CNI: "flannel",
},
Kubeadm: &userdata.Kubeadm{},
Trustd: &userdata.Trustd{
Token: nc.Machine.Token,
Endpoints: nc.Cluster.ControlPlane.IPs,
},
},
}
if nc.Machine.Install != nil {
translateV1Install(nc, ud)
}
switch nc.Machine.Type {
case "init":
err = translateV1Init(nc, ud)
if err != nil {
return nil, err
}
case "controlplane":
err = translateV1ControlPlane(nc, ud)
if err != nil {
return nil, err
}
case "worker":
translateV1Worker(nc, ud)
}
return ud, nil
}
func translateV1Install(nc *v1.NodeConfig, ud *userdata.UserData) {
ud.Install = &userdata.Install{
Wipe: nc.Machine.Install.Wipe,
Force: nc.Machine.Install.Force,
}
if nc.Machine.Install.Boot != nil {
ud.Install.Boot = &userdata.BootDevice{
InstallDevice: userdata.InstallDevice{
Device: nc.Machine.Install.Boot.InstallDisk.Disk,
Size: nc.Machine.Install.Boot.InstallDisk.Size,
},
Kernel: nc.Machine.Install.Boot.Kernel,
Initramfs: nc.Machine.Install.Boot.Initramfs,
}
}
if nc.Machine.Install.Ephemeral != nil {
ud.Install.Ephemeral = &userdata.InstallDevice{
Device: nc.Machine.Install.Ephemeral.Disk,
Size: nc.Machine.Install.Ephemeral.Size,
}
}
if nc.Machine.Install.ExtraDisks != nil {
ud.Install.ExtraDevices = []*userdata.ExtraDevice{}
for _, device := range nc.Machine.Install.ExtraDisks {
ed := &userdata.ExtraDevice{
Device: device.Disk,
Partitions: []*userdata.ExtraDevicePartition{},
}
for _, partition := range device.Partitions {
partToAppend := &userdata.ExtraDevicePartition{
Size: partition.Size,
MountPoint: partition.MountPoint,
}
ed.Partitions = append(ed.Partitions, partToAppend)
}
ud.Install.ExtraDevices = append(ud.Install.ExtraDevices, ed)
}
}
if nc.Machine.Install.ExtraKernelArgs != nil {
ud.Install.ExtraKernelArgs = nc.Machine.Install.ExtraKernelArgs
}
}
func translateV1Init(nc *v1.NodeConfig, ud *userdata.UserData) error {
// Convert and decode certs back to byte slices
osCert, err := base64.StdEncoding.DecodeString(nc.Machine.CA.Crt)
if err != nil {
return err
}
osKey, err := base64.StdEncoding.DecodeString(nc.Machine.CA.Key)
if err != nil {
return err
}
kubeCert, err := base64.StdEncoding.DecodeString(nc.Cluster.CA.Crt)
if err != nil {
return err
}
kubeKey, err := base64.StdEncoding.DecodeString(nc.Cluster.CA.Key)
if err != nil {
return err
}
// Inject certs and SANs
ud.Security.OS = &userdata.OSSecurity{
CA: &x509.PEMEncodedCertificateAndKey{
Crt: osCert,
Key: osKey,
},
}
ud.Security.Kubernetes = &userdata.KubernetesSecurity{
CA: &x509.PEMEncodedCertificateAndKey{
Crt: kubeCert,
Key: kubeKey,
},
}
ud.Services.Trustd.CertSANs = []string{nc.Cluster.ControlPlane.IPs[nc.Cluster.ControlPlane.Index], "127.0.0.1", "::1"}
ud.Services.Kubeadm.Token = nc.Cluster.InitToken
ud.Services.Kubeadm.ControlPlane = true
kubeadmToken := strings.Split(nc.Cluster.Token, ".")
// Craft an init kubeadm config
initConfig := &kubeadm.InitConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "InitConfiguration",
APIVersion: "kubeadm.k8s.io/v1beta2",
},
BootstrapTokens: []kubeadm.BootstrapToken{
{
Token: &kubeadm.BootstrapTokenString{
ID: kubeadmToken[0],
Secret: kubeadmToken[1],
},
TTL: &metav1.Duration{
Duration: time.Duration(0),
},
},
},
NodeRegistration: kubeadm.NodeRegistrationOptions{
KubeletExtraArgs: nc.Machine.Kubelet.ExtraArgs,
},
}
clusterConfig := &kubeadm.ClusterConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ClusterConfiguration",
APIVersion: "kubeadm.k8s.io/v1beta2",
},
ClusterName: nc.Cluster.ClusterName,
KubernetesVersion: constants.KubernetesVersion,
ControlPlaneEndpoint: nc.Cluster.ControlPlane.IPs[0] + ":443",
Networking: kubeadm.Networking{
DNSDomain: nc.Cluster.Network.DNSDomain,
PodSubnet: nc.Cluster.Network.PodSubnet[0],
ServiceSubnet: nc.Cluster.Network.ServiceSubnet[0],
},
APIServer: kubeadm.APIServer{
ControlPlaneComponent: kubeadm.ControlPlaneComponent{
ExtraArgs: nc.Cluster.APIServer.ExtraArgs,
},
CertSANs: nc.Cluster.APIServer.CertSANs,
TimeoutForControlPlane: &metav1.Duration{
Duration: time.Duration(0),
},
},
ControllerManager: kubeadm.ControlPlaneComponent{
ExtraArgs: nc.Cluster.ControllerManager.ExtraArgs,
},
Scheduler: kubeadm.ControlPlaneComponent{
ExtraArgs: nc.Cluster.Scheduler.ExtraArgs,
},
}
kubeletConfig := &kubeletconfig.KubeletConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "KubeletConfiguration",
APIVersion: "kubelet.config.k8s.io/v1beta1",
},
FeatureGates: map[string]bool{
"ExperimentalCriticalPodAnnotation": true,
},
}
proxyConfig := &kubeproxyconfig.KubeProxyConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "KubeProxyConfiguration",
APIVersion: "kubeproxy.config.k8s.io/v1alpha1",
},
Mode: "ipvs",
IPVS: kubeproxyconfig.KubeProxyIPVSConfiguration{
Scheduler: "lc",
},
}
ud.Services.Kubeadm.InitConfiguration = initConfig
ud.Services.Kubeadm.ClusterConfiguration = clusterConfig
ud.Services.Kubeadm.KubeletConfiguration = kubeletConfig
ud.Services.Kubeadm.KubeProxyConfiguration = proxyConfig
return nil
}
func translateV1ControlPlane(nc *v1.NodeConfig, ud *userdata.UserData) error {
// Convert and decode certs back to byte slices
osCert, err := base64.StdEncoding.DecodeString(nc.Machine.CA.Crt)
if err != nil {
return err
}
osKey, err := base64.StdEncoding.DecodeString(nc.Machine.CA.Key)
if err != nil {
return err
}
// Inject certs and SANs
ud.Security.OS = &userdata.OSSecurity{
CA: &x509.PEMEncodedCertificateAndKey{
Crt: osCert,
Key: osKey,
},
}
ud.Services.Trustd.CertSANs = []string{nc.Cluster.ControlPlane.IPs[nc.Cluster.ControlPlane.Index], "127.0.0.1", "::1"}
ud.Services.Kubeadm.ControlPlane = true
// Craft a control plane kubeadm config
controlPlaneConfig := &kubeadm.JoinConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "JoinConfiguration",
APIVersion: "kubeadm.k8s.io/v1beta2",
},
ControlPlane: &kubeadm.JoinControlPlane{},
Discovery: kubeadm.Discovery{
BootstrapToken: &kubeadm.BootstrapTokenDiscovery{
Token: nc.Cluster.Token,
APIServerEndpoint: nc.Cluster.ControlPlane.IPs[nc.Cluster.ControlPlane.Index-1] + ":6443",
UnsafeSkipCAVerification: true,
},
},
NodeRegistration: kubeadm.NodeRegistrationOptions{
KubeletExtraArgs: nc.Machine.Kubelet.ExtraArgs,
},
}
ud.Services.Kubeadm.JoinConfiguration = controlPlaneConfig
return nil
}
func translateV1Worker(nc *v1.NodeConfig, ud *userdata.UserData) {
//Craft a worker kubeadm config
workerConfig := &kubeadm.JoinConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "JoinConfiguration",
APIVersion: "kubeadm.k8s.io/v1beta2",
},
Discovery: kubeadm.Discovery{
BootstrapToken: &kubeadm.BootstrapTokenDiscovery{
Token: nc.Cluster.Token,
APIServerEndpoint: nc.Cluster.ControlPlane.IPs[0] + ":443",
UnsafeSkipCAVerification: true,
},
},
NodeRegistration: kubeadm.NodeRegistrationOptions{
KubeletExtraArgs: nc.Machine.Kubelet.ExtraArgs,
},
}
ud.Services.Kubeadm.JoinConfiguration = workerConfig
}

View File

@ -5,12 +5,6 @@
package userdata
import (
"encoding/base64"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -26,22 +20,6 @@ func TestValidateSuite(t *testing.T) {
suite.Run(t, new(validateSuite))
}
func (suite *validateSuite) TestDownloadRetry() {
// Disable logging for test
log.SetOutput(ioutil.Discard)
ts := testUDServer()
defer ts.Close()
var err error
_, err = Download(ts.URL, WithMaxWait(0.1))
suite.Require().NoError(err)
_, err = Download(ts.URL, WithFormat(b64), WithRetries(1), WithHeaders(map[string]string{"Metadata": "true", "format": b64}))
suite.Require().NoError(err)
log.SetOutput(os.Stderr)
}
func (suite *validateSuite) TestKubeadmMarshal() {
var kubeadm Kubeadm
@ -56,97 +34,6 @@ func (suite *validateSuite) TestKubeadmMarshal() {
assert.Equal(suite.T(), kubeadmConfig, string(out))
}
func testUDServer() *httptest.Server {
var count int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count++
log.Printf("Request %d\n", count)
if count < 2 {
w.WriteHeader(http.StatusInternalServerError)
}
if r.Header.Get("format") == b64 {
// nolint: errcheck
w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(testConfig))))
} else {
// nolint: errcheck
w.Write([]byte(testConfig))
}
}))
return ts
}
// nolint: lll
const testConfig = `version: "1"
security:
os:
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
identity:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
kubernetes:
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
sa:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
frontproxy:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
etcd:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
networking:
os: {}
kubernetes: {}
services:
init:
cni: flannel
kubeadm:
initToken: 528d1ad6-3485-49ad-94cd-0f44a35877ac
certificateKey: 'test'
configuration: |
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
localAPIEndpoint:
bindPort: 6443
bootstrapTokens:
- token: '1qbsj9.3oz5hsk6grdfp98b'
ttl: 0s
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
clusterName: test
kubernetesVersion: v1.16.0-alpha.3
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
ipvs:
scheduler: lc
trustd:
username: 'test'
password: 'test'
endpoints: [ "1.2.3.4" ]
certSANs: []
install:
wipe: true
force: true
boot:
force: true
device: /dev/sda
size: 1024000000
ephemeral:
force: true
device: /dev/sda
size: 1024000000
`
// nolint: lll
const kubeadmConfig = `configuration: |
apiVersion: kubeadm.k8s.io/v1beta2
@ -189,4 +76,5 @@ const kubeadmConfig = `configuration: |
scheduler: {}
certificateKey: test
initToken: 528d1ad6-3485-49ad-94cd-0f44a35877ac
controlplane: true
`

View File

@ -0,0 +1,64 @@
/* 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 v1
import "github.com/talos-systems/talos/pkg/userdata/token"
// ClusterConfig reperesents the cluster-wide config values
type ClusterConfig struct {
ControlPlane *ControlPlaneConfig `yaml:"controlPlane"`
ClusterName string `yaml:"clusterName,omitempty"`
Network *ClusterNetworkConfig `yaml:"network,omitempty"`
Token string `yaml:"token,omitempty"`
InitToken *token.Token `yaml:"initToken,omitempty"`
CA *ClusterCAConfig `yaml:"ca,omitempty"`
APIServer *APIServerConfig `yaml:"apiServer,omitempty"`
ControllerManager *ControllerManagerConfig `yaml:"controllerManager,omitempty"`
Scheduler *SchedulerConfig `yaml:"scheduler,omitempty"`
Etcd *EtcdConfig `yaml:"etcd,omitempty"`
}
// ControlPlaneConfig represents control plane config vals
type ControlPlaneConfig struct {
IPs []string `yaml:"ips"`
Index int `yaml:"index,omitempty"`
}
// APIServerConfig represents kube apiserver config vals
type APIServerConfig struct {
Image string `yaml:"image,omitempty"`
ExtraArgs map[string]string `yaml:"extraArgs,omitempty"`
CertSANs []string `yaml:"certSANs,omitempty"`
}
// ControllerManagerConfig represents kube controller manager config vals
type ControllerManagerConfig struct {
Image string `yaml:"image,omitempty"`
ExtraArgs map[string]string `yaml:"extraArgs,omitempty"`
}
// SchedulerConfig represents kube scheduler config vals
type SchedulerConfig struct {
Image string `yaml:"image,omitempty"`
ExtraArgs map[string]string `yaml:"extraArgs,omitempty"`
}
// EtcdConfig represents etcd config vals
type EtcdConfig struct {
Image string `yaml:"image,omitempty"`
}
// ClusterNetworkConfig represents kube networking config vals
type ClusterNetworkConfig struct {
DNSDomain string `yaml:"dnsDomain"`
PodSubnet []string `yaml:"podSubnets"`
ServiceSubnet []string `yaml:"serviceSubnets"`
}
// ClusterCAConfig represents kube cert config vals
type ClusterCAConfig struct {
Crt string `yaml:"crt"`
Key string `yaml:"key"`
}

40
pkg/userdata/v1/errors.go Normal file
View File

@ -0,0 +1,40 @@
/* 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 v1 provides user-facing v1 machine configs
// nolint: dupl
package v1
import "errors"
var (
// General
// ErrRequiredSection denotes a section is required
ErrRequiredSection = errors.New("required userdata section")
// ErrInvalidVersion denotes that the config file version is invalid
ErrInvalidVersion = errors.New("invalid config version")
// Security
// ErrInvalidCert denotes that the certificate specified is invalid
ErrInvalidCert = errors.New("certificate is invalid")
// ErrInvalidCertType denotes that the certificate type is invalid
ErrInvalidCertType = errors.New("certificate type is invalid")
// Services
// ErrUnsupportedCNI denotes that the specified CNI is invalid
ErrUnsupportedCNI = errors.New("unsupported CNI driver")
// ErrInvalidTrustdToken denotes that a trustd token has not been specified
ErrInvalidTrustdToken = errors.New("trustd token is invalid")
// Networking
// ErrBadAddressing denotes that an incorrect combination of network
// address methods have been specified
ErrBadAddressing = errors.New("invalid network device addressing method")
// ErrInvalidAddress denotes that a bad address was provided
ErrInvalidAddress = errors.New("invalid network address")
)

View File

@ -0,0 +1,45 @@
/* 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 (
v1 "github.com/talos-systems/talos/pkg/userdata/v1"
yaml "gopkg.in/yaml.v2"
)
func controlPlaneUd(in *Input) (string, error) {
machine := &v1.MachineConfig{
Type: "controlplane",
Token: in.TrustdInfo.Token,
CA: &v1.MachineCAConfig{
Crt: in.Certs.OsCert,
Key: in.Certs.OsKey,
},
Kubelet: &v1.KubeletConfig{},
Network: &v1.NetworkConfig{},
}
cluster := &v1.ClusterConfig{
Token: in.KubeadmTokens.BootstrapToken,
ControlPlane: &v1.ControlPlaneConfig{
IPs: in.MasterIPs,
Index: in.Index,
},
}
ud := v1.NodeConfig{
Version: "v1",
Machine: machine,
Cluster: cluster,
}
udMarshal, err := yaml.Marshal(ud)
if err != nil {
return "", err
}
return string(udMarshal), nil
}

View File

@ -0,0 +1,367 @@
/* 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"
"github.com/talos-systems/talos/pkg/userdata/token"
)
// 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
MasterIPs []string
AdditionalSubjectAltNames []string
ClusterName string
ServiceDomain string
PodNet []string
ServiceNet []string
KubernetesVersion string
KubeadmTokens *KubeadmTokens
TrustdInfo *TrustdInfo
InitToken *token.Token
//
// 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 first master node
func (i *Input) GetControlPlaneEndpoint(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
}
// Create the init token
tok, err := token.NewToken()
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,
InitToken: tok,
}
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")
}

View File

@ -0,0 +1,64 @@
/* 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_test
import (
"net"
"testing"
"github.com/stretchr/testify/suite"
v1 "github.com/talos-systems/talos/pkg/userdata/v1"
udgenv1 "github.com/talos-systems/talos/pkg/userdata/v1/generate"
"gopkg.in/yaml.v2"
)
var (
input *udgenv1.Input
)
type GenerateSuite struct {
suite.Suite
}
func TestGenerateSuite(t *testing.T) {
suite.Run(t, new(GenerateSuite))
}
func (suite *GenerateSuite) SetupSuite() {
var err error
input, err = udgenv1.NewInput("test", []string{"10.0.1.5", "10.0.1.6", "10.0.1.7"})
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateInitSuccess() {
input.IP = net.ParseIP("10.0.1.5")
dataString, err := udgenv1.Userdata(udgenv1.TypeInit, input)
suite.Require().NoError(err)
data := &v1.NodeConfig{}
err = yaml.Unmarshal([]byte(dataString), data)
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateControlPlaneSuccess() {
input.IP = net.ParseIP("10.0.1.6")
dataString, err := udgenv1.Userdata(udgenv1.TypeControlPlane, input)
suite.Require().NoError(err)
data := &v1.NodeConfig{}
err = yaml.Unmarshal([]byte(dataString), data)
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateWorkerSuccess() {
dataString, err := udgenv1.Userdata(udgenv1.TypeJoin, input)
suite.Require().NoError(err)
data := &v1.NodeConfig{}
err = yaml.Unmarshal([]byte(dataString), data)
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateTalosconfigSuccess() {
_, err := udgenv1.Talosconfig(input)
suite.Require().NoError(err)
}

View File

@ -0,0 +1,64 @@
/* 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 (
v1 "github.com/talos-systems/talos/pkg/userdata/v1"
yaml "gopkg.in/yaml.v2"
)
func initUd(in *Input) (string, error) {
machine := &v1.MachineConfig{
Type: "init",
Kubelet: &v1.KubeletConfig{},
Network: &v1.NetworkConfig{},
CA: &v1.MachineCAConfig{
Crt: in.Certs.OsCert,
Key: in.Certs.OsKey,
},
Token: in.TrustdInfo.Token,
}
certSANs := in.GetAPIServerSANs()
cluster := &v1.ClusterConfig{
ClusterName: in.ClusterName,
ControlPlane: &v1.ControlPlaneConfig{
IPs: in.MasterIPs,
Index: in.Index,
},
APIServer: &v1.APIServerConfig{
CertSANs: certSANs,
},
ControllerManager: &v1.ControllerManagerConfig{},
Scheduler: &v1.SchedulerConfig{},
Etcd: &v1.EtcdConfig{},
Network: &v1.ClusterNetworkConfig{
DNSDomain: in.ServiceDomain,
PodSubnet: in.PodNet,
ServiceSubnet: in.ServiceNet,
},
CA: &v1.ClusterCAConfig{
Crt: in.Certs.K8sCert,
Key: in.Certs.K8sKey,
},
Token: in.KubeadmTokens.BootstrapToken,
InitToken: in.InitToken,
}
ud := v1.NodeConfig{
Version: "v1",
Machine: machine,
Cluster: cluster,
}
udMarshal, err := yaml.Marshal(ud)
if err != nil {
return "", err
}
return string(udMarshal), nil
}

View File

@ -0,0 +1,40 @@
/* 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 (
v1 "github.com/talos-systems/talos/pkg/userdata/v1"
yaml "gopkg.in/yaml.v2"
)
func workerUd(in *Input) (string, error) {
machine := &v1.MachineConfig{
Type: "worker",
Token: in.TrustdInfo.Token,
Kubelet: &v1.KubeletConfig{},
Network: &v1.NetworkConfig{},
}
cluster := &v1.ClusterConfig{
Token: in.KubeadmTokens.BootstrapToken,
ControlPlane: &v1.ControlPlaneConfig{
IPs: in.MasterIPs,
},
}
ud := v1.NodeConfig{
Version: "v1",
Machine: machine,
Cluster: cluster,
}
udMarshal, err := yaml.Marshal(ud)
if err != nil {
return "", err
}
return string(udMarshal), nil
}

View File

@ -0,0 +1,41 @@
/* 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 (
"bytes"
"html/template"
)
// Talosconfig returns the talos admin Talos config.
func Talosconfig(in *Input) (string, error) {
return renderTemplate(in, talosconfigTempl)
}
const talosconfigTempl = `context: {{ .ClusterName }}
contexts:
{{ .ClusterName }}:
target: {{ index .MasterIPs 0 }}
ca: {{ .Certs.OsCert }}
crt: {{ .Certs.AdminCert }}
key: {{ .Certs.AdminKey }}
`
// renderTemplate will output a templated string.
func renderTemplate(in *Input, udTemplate string) (string, error) {
// So we can have a simple add func
funcs := template.FuncMap{"add": add}
templ := template.Must(template.New("udTemplate").Funcs(funcs).Parse(udTemplate))
var buf bytes.Buffer
if err := templ.Execute(&buf, in); err != nil {
return "", err
}
return buf.String(), nil
}
func add(a, b int) int {
return a + b
}

View File

@ -0,0 +1,51 @@
/* 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 v1 provides user-facing v1 machine configs
//nolint: dupl
package v1
// Install represents the installation options for preparing a node.
type Install struct {
Boot *BootDisk `yaml:"boot,omitempty"`
Ephemeral *InstallDisk `yaml:"ephemeral,omitempty"`
ExtraDisks []*ExtraDisk `yaml:"extraDisks,omitempty"`
ExtraKernelArgs []string `yaml:"extraKernelArgs,omitempty"`
Wipe bool `yaml:"wipe"`
Force bool `yaml:"force"`
}
// BootDisk represents the install options specific to the boot partition.
type BootDisk struct {
InstallDisk `yaml:",inline"`
Kernel string `yaml:"kernel"`
Initramfs string `yaml:"initramfs"`
}
// RootDisk represents the install options specific to the root partition.
type RootDisk struct {
InstallDisk `yaml:",inline"`
Rootfs string `yaml:"rootfs"`
}
// InstallDisk represents the specific directions for each partition.
type InstallDisk struct {
Disk string `yaml:"disk,omitempty"`
Size uint `yaml:"size,omitempty"`
}
// ExtraDisk represents the options available for partitioning, formatting,
// and mounting extra disks.
type ExtraDisk struct {
Disk string `yaml:"disk,omitempty"`
Partitions []*ExtraDiskPartition `yaml:"partitions,omitempty"`
}
// ExtraDiskPartition represents the options for a device partition.
type ExtraDiskPartition struct {
Size uint `yaml:"size,omitempty"`
MountPoint string `yaml:"mountpoint,omitempty"`
}

View File

@ -0,0 +1,33 @@
/* 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 v1
// MachineConfig reperesents the machine-specific config values
type MachineConfig struct {
Type string `yaml:"type"`
Token string `yaml:"token"`
CA *MachineCAConfig `yaml:"ca,omitempty"`
Kubelet *KubeletConfig `yaml:"kubelet,omitempty"`
Network *NetworkConfig `yaml:"network,omitempty"`
Install *Install `yaml:"install,omitempty"`
}
// KubeletConfig reperesents the kubelet config values
type KubeletConfig struct {
Image string `yaml:"image,omitempty"`
ExtraArgs map[string]string `yaml:"extraArgs,omitempty"`
}
// NetworkConfig reperesents the machine's networking config values
type NetworkConfig struct {
Hostname string `yaml:"hostname,omitempty"`
Interfaces []*Device `yaml:"interfaces,omitempty"`
}
// MachineCAConfig reperesents the machine's talos cert config values
type MachineCAConfig struct {
Crt string `yaml:"crt,omitempty"`
Key string `yaml:"key,omitempty"`
}

View File

@ -0,0 +1,124 @@
/* 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 v1 provides user-facing v1 machine configs
// nolint: dupl
package v1
import (
"net"
"strconv"
"github.com/hashicorp/go-multierror"
"golang.org/x/xerrors"
)
// Device represents a network interface
// nolint: dupl
type Device struct {
Interface string `yaml:"interface"`
CIDR string `yaml:"cidr"`
DHCP bool `yaml:"dhcp"`
Routes []Route `yaml:"routes"`
Bond *Bond `yaml:"bond"`
MTU int `yaml:"mtu"`
}
// NetworkDeviceCheck defines the function type for checks
// nolint: dupl
type NetworkDeviceCheck func(*Device) error
// Validate triggers the specified validation checks to run
// nolint: dupl
func (d *Device) Validate(checks ...NetworkDeviceCheck) error {
var result *multierror.Error
for _, check := range checks {
result = multierror.Append(result, check(d))
}
return result.ErrorOrNil()
}
// CheckDeviceInterface ensures that the interface has been specified
// nolint: dupl
func CheckDeviceInterface() NetworkDeviceCheck {
return func(d *Device) error {
var result *multierror.Error
if d.Interface == "" {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.interface", "", ErrRequiredSection))
}
return result.ErrorOrNil()
}
}
// CheckDeviceAddressing ensures that an appropriate addressing method
// has been specified
// nolint: dupl
func CheckDeviceAddressing() NetworkDeviceCheck {
return func(d *Device) error {
var result *multierror.Error
// Test for both dhcp and cidr specified
if d.DHCP && d.CIDR != "" {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device", "", ErrBadAddressing))
}
// test for neither dhcp nor cidr specified
if !d.DHCP && d.CIDR == "" {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device", "", ErrBadAddressing))
}
// ensure cidr is a valid address
if d.CIDR != "" {
if _, _, err := net.ParseCIDR(d.CIDR); err != nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.CIDR", "", err))
}
}
return result.ErrorOrNil()
}
}
// CheckDeviceRoutes ensures that the specified routes are valid
// nolint: dupl
func CheckDeviceRoutes() NetworkDeviceCheck {
return func(d *Device) error {
var result *multierror.Error
if len(d.Routes) == 0 {
return result.ErrorOrNil()
}
for idx, route := range d.Routes {
if _, _, err := net.ParseCIDR(route.Network); err != nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.route["+strconv.Itoa(idx)+"].Network", route.Network, ErrInvalidAddress))
}
if ip := net.ParseIP(route.Gateway); ip == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.route["+strconv.Itoa(idx)+"].Gateway", route.Gateway, ErrInvalidAddress))
}
}
return result.ErrorOrNil()
}
}
// Bond contains the various options for configuring a
// bonded interface
// nolint: dupl
type Bond struct {
Mode string `yaml:"mode"`
HashPolicy string `yaml:"hashpolicy"`
LACPRate string `yaml:"lacprate"`
Interfaces []string `yaml:"interfaces"`
}
// Route represents a network route
// nolint: dupl
type Route struct {
Network string `yaml:"network"`
Gateway string `yaml:"gateway"`
}

View File

@ -0,0 +1,12 @@
/* 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 v1
// NodeConfig holds the full representation of the node config
type NodeConfig struct {
Version string
Machine *MachineConfig
Cluster *ClusterConfig
}