Skip to content

Commit eccb21d

Browse files
committed
feat: add presets to the 'cluster create qemu' command
* add 'iso', 'pxe', 'disk-image', 'maintenance' and 'secureboot' presets * swith the image-factory e2e test to use the create qemu command with presets * add a '--omni-api-endpoint' to simplify connecting machines to omni Signed-off-by: Orzelius <33936483+Orzelius@users.noreply.github.com>
1 parent ec0a813 commit eccb21d

File tree

25 files changed

+865
-118
lines changed

25 files changed

+865
-118
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package constants
6+
7+
const (
8+
// ImageFactoryEmptySchematicID is the ID of an empty image factory schematic.
9+
ImageFactoryEmptySchematicID = "376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba"
10+
11+
// ImageFactoryURL is the url of the Sidero hosted image factory.
12+
ImageFactoryURL = "https://factory.talos.dev/"
13+
)

cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common.go

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/netip"
1212
"os"
1313
"slices"
14+
"strconv"
1415
"strings"
1516

1617
"github.com/google/uuid"
@@ -23,8 +24,10 @@ import (
2324
"github.com/siderolabs/talos/pkg/machinery/config"
2425
"github.com/siderolabs/talos/pkg/machinery/config/bundle"
2526
"github.com/siderolabs/talos/pkg/machinery/config/configpatcher"
27+
"github.com/siderolabs/talos/pkg/machinery/config/container"
2628
"github.com/siderolabs/talos/pkg/machinery/config/generate"
2729
"github.com/siderolabs/talos/pkg/machinery/config/machine"
30+
"github.com/siderolabs/talos/pkg/machinery/config/types/siderolink"
2831
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
2932
"github.com/siderolabs/talos/pkg/machinery/constants"
3033
"github.com/siderolabs/talos/pkg/provision"
@@ -67,6 +70,7 @@ type Maker[ExtraOps any] struct {
6770
Cidrs []netip.Prefix
6871
InClusterEndpoint string
6972
Endpoints []string
73+
WithOmni bool
7074

7175
ProvisionOps []provision.Option
7276
GenOps []generate.Option
@@ -101,15 +105,18 @@ func (m *Maker[T]) InitExtra() error {
101105
return err
102106
}
103107

104-
if err := m.extraOptionsProvider.AddExtraGenOps(); err != nil {
105-
return err
106-
}
108+
// skip generating machine config if nodes are to be used with omni
109+
if !m.WithOmni {
110+
if err := m.extraOptionsProvider.AddExtraGenOps(); err != nil {
111+
return err
112+
}
107113

108-
if err := m.extraOptionsProvider.AddExtraProvisionOpts(); err != nil {
109-
return err
114+
if err := m.extraOptionsProvider.AddExtraConfigBundleOpts(); err != nil {
115+
return err
116+
}
110117
}
111118

112-
if err := m.extraOptionsProvider.AddExtraConfigBundleOpts(); err != nil {
119+
if err := m.extraOptionsProvider.AddExtraProvisionOpts(); err != nil {
113120
return err
114121
}
115122

@@ -125,7 +132,13 @@ func (m *Maker[T]) InitExtra() error {
125132
}
126133

127134
// InitCommon initializes the common fields.
135+
//
136+
//nolint:gocyclo
128137
func (m *Maker[T]) InitCommon() error {
138+
if m.Ops.OmniAPIEndpoint != "" {
139+
m.WithOmni = true
140+
}
141+
129142
if err := m.initVersionContract(); err != nil {
130143
return err
131144
}
@@ -152,15 +165,18 @@ func (m *Maker[T]) InitCommon() error {
152165
return err
153166
}
154167

155-
if err := m.initGenOps(); err != nil {
156-
return err
157-
}
168+
// skip generating machine config if nodes are to be used with omni
169+
if !m.WithOmni {
170+
if err := m.initGenOps(); err != nil {
171+
return err
172+
}
158173

159-
if err := m.initProvisionOps(); err != nil {
160-
return err
174+
if err := m.initConfigBundleOps(); err != nil {
175+
return err
176+
}
161177
}
162178

163-
if err := m.initConfigBundleOps(); err != nil {
179+
if err := m.initProvisionOps(); err != nil {
164180
return err
165181
}
166182

@@ -210,6 +226,53 @@ func (m *Maker[T]) initVersionContract() error {
210226
// GetClusterConfigs prepares and returns the cluster create request data. This method is ment to be called after the implemeting maker
211227
// logic has been run.
212228
func (m *Maker[T]) GetClusterConfigs() (clusterops.ClusterConfigs, error) {
229+
var configBundle *bundle.Bundle
230+
231+
if !m.WithOmni {
232+
cfgBundle, err := m.finalizeMachineConfigs()
233+
if err != nil {
234+
return clusterops.ClusterConfigs{}, err
235+
}
236+
237+
configBundle = cfgBundle
238+
} else {
239+
err := m.applyOmniConfigs()
240+
if err != nil {
241+
return clusterops.ClusterConfigs{}, err
242+
}
243+
}
244+
245+
return clusterops.ClusterConfigs{
246+
ClusterRequest: m.ClusterRequest,
247+
ProvisionOptions: m.ProvisionOps,
248+
ConfigBundle: configBundle,
249+
}, nil
250+
}
251+
252+
func (m *Maker[T]) applyOmniConfigs() error {
253+
cfg := siderolink.NewConfigV1Alpha1()
254+
255+
parsedURL, err := ParseOmniAPIUrl(m.Ops.OmniAPIEndpoint)
256+
if err != nil {
257+
return fmt.Errorf("error parsing omni api url: %w", err)
258+
}
259+
260+
cfg.APIUrlConfig.URL = parsedURL
261+
262+
ctr, err := container.New(cfg)
263+
if err != nil {
264+
return err
265+
}
266+
267+
m.ForEachNode(func(i int, node *provision.NodeRequest) {
268+
node.Config = ctr
269+
node.Name = m.Ops.RootOps.ClusterName + "-machine-" + strconv.Itoa(i+1)
270+
})
271+
272+
return nil
273+
}
274+
275+
func (m *Maker[T]) finalizeMachineConfigs() (*bundle.Bundle, error) {
213276
// These options needs to be generated after the implementing maker has made changes to the cluster request.
214277
m.GenOps = slices.Concat(m.GenOps, m.Provisioner.GenOptions(m.ClusterRequest.Network, m.VersionContract))
215278
m.GenOps = slices.Concat(m.GenOps, []generate.Option{generate.WithEndpointList(m.Endpoints)})
@@ -226,7 +289,7 @@ func (m *Maker[T]) GetClusterConfigs() (clusterops.ClusterConfigs, error) {
226289

227290
configBundle, err := bundle.NewBundle(m.ConfigBundleOps...)
228291
if err != nil {
229-
return clusterops.ClusterConfigs{}, err
292+
return nil, err
230293
}
231294

232295
if m.ClusterRequest.Nodes[0].Type == machine.TypeInit {
@@ -243,15 +306,15 @@ func (m *Maker[T]) GetClusterConfigs() (clusterops.ClusterConfigs, error) {
243306
if m.Ops.WireguardCIDR != "" {
244307
wireguardConfigBundle, err := helpers.NewWireguardConfigBundle(m.IPs[0], m.Ops.WireguardCIDR, 51111, m.Ops.Controlplanes)
245308
if err != nil {
246-
return clusterops.ClusterConfigs{}, err
309+
return nil, err
247310
}
248311

249312
for i := range m.ClusterRequest.Nodes {
250313
node := &m.ClusterRequest.Nodes[i]
251314

252315
patchedCfg, err := wireguardConfigBundle.PatchConfig(node.IPs[0], node.Config)
253316
if err != nil {
254-
return clusterops.ClusterConfigs{}, err
317+
return nil, err
255318
}
256319

257320
node.Config = patchedCfg
@@ -260,11 +323,7 @@ func (m *Maker[T]) GetClusterConfigs() (clusterops.ClusterConfigs, error) {
260323

261324
m.ProvisionOps = append(m.ProvisionOps, provision.WithTalosConfig(configBundle.TalosConfig()))
262325

263-
return clusterops.ClusterConfigs{
264-
ClusterRequest: m.ClusterRequest,
265-
ProvisionOptions: m.ProvisionOps,
266-
ConfigBundle: configBundle,
267-
}, nil
326+
return configBundle, nil
268327
}
269328

270329
// ForEachNode iterates over all nodes allowing modification of each node.

cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,24 @@ func TestCommonMaker(t *testing.T) {
119119

120120
_, err = m.GetClusterConfigs()
121121
assert.NoError(t, err)
122+
123+
m.Ops.OmniAPIEndpoint = "grpc://10.5.0.1:8090?jointoken=my-token"
124+
err = m.Init()
125+
assert.NoError(t, err)
126+
127+
clusterCfgs, err := m.GetClusterConfigs()
128+
assert.NoError(t, err)
129+
130+
req := clusterCfgs.ClusterRequest
131+
assert.Equal(t, "test-cluster-machine-1", req.Nodes[0].Name)
132+
assert.Equal(t, "test-cluster-machine-2", req.Nodes[1].Name)
133+
134+
cfgBytes, err := req.Nodes[0].Config.Bytes()
135+
assert.NoError(t, err)
136+
137+
assert.Contains(t, string(cfgBytes), "apiVersion: v1alpha1")
138+
assert.Contains(t, string(cfgBytes), "kind: SideroLinkConfig")
139+
assert.Contains(t, string(cfgBytes), "apiUrl: grpc://10.5.0.1:8090?jointoken=my-token")
122140
}
123141

124142
func TestCommonMaker_MachineConfig(t *testing.T) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package makers
6+
7+
import (
8+
"fmt"
9+
"net/url"
10+
"strings"
11+
)
12+
13+
// ParseOmniAPIUrl validates and parses the omni api url.
14+
func ParseOmniAPIUrl(urlIn string) (*url.URL, error) {
15+
if !strings.HasPrefix(urlIn, "grpc://") && !strings.HasPrefix(urlIn, "https://") {
16+
return nil, fmt.Errorf("invalid url scheme: must be either 'grpc://' or 'https://'")
17+
}
18+
19+
if !strings.Contains(urlIn, "?jointoken=") {
20+
return nil, fmt.Errorf("invalid url: must contain a jointoken query parameter")
21+
}
22+
23+
url, err := url.Parse(urlIn)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
if url.Port() == "" {
29+
return nil, fmt.Errorf("invalid url: must contain a port")
30+
}
31+
32+
return url, nil
33+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package makers_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
12+
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers"
13+
)
14+
15+
func TestParseOmniAPIUrl(t *testing.T) {
16+
t.Run("valid grpc", func(t *testing.T) {
17+
u, err := makers.ParseOmniAPIUrl("grpc://10.5.0.1:8090?jointoken=abc")
18+
assert.NoError(t, err)
19+
20+
if assert.NotNil(t, u) {
21+
assert.Equal(t, "grpc", u.Scheme)
22+
assert.Equal(t, "10.5.0.1:8090", u.Host)
23+
assert.Equal(t, "abc", u.Query().Get("jointoken"))
24+
}
25+
})
26+
27+
t.Run("valid https", func(t *testing.T) {
28+
u, err := makers.ParseOmniAPIUrl("https://example.com:443?jointoken=token123")
29+
assert.NoError(t, err)
30+
31+
if assert.NotNil(t, u) {
32+
assert.Equal(t, "https", u.Scheme)
33+
assert.Equal(t, "example.com:443", u.Host)
34+
assert.Equal(t, "token123", u.Query().Get("jointoken"))
35+
}
36+
})
37+
38+
t.Run("invalid scheme", func(t *testing.T) {
39+
u, err := makers.ParseOmniAPIUrl("http://10.5.0.1:8090?jointoken=abc")
40+
assert.Error(t, err)
41+
assert.Nil(t, u)
42+
})
43+
44+
t.Run("missing jointoken", func(t *testing.T) {
45+
u, err := makers.ParseOmniAPIUrl("grpc://10.5.0.1:8090")
46+
assert.Error(t, err)
47+
assert.Nil(t, u)
48+
})
49+
50+
t.Run("missing port", func(t *testing.T) {
51+
u, err := makers.ParseOmniAPIUrl("grpc://10.5.0.1?jointoken=abc")
52+
assert.Error(t, err)
53+
assert.Nil(t, u)
54+
})
55+
}

cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,30 @@ func (m *Qemu) InitExtra() error {
9999
m.SideroLinkBuilder = slb
100100
}
101101

102+
m.initEndpoints()
103+
102104
if m.Ops.WithJSONLogs {
103105
m.initJSONLogs()
104106
}
105107

106108
return nil
107109
}
108110

111+
func (m *Qemu) initEndpoints() {
112+
switch {
113+
case m.Ops.ForceEndpoint != "":
114+
// using non-default endpoints, provision additional cert SANs and fix endpoint list
115+
m.Endpoints = []string{m.Ops.ForceEndpoint}
116+
case m.Ops.ForceInitNodeAsEndpoint:
117+
m.Endpoints = []string{m.IPs[0][0].String()}
118+
case m.Endpoints == nil:
119+
// use control plane nodes as endpoints, client-side load-balancing
120+
for i := range m.Ops.Controlplanes {
121+
m.Endpoints = slices.Concat(m.Endpoints, []string{m.IPs[0][i].String()})
122+
}
123+
}
124+
}
125+
109126
// AddExtraGenOps implements ExtraOptionsProvider.
110127
func (m *Qemu) AddExtraGenOps() error {
111128
m.GenOps = slices.Concat(m.GenOps, []generate.Option{generate.WithInstallImage(m.EOps.NodeInstallImage)})
@@ -134,18 +151,8 @@ func (m *Qemu) AddExtraGenOps() error {
134151
})
135152
}
136153

137-
switch {
138-
case m.Ops.ForceEndpoint != "":
139-
// using non-default endpoints, provision additional cert SANs and fix endpoint list
140-
m.Endpoints = []string{m.Ops.ForceEndpoint}
154+
if m.Ops.ForceEndpoint != "" {
141155
m.GenOps = slices.Concat(m.GenOps, []generate.Option{generate.WithAdditionalSubjectAltNames(m.Endpoints)})
142-
case m.Ops.ForceInitNodeAsEndpoint:
143-
m.Endpoints = []string{m.IPs[0][0].String()}
144-
case m.Endpoints == nil:
145-
// use control plane nodes as endpoints, client-side load-balancing
146-
for i := range m.Ops.Controlplanes {
147-
m.Endpoints = slices.Concat(m.Endpoints, []string{m.IPs[0][i].String()})
148-
}
149156
}
150157

151158
return nil

0 commit comments

Comments
 (0)