mirror of
https://github.com/siderolabs/talos.git
synced 2025-11-29 22:51:35 +01:00
feat: add config code entry screen to dashboard
Implement a screen for entering/managing the config `${code}` variable.
Enable this screen only when the platform is `metal` and there is a `${code}` variable in the `talos.config` kernel cmdline URL query.
Additionally, remove the "Delete" button and its functionality from the network config screen to avoid users accidentally deleting PlatformNetworkConfig parts that are not managed by the dashboard.
Add some tests for the form data parsing on the network config screen.
Remove the unnecessary lock on the summary tab - all updates come from the same goroutine.
Closes siderolabs/talos#6993.
Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
This commit is contained in:
parent
ddb014cfdc
commit
7967ccfc13
@ -9,12 +9,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/siderolabs/go-procfs/procfs"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform"
|
||||||
|
metalurl "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url"
|
||||||
"github.com/siderolabs/talos/internal/pkg/dashboard"
|
"github.com/siderolabs/talos/internal/pkg/dashboard"
|
||||||
"github.com/siderolabs/talos/pkg/grpc/middleware/authz"
|
"github.com/siderolabs/talos/pkg/grpc/middleware/authz"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/client"
|
"github.com/siderolabs/talos/pkg/machinery/client"
|
||||||
@ -51,7 +54,30 @@ func dashboardMain() error {
|
|||||||
currentPlatform, _ := platform.CurrentPlatform() //nolint:errcheck
|
currentPlatform, _ := platform.CurrentPlatform() //nolint:errcheck
|
||||||
if currentPlatform != nil && currentPlatform.Name() == constants.PlatformMetal {
|
if currentPlatform != nil && currentPlatform.Name() == constants.PlatformMetal {
|
||||||
screens = append(screens, dashboard.ScreenNetworkConfig)
|
screens = append(screens, dashboard.ScreenNetworkConfig)
|
||||||
|
|
||||||
|
if showConfigURLTab() {
|
||||||
|
screens = append(screens, dashboard.ScreenConfigURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dashboard.Run(adminCtx, c, dashboard.WithAllowExitKeys(false), dashboard.WithScreens(screens...))
|
return dashboard.Run(adminCtx, c, dashboard.WithAllowExitKeys(false), dashboard.WithScreens(screens...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showConfigURLTab() bool {
|
||||||
|
option := procfs.ProcCmdline().Get(constants.KernelParamConfig).First()
|
||||||
|
if option == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(*option)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
codeVar := metalurl.AllVariables()[constants.CodeKey]
|
||||||
|
if codeVar == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeVar.Matches(parsedURL.Query())
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import (
|
|||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/internal/netutils"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/internal/netutils"
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/internal/url"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url"
|
||||||
"github.com/siderolabs/talos/internal/pkg/meta"
|
"github.com/siderolabs/talos/internal/pkg/meta"
|
||||||
"github.com/siderolabs/talos/pkg/download"
|
"github.com/siderolabs/talos/pkg/download"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import (
|
|||||||
|
|
||||||
// Populate populates the config download URL with values replacing variables.
|
// Populate populates the config download URL with values replacing variables.
|
||||||
func Populate(ctx context.Context, downloadURL string, st state.State) (string, error) {
|
func Populate(ctx context.Context, downloadURL string, st state.State) (string, error) {
|
||||||
return PopulateVariables(ctx, downloadURL, st, AllVariables())
|
return PopulateVariables(ctx, downloadURL, st, maps.Values(AllVariables()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PopulateVariables populates the config download URL with values replacing variables.
|
// PopulateVariables populates the config download URL with values replacing variables.
|
||||||
@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/internal/url"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url"
|
||||||
"github.com/siderolabs/talos/internal/pkg/meta"
|
"github.com/siderolabs/talos/internal/pkg/meta"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
|
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/resources/hardware"
|
"github.com/siderolabs/talos/pkg/machinery/resources/hardware"
|
||||||
@ -28,27 +28,27 @@ type Variable struct {
|
|||||||
r *regexp.Regexp
|
r *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllVariables is a list of all supported variables.
|
// AllVariables is a map of all supported variables.
|
||||||
func AllVariables() []*Variable {
|
func AllVariables() map[string]*Variable {
|
||||||
return []*Variable{
|
return map[string]*Variable{
|
||||||
{
|
constants.UUIDKey: {
|
||||||
Key: constants.UUIDKey,
|
Key: constants.UUIDKey,
|
||||||
MatchOnArg: true,
|
MatchOnArg: true,
|
||||||
Value: UUIDValue(),
|
Value: UUIDValue(),
|
||||||
},
|
},
|
||||||
{
|
constants.SerialNumberKey: {
|
||||||
Key: constants.SerialNumberKey,
|
Key: constants.SerialNumberKey,
|
||||||
Value: SerialNumberValue(),
|
Value: SerialNumberValue(),
|
||||||
},
|
},
|
||||||
{
|
constants.MacKey: {
|
||||||
Key: constants.MacKey,
|
Key: constants.MacKey,
|
||||||
Value: MACValue(),
|
Value: MACValue(),
|
||||||
},
|
},
|
||||||
{
|
constants.HostnameKey: {
|
||||||
Key: constants.HostnameKey,
|
Key: constants.HostnameKey,
|
||||||
Value: HostnameValue(),
|
Value: HostnameValue(),
|
||||||
},
|
},
|
||||||
{
|
constants.CodeKey: {
|
||||||
Key: constants.CodeKey,
|
Key: constants.CodeKey,
|
||||||
Value: CodeValue(),
|
Value: CodeValue(),
|
||||||
},
|
},
|
||||||
@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/internal/url"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
184
internal/pkg/dashboard/configurl.go
Normal file
184
internal/pkg/dashboard/configurl.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// 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 dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
|
||||||
|
"github.com/siderolabs/talos/internal/pkg/meta"
|
||||||
|
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const unset = "[gray](unset)[-]"
|
||||||
|
|
||||||
|
type configURLData struct {
|
||||||
|
existingCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigURLGrid represents the config URL grid.
|
||||||
|
type ConfigURLGrid struct {
|
||||||
|
tview.Grid
|
||||||
|
|
||||||
|
dashboard *Dashboard
|
||||||
|
|
||||||
|
form *tview.Form
|
||||||
|
|
||||||
|
existingCode *tview.TextView
|
||||||
|
newCodeField *tview.InputField
|
||||||
|
infoView *tview.TextView
|
||||||
|
|
||||||
|
selectedNode string
|
||||||
|
nodeMap map[string]*configURLData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigURLGrid returns a new config URL grid.
|
||||||
|
func NewConfigURLGrid(ctx context.Context, dashboard *Dashboard) *ConfigURLGrid {
|
||||||
|
grid := &ConfigURLGrid{
|
||||||
|
Grid: *tview.NewGrid(),
|
||||||
|
dashboard: dashboard,
|
||||||
|
form: tview.NewForm(),
|
||||||
|
existingCode: tview.NewTextView().SetDynamicColors(true).SetLabel("Existing Code").SetText(unset).SetSize(1, 0).SetScrollable(false),
|
||||||
|
newCodeField: tview.NewInputField().SetLabel("New Code"),
|
||||||
|
infoView: tview.NewTextView().SetDynamicColors(true).SetSize(2, 0).SetScrollable(false),
|
||||||
|
|
||||||
|
nodeMap: make(map[string]*configURLData),
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.SetRows(-1, 12, -1).SetColumns(-1, 48, -1)
|
||||||
|
|
||||||
|
grid.form.SetBorder(true)
|
||||||
|
grid.form.AddFormItem(grid.existingCode)
|
||||||
|
grid.form.AddFormItem(grid.newCodeField)
|
||||||
|
grid.form.AddButton("Save", func() {
|
||||||
|
ctx = nodeContext(ctx, grid.selectedNode)
|
||||||
|
|
||||||
|
value := grid.newCodeField.GetText()
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
grid.infoView.SetText("[red]Error: No code entered[-]")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dashboard.cli.MetaWrite(ctx, meta.DownloadURLCode, []byte(value))
|
||||||
|
if err != nil {
|
||||||
|
grid.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.clearForm()
|
||||||
|
grid.infoView.SetText("[green]Saved successfully[-]")
|
||||||
|
grid.dashboard.selectScreen(ScreenSummary)
|
||||||
|
})
|
||||||
|
grid.form.AddButton("Delete", func() {
|
||||||
|
ctx = nodeContext(ctx, grid.selectedNode)
|
||||||
|
|
||||||
|
err := dashboard.cli.MetaDelete(ctx, meta.DownloadURLCode)
|
||||||
|
if err != nil {
|
||||||
|
if status.Code(err) == codes.NotFound {
|
||||||
|
grid.clearForm()
|
||||||
|
grid.infoView.SetText("[green]Already deleted[-]")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.clearForm()
|
||||||
|
grid.infoView.SetText("[green]Deleted successfully[-]")
|
||||||
|
})
|
||||||
|
|
||||||
|
grid.form.AddFormItem(grid.infoView)
|
||||||
|
|
||||||
|
grid.AddItem(tview.NewBox(), 0, 0, 1, 3, 0, 0, false)
|
||||||
|
grid.AddItem(tview.NewBox(), 1, 0, 1, 1, 0, 0, false)
|
||||||
|
grid.AddItem(grid.form, 1, 1, 1, 1, 0, 0, false)
|
||||||
|
grid.AddItem(tview.NewBox(), 1, 2, 1, 1, 0, 0, false)
|
||||||
|
grid.AddItem(tview.NewBox(), 2, 0, 1, 3, 0, 0, false)
|
||||||
|
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnScreenSelect implements the screenSelectListener interface.
|
||||||
|
func (widget *ConfigURLGrid) onScreenSelect(active bool) {
|
||||||
|
if active {
|
||||||
|
widget.dashboard.app.SetFocus(widget.form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnNodeSelect implements the NodeSelectListener interface.
|
||||||
|
func (widget *ConfigURLGrid) OnNodeSelect(node string) {
|
||||||
|
if node != widget.selectedNode {
|
||||||
|
widget.selectedNode = node
|
||||||
|
|
||||||
|
widget.clearForm()
|
||||||
|
widget.redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnResourceDataChange implements the ResourceDataListener interface.
|
||||||
|
func (widget *ConfigURLGrid) OnResourceDataChange(data resourcedata.Data) {
|
||||||
|
widget.updateNodeData(data)
|
||||||
|
|
||||||
|
if data.Node == widget.selectedNode {
|
||||||
|
widget.redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *ConfigURLGrid) updateNodeData(data resourcedata.Data) {
|
||||||
|
nodeData := widget.getOrCreateNodeData(data.Node)
|
||||||
|
|
||||||
|
//nolint:gocritic
|
||||||
|
switch res := data.Resource.(type) {
|
||||||
|
case *runtimeres.MetaKey:
|
||||||
|
if res.Metadata().ID() == runtimeres.MetaKeyTagToID(meta.DownloadURLCode) {
|
||||||
|
if data.Deleted {
|
||||||
|
nodeData.existingCode = unset
|
||||||
|
} else {
|
||||||
|
val := res.TypedSpec().Value
|
||||||
|
if val == "" {
|
||||||
|
val = "(empty)"
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeData.existingCode = fmt.Sprintf("[blue]%s[-]", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *ConfigURLGrid) redraw() {
|
||||||
|
data := widget.getOrCreateNodeData(widget.selectedNode)
|
||||||
|
|
||||||
|
widget.existingCode.SetText(data.existingCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *ConfigURLGrid) getOrCreateNodeData(node string) *configURLData {
|
||||||
|
nodeData, ok := widget.nodeMap[node]
|
||||||
|
if !ok {
|
||||||
|
nodeData = &configURLData{
|
||||||
|
existingCode: unset,
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.nodeMap[node] = nodeData
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *ConfigURLGrid) clearForm() {
|
||||||
|
widget.form.SetFocus(0)
|
||||||
|
widget.infoView.SetText("")
|
||||||
|
widget.newCodeField.SetText("")
|
||||||
|
}
|
||||||
28
internal/pkg/dashboard/context.go
Normal file
28
internal/pkg/dashboard/context.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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 dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
|
"github.com/siderolabs/talos/pkg/machinery/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nodeContext(ctx context.Context, selectedNode string) context.Context {
|
||||||
|
md, mdOk := metadata.FromOutgoingContext(ctx)
|
||||||
|
if mdOk {
|
||||||
|
md.Delete("nodes")
|
||||||
|
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedNode != "" {
|
||||||
|
ctx = client.WithNode(ctx, selectedNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@ -48,6 +48,9 @@ const (
|
|||||||
|
|
||||||
// ScreenNetworkConfig is the network configuration screen.
|
// ScreenNetworkConfig is the network configuration screen.
|
||||||
ScreenNetworkConfig Screen = "Network Config"
|
ScreenNetworkConfig Screen = "Network Config"
|
||||||
|
|
||||||
|
// ScreenConfigURL is the config URL screen.
|
||||||
|
ScreenConfigURL Screen = "Config URL"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIDataListener is a listener which is notified when API-sourced data is updated.
|
// APIDataListener is a listener which is notified when API-sourced data is updated.
|
||||||
@ -114,6 +117,7 @@ type Dashboard struct {
|
|||||||
summaryGrid *SummaryGrid
|
summaryGrid *SummaryGrid
|
||||||
monitorGrid *MonitorGrid
|
monitorGrid *MonitorGrid
|
||||||
networkConfigGrid *NetworkConfigGrid
|
networkConfigGrid *NetworkConfigGrid
|
||||||
|
configURLGrid *ConfigURLGrid
|
||||||
|
|
||||||
selectedScreenConfig *screenConfig
|
selectedScreenConfig *screenConfig
|
||||||
screenConfigs []screenConfig
|
screenConfigs []screenConfig
|
||||||
@ -156,6 +160,7 @@ func buildDashboard(ctx context.Context, cli *client.Client, opts ...Option) (*D
|
|||||||
dashboard.summaryGrid = NewSummaryGrid(dashboard.app)
|
dashboard.summaryGrid = NewSummaryGrid(dashboard.app)
|
||||||
dashboard.monitorGrid = NewMonitorGrid(dashboard.app)
|
dashboard.monitorGrid = NewMonitorGrid(dashboard.app)
|
||||||
dashboard.networkConfigGrid = NewNetworkConfigGrid(ctx, dashboard)
|
dashboard.networkConfigGrid = NewNetworkConfigGrid(ctx, dashboard)
|
||||||
|
dashboard.configURLGrid = NewConfigURLGrid(ctx, dashboard)
|
||||||
|
|
||||||
err := dashboard.initScreenConfigs(defOptions.screens)
|
err := dashboard.initScreenConfigs(defOptions.screens)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -213,6 +218,7 @@ func buildDashboard(ctx context.Context, cli *client.Client, opts ...Option) (*D
|
|||||||
header,
|
header,
|
||||||
dashboard.summaryGrid,
|
dashboard.summaryGrid,
|
||||||
dashboard.networkConfigGrid,
|
dashboard.networkConfigGrid,
|
||||||
|
dashboard.configURLGrid,
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard.logDataListeners = []LogDataListener{
|
dashboard.logDataListeners = []LogDataListener{
|
||||||
@ -223,6 +229,7 @@ func buildDashboard(ctx context.Context, cli *client.Client, opts ...Option) (*D
|
|||||||
header,
|
header,
|
||||||
dashboard.summaryGrid,
|
dashboard.summaryGrid,
|
||||||
dashboard.networkConfigGrid,
|
dashboard.networkConfigGrid,
|
||||||
|
dashboard.configURLGrid,
|
||||||
dashboard.footer,
|
dashboard.footer,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +260,8 @@ func (d *Dashboard) initScreenConfigs(screens []Screen) error {
|
|||||||
return d.monitorGrid
|
return d.monitorGrid
|
||||||
case ScreenNetworkConfig:
|
case ScreenNetworkConfig:
|
||||||
return d.networkConfigGrid
|
return d.networkConfigGrid
|
||||||
|
case ScreenConfigURL:
|
||||||
|
return d.configURLGrid
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -274,7 +283,7 @@ func (d *Dashboard) initScreenConfigs(screens []Screen) error {
|
|||||||
allowNodeNavigation: true,
|
allowNodeNavigation: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if screen == ScreenNetworkConfig {
|
if screen == ScreenNetworkConfig || screen == ScreenConfigURL {
|
||||||
config.allowNodeNavigation = false
|
config.allowNodeNavigation = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,26 +20,32 @@ import (
|
|||||||
const (
|
const (
|
||||||
interfaceNone = "(none)"
|
interfaceNone = "(none)"
|
||||||
|
|
||||||
modeDHCP = "DHCP"
|
// ModeDHCP is the DHCP mode for the link.
|
||||||
modeStatic = "Static"
|
ModeDHCP = "DHCP"
|
||||||
|
|
||||||
|
// ModeStatic is the static IP mode for the link.
|
||||||
|
ModeStatic = "Static"
|
||||||
)
|
)
|
||||||
|
|
||||||
type networkConfigFormData struct {
|
// NetworkConfigFormData is the form data for the network config.
|
||||||
base runtime.PlatformNetworkConfig
|
type NetworkConfigFormData struct {
|
||||||
hostname string
|
Base runtime.PlatformNetworkConfig
|
||||||
dnsServers string
|
Hostname string
|
||||||
timeServers string
|
DNSServers string
|
||||||
iface string
|
TimeServers string
|
||||||
mode string
|
Iface string
|
||||||
addresses string
|
Mode string
|
||||||
gateway string
|
Addresses string
|
||||||
|
Gateway string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToPlatformNetworkConfig converts the form data to a PlatformNetworkConfig.
|
||||||
|
//
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.PlatformNetworkConfig, error) {
|
func (formData *NetworkConfigFormData) ToPlatformNetworkConfig() (*runtime.PlatformNetworkConfig, error) {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
config := &formData.base
|
config := &formData.Base
|
||||||
|
|
||||||
// zero-out the fields managed by the form
|
// zero-out the fields managed by the form
|
||||||
config.Hostnames = nil
|
config.Hostnames = nil
|
||||||
@ -50,16 +56,16 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
config.Addresses = nil
|
config.Addresses = nil
|
||||||
config.Routes = nil
|
config.Routes = nil
|
||||||
|
|
||||||
if formData.hostname != "" {
|
if formData.Hostname != "" {
|
||||||
config.Hostnames = []network.HostnameSpecSpec{
|
config.Hostnames = []network.HostnameSpecSpec{
|
||||||
{
|
{
|
||||||
Hostname: formData.hostname,
|
Hostname: formData.Hostname,
|
||||||
ConfigLayer: network.ConfigPlatform,
|
ConfigLayer: network.ConfigPlatform,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsServers, err := formData.parseAddresses(formData.dnsServers)
|
dnsServers, err := formData.parseAddresses(formData.DNSServers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to parse DNS servers: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to parse DNS servers: %w", err))
|
||||||
}
|
}
|
||||||
@ -73,7 +79,7 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeServers := formData.splitInputList(formData.timeServers)
|
timeServers := formData.splitInputList(formData.TimeServers)
|
||||||
|
|
||||||
if len(timeServers) > 0 {
|
if len(timeServers) > 0 {
|
||||||
config.TimeServers = []network.TimeServerSpecSpec{
|
config.TimeServers = []network.TimeServerSpecSpec{
|
||||||
@ -84,11 +90,11 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ifaceSelected := formData.iface != "" && formData.iface != interfaceNone
|
ifaceSelected := formData.Iface != "" && formData.Iface != interfaceNone
|
||||||
if ifaceSelected {
|
if ifaceSelected {
|
||||||
config.Links = []network.LinkSpecSpec{
|
config.Links = []network.LinkSpecSpec{
|
||||||
{
|
{
|
||||||
Name: formData.iface,
|
Name: formData.Iface,
|
||||||
Logical: false,
|
Logical: false,
|
||||||
Up: true,
|
Up: true,
|
||||||
Type: nethelpers.LinkEther,
|
Type: nethelpers.LinkEther,
|
||||||
@ -96,12 +102,12 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch formData.mode {
|
switch formData.Mode {
|
||||||
case modeDHCP:
|
case ModeDHCP:
|
||||||
config.Operators = []network.OperatorSpecSpec{
|
config.Operators = []network.OperatorSpecSpec{
|
||||||
{
|
{
|
||||||
Operator: network.OperatorDHCP4,
|
Operator: network.OperatorDHCP4,
|
||||||
LinkName: formData.iface,
|
LinkName: formData.Iface,
|
||||||
RequireUp: true,
|
RequireUp: true,
|
||||||
DHCP4: network.DHCP4OperatorSpec{
|
DHCP4: network.DHCP4OperatorSpec{
|
||||||
RouteMetric: 1024,
|
RouteMetric: 1024,
|
||||||
@ -109,8 +115,8 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
ConfigLayer: network.ConfigPlatform,
|
ConfigLayer: network.ConfigPlatform,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case modeStatic:
|
case ModeStatic:
|
||||||
config.Addresses, err = formData.buildAddresses(formData.iface)
|
config.Addresses, err = formData.buildAddresses(formData.Iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = multierror.Append(errs, err)
|
errs = multierror.Append(errs, err)
|
||||||
}
|
}
|
||||||
@ -119,7 +125,7 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
errs = multierror.Append(errs, fmt.Errorf("no addresses specified"))
|
errs = multierror.Append(errs, fmt.Errorf("no addresses specified"))
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Routes, err = formData.buildRoutes(formData.iface)
|
config.Routes, err = formData.buildRoutes(formData.Iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = multierror.Append(errs, err)
|
errs = multierror.Append(errs, err)
|
||||||
}
|
}
|
||||||
@ -133,7 +139,7 @@ func (formData *networkConfigFormData) toPlatformNetworkConfig() (*runtime.Platf
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formData *networkConfigFormData) parseAddresses(text string) ([]netip.Addr, error) {
|
func (formData *NetworkConfigFormData) parseAddresses(text string) ([]netip.Addr, error) {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
split := formData.splitInputList(text)
|
split := formData.splitInputList(text)
|
||||||
@ -157,10 +163,10 @@ func (formData *networkConfigFormData) parseAddresses(text string) ([]netip.Addr
|
|||||||
return addresses, nil
|
return addresses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formData *networkConfigFormData) buildAddresses(linkName string) ([]network.AddressSpecSpec, error) {
|
func (formData *NetworkConfigFormData) buildAddresses(linkName string) ([]network.AddressSpecSpec, error) {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
addressesSplit := formData.splitInputList(formData.addresses)
|
addressesSplit := formData.splitInputList(formData.Addresses)
|
||||||
addresses := make([]network.AddressSpecSpec, 0, len(addressesSplit))
|
addresses := make([]network.AddressSpecSpec, 0, len(addressesSplit))
|
||||||
|
|
||||||
for _, address := range addressesSplit {
|
for _, address := range addressesSplit {
|
||||||
@ -193,8 +199,8 @@ func (formData *networkConfigFormData) buildAddresses(linkName string) ([]networ
|
|||||||
return addresses, nil
|
return addresses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formData *networkConfigFormData) buildRoutes(linkName string) ([]network.RouteSpecSpec, error) {
|
func (formData *NetworkConfigFormData) buildRoutes(linkName string) ([]network.RouteSpecSpec, error) {
|
||||||
gateway := strings.TrimSpace(formData.gateway)
|
gateway := strings.TrimSpace(formData.Gateway)
|
||||||
|
|
||||||
gatewayAddr, err := netip.ParseAddr(gateway)
|
gatewayAddr, err := netip.ParseAddr(gateway)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -220,7 +226,7 @@ func (formData *networkConfigFormData) buildRoutes(linkName string) ([]network.R
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formData *networkConfigFormData) splitInputList(text string) []string {
|
func (formData *NetworkConfigFormData) splitInputList(text string) []string {
|
||||||
parts := strings.FieldsFunc(text, func(r rune) bool {
|
parts := strings.FieldsFunc(text, func(r rune) bool {
|
||||||
return r == ',' || unicode.IsSpace(r)
|
return r == ',' || unicode.IsSpace(r)
|
||||||
})
|
})
|
||||||
|
|||||||
245
internal/pkg/dashboard/formdata_test.go
Normal file
245
internal/pkg/dashboard/formdata_test.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// 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 dashboard_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
||||||
|
"github.com/siderolabs/talos/internal/pkg/dashboard"
|
||||||
|
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
|
||||||
|
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
||||||
|
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmptyFormData(t *testing.T) {
|
||||||
|
formData := dashboard.NetworkConfigFormData{}
|
||||||
|
|
||||||
|
config, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, runtime.PlatformNetworkConfig{}, *config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDataZeroOut(t *testing.T) {
|
||||||
|
base := runtime.PlatformNetworkConfig{
|
||||||
|
Addresses: []network.AddressSpecSpec{
|
||||||
|
{
|
||||||
|
LinkName: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Links: []network.LinkSpecSpec{
|
||||||
|
{
|
||||||
|
Name: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: []network.RouteSpecSpec{
|
||||||
|
{
|
||||||
|
OutLinkName: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hostnames: []network.HostnameSpecSpec{
|
||||||
|
{
|
||||||
|
Hostname: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resolvers: []network.ResolverSpecSpec{
|
||||||
|
{
|
||||||
|
DNSServers: []netip.Addr{
|
||||||
|
netip.MustParseAddr("1.2.3.4"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TimeServers: []network.TimeServerSpecSpec{
|
||||||
|
{
|
||||||
|
NTPServers: []string{"foobar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Operators: []network.OperatorSpecSpec{
|
||||||
|
{
|
||||||
|
LinkName: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExternalIPs: []netip.Addr{
|
||||||
|
netip.MustParseAddr("2.3.4.5"),
|
||||||
|
},
|
||||||
|
Metadata: &runtimeres.PlatformMetadataSpec{
|
||||||
|
Platform: "foobar",
|
||||||
|
Spot: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
formData := dashboard.NetworkConfigFormData{
|
||||||
|
Base: base,
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// assert that the fields managed by the form are zeroed out
|
||||||
|
assert.Empty(t, config.Addresses)
|
||||||
|
assert.Empty(t, config.Links)
|
||||||
|
assert.Empty(t, config.Routes)
|
||||||
|
assert.Empty(t, config.Hostnames)
|
||||||
|
assert.Empty(t, config.Resolvers)
|
||||||
|
assert.Empty(t, config.TimeServers)
|
||||||
|
assert.Empty(t, config.Operators)
|
||||||
|
|
||||||
|
// assert that the fields not managed by the form are untouched
|
||||||
|
assert.Equal(t, base.ExternalIPs, config.ExternalIPs)
|
||||||
|
assert.Equal(t, base.Metadata, config.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilledFormNoIface(t *testing.T) {
|
||||||
|
formData := dashboard.NetworkConfigFormData{
|
||||||
|
Base: runtime.PlatformNetworkConfig{
|
||||||
|
Metadata: &runtimeres.PlatformMetadataSpec{
|
||||||
|
Platform: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hostname: "foobar",
|
||||||
|
DNSServers: "1.2.3.4 5.6.7.8",
|
||||||
|
TimeServers: "a.example.com , b.example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
runtime.PlatformNetworkConfig{
|
||||||
|
Hostnames: []network.HostnameSpecSpec{{
|
||||||
|
Hostname: "foobar",
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
}},
|
||||||
|
Resolvers: []network.ResolverSpecSpec{{
|
||||||
|
DNSServers: []netip.Addr{
|
||||||
|
netip.MustParseAddr("1.2.3.4"),
|
||||||
|
netip.MustParseAddr("5.6.7.8"),
|
||||||
|
},
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
}},
|
||||||
|
TimeServers: []network.TimeServerSpecSpec{
|
||||||
|
{
|
||||||
|
NTPServers: []string{"a.example.com", "b.example.com"},
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: &runtimeres.PlatformMetadataSpec{
|
||||||
|
Platform: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*config,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilledFormModeDHCP(t *testing.T) {
|
||||||
|
formData := dashboard.NetworkConfigFormData{
|
||||||
|
Iface: "eth0",
|
||||||
|
Mode: dashboard.ModeDHCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, runtime.PlatformNetworkConfig{
|
||||||
|
Links: []network.LinkSpecSpec{
|
||||||
|
{
|
||||||
|
Name: formData.Iface,
|
||||||
|
Logical: false,
|
||||||
|
Up: true,
|
||||||
|
Type: nethelpers.LinkEther,
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Operators: []network.OperatorSpecSpec{
|
||||||
|
{
|
||||||
|
Operator: network.OperatorDHCP4,
|
||||||
|
LinkName: formData.Iface,
|
||||||
|
RequireUp: true,
|
||||||
|
DHCP4: network.DHCP4OperatorSpec{
|
||||||
|
RouteMetric: 1024,
|
||||||
|
},
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, *config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilledFormModeStaticNoAddresses(t *testing.T) {
|
||||||
|
formData := dashboard.NetworkConfigFormData{
|
||||||
|
Iface: "eth0",
|
||||||
|
Mode: dashboard.ModeStatic,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.ErrorContains(t, err, "no addresses specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilledFormModeStaticNoGateway(t *testing.T) {
|
||||||
|
formData := dashboard.NetworkConfigFormData{
|
||||||
|
Iface: "eth0",
|
||||||
|
Mode: dashboard.ModeStatic,
|
||||||
|
Addresses: "1.2.3.4/24",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.ErrorContains(t, err, "unable to parse")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilledFormModeStatic(t *testing.T) {
|
||||||
|
formData := dashboard.NetworkConfigFormData{
|
||||||
|
Iface: "eth42",
|
||||||
|
Mode: dashboard.ModeStatic,
|
||||||
|
Addresses: "1.2.3.4/24 2.3.4.5/32",
|
||||||
|
Gateway: "3.4.5.6",
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := formData.ToPlatformNetworkConfig()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, runtime.PlatformNetworkConfig{
|
||||||
|
Links: []network.LinkSpecSpec{
|
||||||
|
{
|
||||||
|
Name: formData.Iface,
|
||||||
|
Logical: false,
|
||||||
|
Up: true,
|
||||||
|
Type: nethelpers.LinkEther,
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Addresses: []network.AddressSpecSpec{
|
||||||
|
{
|
||||||
|
Address: netip.MustParsePrefix("1.2.3.4/24"),
|
||||||
|
LinkName: formData.Iface,
|
||||||
|
Family: nethelpers.FamilyInet4,
|
||||||
|
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Address: netip.MustParsePrefix("2.3.4.5/32"),
|
||||||
|
LinkName: formData.Iface,
|
||||||
|
Family: nethelpers.FamilyInet4,
|
||||||
|
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: []network.RouteSpecSpec{
|
||||||
|
{
|
||||||
|
Family: nethelpers.FamilyInet4,
|
||||||
|
Gateway: netip.MustParseAddr("3.4.5.6"),
|
||||||
|
OutLinkName: "eth42",
|
||||||
|
Table: nethelpers.TableMain,
|
||||||
|
Scope: nethelpers.ScopeGlobal,
|
||||||
|
Type: nethelpers.TypeUnicast,
|
||||||
|
Protocol: nethelpers.ProtocolStatic,
|
||||||
|
ConfigLayer: network.ConfigPlatform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, *config)
|
||||||
|
}
|
||||||
@ -14,20 +14,16 @@ import (
|
|||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
"github.com/siderolabs/gen/maps"
|
"github.com/siderolabs/gen/maps"
|
||||||
"github.com/siderolabs/go-pointer"
|
"github.com/siderolabs/go-pointer"
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
||||||
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
|
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
|
||||||
"github.com/siderolabs/talos/internal/pkg/meta"
|
"github.com/siderolabs/talos/internal/pkg/meta"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/client"
|
|
||||||
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
||||||
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
|
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pageDeleteModal = "deleteModal"
|
|
||||||
|
|
||||||
formItemHostname = "Hostname"
|
formItemHostname = "Hostname"
|
||||||
formItemDNSServers = "DNS Servers"
|
formItemDNSServers = "DNS Servers"
|
||||||
formItemTimeServers = "Time Servers"
|
formItemTimeServers = "Time Servers"
|
||||||
@ -35,8 +31,6 @@ const (
|
|||||||
formItemMode = "Mode"
|
formItemMode = "Mode"
|
||||||
formItemAddresses = "Addresses"
|
formItemAddresses = "Addresses"
|
||||||
formItemGateway = "Gateway"
|
formItemGateway = "Gateway"
|
||||||
|
|
||||||
buttonConfirmDelete = "Delete"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type networkConfigData struct {
|
type networkConfigData struct {
|
||||||
@ -125,7 +119,7 @@ func NewNetworkConfigGrid(ctx context.Context, dashboard *Dashboard) *NetworkCon
|
|||||||
|
|
||||||
widget.modeDropdown = tview.NewDropDown().SetLabel(formItemMode)
|
widget.modeDropdown = tview.NewDropDown().SetLabel(formItemMode)
|
||||||
widget.modeDropdown.SetBlurFunc(widget.formEdited)
|
widget.modeDropdown.SetBlurFunc(widget.formEdited)
|
||||||
widget.modeDropdown.SetOptions([]string{modeDHCP, modeStatic}, func(_ string, _ int) {
|
widget.modeDropdown.SetOptions([]string{ModeDHCP, ModeStatic}, func(_ string, _ int) {
|
||||||
widget.formEdited()
|
widget.formEdited()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -150,27 +144,6 @@ func NewNetworkConfigGrid(ctx context.Context, dashboard *Dashboard) *NetworkCon
|
|||||||
saveButton := widget.configForm.GetButton(0)
|
saveButton := widget.configForm.GetButton(0)
|
||||||
saveButton.SetBlurFunc(widget.formEdited)
|
saveButton.SetBlurFunc(widget.formEdited)
|
||||||
|
|
||||||
widget.configForm.AddButton("Delete", func() {
|
|
||||||
widget.dashboard.pages.SwitchToPage(pageDeleteModal)
|
|
||||||
})
|
|
||||||
|
|
||||||
deleteButton := widget.configForm.GetButton(1)
|
|
||||||
deleteButton.SetBlurFunc(widget.formEdited)
|
|
||||||
|
|
||||||
deleteModal := tview.NewModal().
|
|
||||||
SetText("Are you sure you want to delete the configuration?").
|
|
||||||
AddButtons([]string{buttonConfirmDelete, "Cancel"}).
|
|
||||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
|
||||||
if buttonLabel == buttonConfirmDelete {
|
|
||||||
widget.delete(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.dashboard.pages.SwitchToPage(pageMain)
|
|
||||||
widget.dashboard.app.SetFocus(deleteButton)
|
|
||||||
})
|
|
||||||
|
|
||||||
widget.dashboard.pages.AddPage(pageDeleteModal, deleteModal, true, false)
|
|
||||||
|
|
||||||
inputCapture := func(event *tcell.EventKey) *tcell.EventKey {
|
inputCapture := func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if widget.handleFocusSwitch(event) {
|
if widget.handleFocusSwitch(event) {
|
||||||
return nil
|
return nil
|
||||||
@ -238,7 +211,7 @@ func (widget *NetworkConfigGrid) formEdited() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch currentMode {
|
switch currentMode {
|
||||||
case modeDHCP:
|
case ModeDHCP:
|
||||||
resetInputField(widget.addressesField)
|
resetInputField(widget.addressesField)
|
||||||
resetInputField(widget.gatewayField)
|
resetInputField(widget.gatewayField)
|
||||||
|
|
||||||
@ -249,7 +222,7 @@ func (widget *NetworkConfigGrid) formEdited() {
|
|||||||
if itemIndex := widget.configForm.GetFormItemIndex(formItemGateway); itemIndex != -1 {
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemGateway); itemIndex != -1 {
|
||||||
widget.configForm.RemoveFormItem(itemIndex)
|
widget.configForm.RemoveFormItem(itemIndex)
|
||||||
}
|
}
|
||||||
case modeStatic:
|
case ModeStatic:
|
||||||
if itemIndex := widget.configForm.GetFormItemIndex(formItemAddresses); itemIndex == -1 {
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemAddresses); itemIndex == -1 {
|
||||||
widget.configForm.AddFormItem(widget.addressesField)
|
widget.configForm.AddFormItem(widget.addressesField)
|
||||||
}
|
}
|
||||||
@ -278,18 +251,18 @@ func (widget *NetworkConfigGrid) formEdited() {
|
|||||||
|
|
||||||
data := widget.getOrCreateNodeData(widget.selectedNode)
|
data := widget.getOrCreateNodeData(widget.selectedNode)
|
||||||
|
|
||||||
formData := networkConfigFormData{
|
formData := NetworkConfigFormData{
|
||||||
base: pointer.SafeDeref(data.existingConfig),
|
Base: pointer.SafeDeref(data.existingConfig),
|
||||||
hostname: widget.hostnameField.GetText(),
|
Hostname: widget.hostnameField.GetText(),
|
||||||
dnsServers: widget.dnsServersField.GetText(),
|
DNSServers: widget.dnsServersField.GetText(),
|
||||||
timeServers: widget.timeServersField.GetText(),
|
TimeServers: widget.timeServersField.GetText(),
|
||||||
iface: currentInterface,
|
Iface: currentInterface,
|
||||||
mode: currentMode,
|
Mode: currentMode,
|
||||||
addresses: widget.addressesField.GetText(),
|
Addresses: widget.addressesField.GetText(),
|
||||||
gateway: widget.gatewayField.GetText(),
|
Gateway: widget.gatewayField.GetText(),
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := formData.toPlatformNetworkConfig()
|
config, err := formData.ToPlatformNetworkConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data.newConfig = nil
|
data.newConfig = nil
|
||||||
data.newConfigError = err
|
data.newConfigError = err
|
||||||
@ -378,6 +351,7 @@ func (widget *NetworkConfigGrid) updateNodeData(data resourcedata.Data) {
|
|||||||
widget.formEdited()
|
widget.formEdited()
|
||||||
})
|
})
|
||||||
case *runtimeres.MetaKey:
|
case *runtimeres.MetaKey:
|
||||||
|
if res.Metadata().ID() == runtimeres.MetaKeyTagToID(meta.MetalNetworkPlatformConfig) {
|
||||||
if data.Deleted {
|
if data.Deleted {
|
||||||
nodeData.existingConfig = nil
|
nodeData.existingConfig = nil
|
||||||
} else {
|
} else {
|
||||||
@ -394,6 +368,7 @@ func (widget *NetworkConfigGrid) updateNodeData(data resourcedata.Data) {
|
|||||||
widget.formEdited()
|
widget.formEdited()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *NetworkConfigGrid) getOrCreateNodeData(node string) *networkConfigData {
|
func (widget *NetworkConfigGrid) getOrCreateNodeData(node string) *networkConfigData {
|
||||||
@ -438,7 +413,7 @@ func (widget *NetworkConfigGrid) save(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = widget.nodeContext(ctx)
|
ctx = nodeContext(ctx, widget.selectedNode)
|
||||||
|
|
||||||
if err = widget.dashboard.cli.MetaWrite(ctx, meta.MetalNetworkPlatformConfig, configBytes); err != nil {
|
if err = widget.dashboard.cli.MetaWrite(ctx, meta.MetalNetworkPlatformConfig, configBytes); err != nil {
|
||||||
widget.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
widget.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
||||||
@ -451,33 +426,6 @@ func (widget *NetworkConfigGrid) save(ctx context.Context) {
|
|||||||
widget.dashboard.selectScreen(ScreenSummary)
|
widget.dashboard.selectScreen(ScreenSummary)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *NetworkConfigGrid) delete(ctx context.Context) {
|
|
||||||
ctx = widget.nodeContext(ctx)
|
|
||||||
|
|
||||||
if err := widget.dashboard.cli.MetaDelete(ctx, meta.MetalNetworkPlatformConfig); err != nil {
|
|
||||||
widget.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.infoView.SetText("[green]Network config deleted successfully[-]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (widget *NetworkConfigGrid) nodeContext(ctx context.Context) context.Context {
|
|
||||||
md, mdOk := metadata.FromOutgoingContext(ctx)
|
|
||||||
if mdOk {
|
|
||||||
md.Delete("nodes")
|
|
||||||
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
|
||||||
}
|
|
||||||
|
|
||||||
if widget.selectedNode != "" {
|
|
||||||
ctx = client.WithNode(ctx, widget.selectedNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (widget *NetworkConfigGrid) handleFocusSwitch(event *tcell.EventKey) bool {
|
func (widget *NetworkConfigGrid) handleFocusSwitch(event *tcell.EventKey) bool {
|
||||||
switch event.Key() { //nolint:exhaustive
|
switch event.Key() { //nolint:exhaustive
|
||||||
case tcell.KeyCtrlQ:
|
case tcell.KeyCtrlQ:
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
"github.com/siderolabs/talos/internal/pkg/meta"
|
|
||||||
"github.com/siderolabs/talos/pkg/machinery/client"
|
"github.com/siderolabs/talos/pkg/machinery/client"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/resources/cluster"
|
"github.com/siderolabs/talos/pkg/machinery/resources/cluster"
|
||||||
"github.com/siderolabs/talos/pkg/machinery/resources/config"
|
"github.com/siderolabs/talos/pkg/machinery/resources/config"
|
||||||
@ -139,10 +138,6 @@ func (source *Source) runResourceWatch(ctx context.Context, node string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := source.COSI.Watch(ctx, runtime.NewMetaKey(runtime.NamespaceName, runtime.MetaKeyTagToID(meta.MetalNetworkPlatformConfig)).Metadata(), eventCh); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := source.COSI.Watch(ctx, network.NewStatus(network.NamespaceName, network.StatusID).Metadata(), eventCh); err != nil {
|
if err := source.COSI.Watch(ctx, network.NewStatus(network.NamespaceName, network.StatusID).Metadata(), eventCh); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -151,6 +146,10 @@ func (source *Source) runResourceWatch(ctx context.Context, node string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := source.COSI.WatchKind(ctx, runtime.NewMetaKey(runtime.NamespaceName, "").Metadata(), eventCh, state.WithBootstrapContents(true)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := source.COSI.WatchKind(ctx, k8s.NewStaticPodStatus(k8s.NamespaceName, "").Metadata(), eventCh, state.WithBootstrapContents(true)); err != nil {
|
if err := source.COSI.WatchKind(ctx, k8s.NewStaticPodStatus(k8s.NamespaceName, "").Metadata(), eventCh, state.WithBootstrapContents(true)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@
|
|||||||
package dashboard
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
|
|
||||||
"github.com/siderolabs/talos/internal/pkg/dashboard/apidata"
|
"github.com/siderolabs/talos/internal/pkg/dashboard/apidata"
|
||||||
@ -24,7 +22,6 @@ type SummaryGrid struct {
|
|||||||
resourceListeners []ResourceDataListener
|
resourceListeners []ResourceDataListener
|
||||||
nodeSelectListeners []NodeSelectListener
|
nodeSelectListeners []NodeSelectListener
|
||||||
|
|
||||||
lock sync.Mutex
|
|
||||||
active bool
|
active bool
|
||||||
node string
|
node string
|
||||||
logViewers map[string]*components.LogViewer
|
logViewers map[string]*components.LogViewer
|
||||||
@ -70,9 +67,6 @@ func NewSummaryGrid(app *tview.Application) *SummaryGrid {
|
|||||||
|
|
||||||
// OnNodeSelect implements the NodeSelectListener interface.
|
// OnNodeSelect implements the NodeSelectListener interface.
|
||||||
func (widget *SummaryGrid) OnNodeSelect(node string) {
|
func (widget *SummaryGrid) OnNodeSelect(node string) {
|
||||||
widget.lock.Lock()
|
|
||||||
defer widget.lock.Unlock()
|
|
||||||
|
|
||||||
widget.node = node
|
widget.node = node
|
||||||
|
|
||||||
widget.updateLogViewer()
|
widget.updateLogViewer()
|
||||||
@ -98,9 +92,6 @@ func (widget *SummaryGrid) OnResourceDataChange(nodeResource resourcedata.Data)
|
|||||||
|
|
||||||
// OnLogDataChange implements the LogDataListener interface.
|
// OnLogDataChange implements the LogDataListener interface.
|
||||||
func (widget *SummaryGrid) OnLogDataChange(node string, logLine string) {
|
func (widget *SummaryGrid) OnLogDataChange(node string, logLine string) {
|
||||||
widget.lock.Lock()
|
|
||||||
defer widget.lock.Unlock()
|
|
||||||
|
|
||||||
widget.logViewer(node).WriteLog(logLine)
|
widget.logViewer(node).WriteLog(logLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,9 +128,6 @@ func (widget *SummaryGrid) logViewer(node string) *components.LogViewer {
|
|||||||
|
|
||||||
// OnScreenSelect implements the screenSelectListener interface.
|
// OnScreenSelect implements the screenSelectListener interface.
|
||||||
func (widget *SummaryGrid) onScreenSelect(active bool) {
|
func (widget *SummaryGrid) onScreenSelect(active bool) {
|
||||||
widget.lock.Lock()
|
|
||||||
defer widget.lock.Unlock()
|
|
||||||
|
|
||||||
widget.active = active
|
widget.active = active
|
||||||
|
|
||||||
widget.updateLogViewer()
|
widget.updateLogViewer()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user