feat: add support for HTTP Probes

- Add HTTPProbeSpec to ProbeSpecSpec (URL + timeout)
- Implement probeHTTP() to send GET requests, treat 2xx/3xx as success
- Support machine proxy config via httpdefaults.PatchTransport
- Add HTTPProbeConfig v1alpha1 document and controller integration
- Add unit and integration tests for HTTP probe lifecycle

Signed-off-by: Pranav Patil <pranavppatil767@gmail.com>
Co-authored-by: Pranav Patil <pranavppatil767@gmail.com>
Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
This commit is contained in:
Mateusz Urbanek 2026-04-28 10:17:46 +02:00
parent 9b776d5981
commit 876f836430
No known key found for this signature in database
GPG Key ID: F16F84591E26D77F
24 changed files with 1746 additions and 389 deletions

View File

@ -237,6 +237,14 @@ message EthernetStatusSpec {
repeated talos.resource.definitions.enums.NethelpersWOLMode wake_on_lan = 10;
}
// HTTPProbeSpec describes the HTTP Probe.
message HTTPProbeSpec {
// URL to probe: http:// or https:// URL.
common.URL url = 1;
// Timeout for the probe.
google.protobuf.Duration timeout = 2;
}
// HardwareAddrSpec describes spec for the link.
message HardwareAddrSpec {
// Name defines link name
@ -502,10 +510,12 @@ message ProbeSpecSpec {
google.protobuf.Duration interval = 1;
// FailureThreshold is the number of consecutive failures for the probe to be considered failed after having succeeded.
int64 failure_threshold = 2;
// One of the probe types should be specified, for now it's only TCP.
// TCP is the TCP probe spec. One of TCP or HTTP must be specified.
TCPProbeSpec tcp = 3;
// Configuration layer.
talos.resource.definitions.enums.NetworkConfigLayer config_layer = 4;
// HTTP is the HTTP probe spec. One of TCP or HTTP must be specified.
HTTPProbeSpec http = 5;
}
// ProbeStatusSpec describes the Probe.

View File

@ -51,6 +51,13 @@ The default installer image has been updated to use the Image Factory.
title = "Host DNS Configuration"
description = """\
HostDNS configuration was moved from the v1alpha1 config `.machine.features.hostDNS` field to the new `hostDNS` in the `ResolverConfig` document.
"""
[notes.httpProbe]
title = "HTTP Probe Support"
description = """\
Talos now supports HTTP network probes, allowing for monitoring of HTTP endpoints.
HTTP responses with status 200-399 are considered successful, while connection and transport errors are treated as failures.
"""
[make_deps]

View File

@ -9,13 +9,16 @@ import (
"context"
"errors"
"net"
"net/http"
"sync"
"syscall"
"time"
"github.com/hashicorp/go-cleanhttp"
"github.com/siderolabs/gen/channel"
"go.uber.org/zap"
"github.com/siderolabs/talos/pkg/httpdefaults"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
)
@ -121,11 +124,11 @@ func (runner *Runner) run(ctx context.Context, notifyCh chan<- Notification, log
// probe runs a probe.
func (runner *Runner) probe(ctx context.Context) error {
var zeroTCP network.TCPProbeSpec
switch {
case runner.Spec.TCP != zeroTCP:
case runner.Spec.TCP != (network.TCPProbeSpec{}):
return runner.probeTCP(ctx)
case runner.Spec.HTTP != (network.HTTPProbeSpec{}):
return runner.probeHTTP(ctx)
default:
return errors.New("no probe type specified")
}
@ -152,3 +155,36 @@ func (runner *Runner) probeTCP(ctx context.Context) error {
return conn.Close()
}
// probeHTTP runs an HTTP probe.
//
// HTTP responses with status 200-399 are considered success.
// Status 400+ and connection/transport errors are treated as failures.
// The client honors the machine's proxy configuration via httpdefaults.PatchTransport.
func (runner *Runner) probeHTTP(ctx context.Context) error {
client := &http.Client{
Transport: httpdefaults.PatchTransport(cleanhttp.DefaultTransport()),
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
ctx, cancel := context.WithTimeout(ctx, runner.Spec.HTTP.Timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, runner.Spec.HTTP.URL.String(), nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
return errors.New("received non-success status code: " + resp.Status)
}
return resp.Body.Close()
}

View File

@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"testing/synctest"
"time"
@ -21,6 +22,18 @@ import (
"github.com/siderolabs/talos/pkg/machinery/resources/network"
)
type swapHandler struct {
v atomic.Value // stores http.Handler
}
func (h *swapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.v.Load().(http.Handler).ServeHTTP(w, r)
}
func (h *swapHandler) Swap(newHandler http.Handler) {
h.v.Store(newHandler)
}
func TestProbeHTTP(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@ -151,3 +164,75 @@ func TestProbeConsecutiveFailures(t *testing.T) {
assert.False(t, notify.Status.Success)
})
}
func TestProbeHTTPProbe(t *testing.T) {
// Server returns 200 OK.
handler := &swapHandler{}
handler.Swap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
probeURL, err := url.Parse(server.URL)
require.NoError(t, err)
p := probe.Runner{
ID: "http-test",
Spec: network.ProbeSpecSpec{
Interval: 10 * time.Millisecond,
HTTP: network.HTTPProbeSpec{
URL: probeURL,
Timeout: time.Second,
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
t.Cleanup(cancel)
notifyCh := make(chan probe.Notification)
p.Start(ctx, notifyCh, zaptest.NewLogger(t))
t.Cleanup(p.Stop)
// probe should succeed — 2xx/3xx responses count as success
for range 3 {
assert.Equal(t, probe.Notification{
ID: "http-test",
Status: network.ProbeStatusSpec{
Success: true,
},
}, <-notifyCh)
}
// 4xx/5xx responses count as failure
handler.Swap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
for range 3 {
notification := <-notifyCh
assert.Equal(t, "http-test", notification.ID)
assert.False(t, notification.Status.Success)
assert.NotEmpty(t, notification.Status.LastError)
}
// stop the server — now the probe should fail
server.Close()
for {
notification := <-notifyCh
if notification.Status.Success {
continue
}
assert.Equal(t, "http-test", notification.ID)
assert.False(t, notification.Status.Success)
assert.NotEmpty(t, notification.Status.LastError)
break
}
}

View File

@ -131,6 +131,11 @@ func (ctrl *ProbeConfigController) parseMachineConfiguration(cfg *config.Machine
Endpoint: probeConfig.Endpoint(),
Timeout: probeConfig.Timeout(),
}
case configconfig.NetworkHTTPProbeConfig:
spec.HTTP = network.HTTPProbeSpec{
URL: probeConfig.URL().URL,
Timeout: probeConfig.Timeout(),
}
default:
panic(fmt.Sprintf("unsupported probe config type: %T", probeConfig))
}

View File

@ -5,16 +5,19 @@
package network_test
import (
"net/url"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/siderolabs/gen/ensure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
netctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network"
"github.com/siderolabs/talos/pkg/machinery/config/container"
"github.com/siderolabs/talos/pkg/machinery/config/types/meta"
networkcfg "github.com/siderolabs/talos/pkg/machinery/config/types/network"
"github.com/siderolabs/talos/pkg/machinery/resources/config"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
@ -29,7 +32,7 @@ func (suite *ProbeConfigSuite) TestNoConfig() {
ctest.AssertNoResource[*network.ProbeSpec](suite, "tcp:proxy.example.com:3128", rtestutils.WithNamespace(network.NamespaceName))
}
func (suite *ProbeConfigSuite) TestSingleProbe() {
func (suite *ProbeConfigSuite) TestSingleProbe() { //nolint:dupl
probeConfig := networkcfg.NewTCPProbeConfigV1Alpha1("proxy-check")
probeConfig.ProbeInterval = time.Second
probeConfig.ProbeFailureThreshold = 3
@ -132,6 +135,58 @@ func (suite *ProbeConfigSuite) TestMultipleProbes() {
ctest.AssertNoResource[*network.ProbeSpec](suite, "configuration/tcp:8.8.8.8:53", rtestutils.WithNamespace(network.ConfigNamespaceName))
}
func (suite *ProbeConfigSuite) TestHTTPProbe() { //nolint:dupl
probeConfig := networkcfg.NewHTTPProbeConfigV1Alpha1("http-check")
probeConfig.ProbeInterval = time.Second
probeConfig.ProbeFailureThreshold = 3
probeConfig.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse("https://example.com"))}
probeConfig.HTTPTimeout = 10 * time.Second
ctr, err := container.New(probeConfig)
suite.Require().NoError(err)
cfg := config.NewMachineConfig(ctr)
suite.Create(cfg)
ctest.AssertResources(
suite,
[]string{
"configuration/http:https://example.com",
}, func(r *network.ProbeSpec, asrt *assert.Assertions) {
asrt.Equal(time.Second, r.TypedSpec().Interval)
asrt.Equal(3, r.TypedSpec().FailureThreshold)
asrt.Equal("https://example.com", r.TypedSpec().HTTP.URL.String())
asrt.Equal(10*time.Second, r.TypedSpec().HTTP.Timeout)
asrt.Equal(network.ConfigMachineConfiguration, r.TypedSpec().ConfigLayer)
},
rtestutils.WithNamespace(network.ConfigNamespaceName),
)
// Update the probe config
ctest.UpdateWithConflicts(suite, cfg, func(r *config.MachineConfig) error {
docs := r.Container().Documents()
probeDoc := docs[0].(*networkcfg.HTTPProbeConfigV1Alpha1)
probeDoc.ProbeFailureThreshold = 5
return nil
})
ctest.AssertResources(
suite,
[]string{
"configuration/http:https://example.com",
}, func(r *network.ProbeSpec, asrt *assert.Assertions) {
asrt.Equal(5, r.TypedSpec().FailureThreshold)
},
rtestutils.WithNamespace(network.ConfigNamespaceName),
)
// Remove the config
suite.Destroy(cfg)
ctest.AssertNoResource[*network.ProbeSpec](suite, "configuration/http:https://example.com", rtestutils.WithNamespace(network.ConfigNamespaceName))
}
func TestProbeConfigSuite(t *testing.T) {
t.Parallel()

View File

@ -8,14 +8,17 @@ package api
import (
"context"
"net/url"
"time"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/siderolabs/gen/ensure"
"github.com/stretchr/testify/assert"
"github.com/siderolabs/talos/internal/integration/base"
"github.com/siderolabs/talos/pkg/machinery/client"
"github.com/siderolabs/talos/pkg/machinery/config/types/meta"
"github.com/siderolabs/talos/pkg/machinery/config/types/network"
networkres "github.com/siderolabs/talos/pkg/machinery/resources/network"
)
@ -51,7 +54,7 @@ func (suite *ProbeConfigSuite) TearDownTest() {
}
// TestProbeConfig tests that ProbeConfig documents create ProbeSpec resources.
func (suite *ProbeConfigSuite) TestProbeConfig() {
func (suite *ProbeConfigSuite) TestProbeConfig() { //nolint:dupl
node := suite.RandomDiscoveredNodeInternalIP()
nodeCtx := client.WithNode(suite.ctx, node)
@ -67,6 +70,7 @@ func (suite *ProbeConfigSuite) TestProbeConfig() {
suite.PatchMachineConfig(nodeCtx, probeConfig)
// Wait for ProbeSpec resource to be created
//nolint:dupl
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, "tcp:"+googleDNS,
func(spec *networkres.ProbeSpec, asrt *assert.Assertions) {
asrt.Equal(2*time.Second, spec.TypedSpec().Interval)
@ -195,6 +199,76 @@ func (suite *ProbeConfigSuite) TestProbeStatus() {
rtestutils.AssertNoResource[*networkres.ProbeSpec](nodeCtx, suite.T(), suite.Client.COSI, "tcp:"+googleDNS)
}
// TestHTTPProbeConfig tests that HTTPProbeConfig documents create ProbeSpec resources and run probes.
func (suite *ProbeConfigSuite) TestHTTPProbeConfig() {
if suite.Airgapped {
suite.T().Skip("skipping test in airgapped mode")
}
node := suite.RandomDiscoveredNodeInternalIP()
nodeCtx := client.WithNode(suite.ctx, node)
suite.T().Logf("testing HTTPProbeConfig on node %q", node)
const probeURL = "https://example.com"
probeConfig := network.NewHTTPProbeConfigV1Alpha1("http-test-probe")
probeConfig.ProbeInterval = 1 * time.Second
probeConfig.ProbeFailureThreshold = 3
probeConfig.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(probeURL))}
probeConfig.HTTPTimeout = 5 * time.Second
suite.PatchMachineConfig(nodeCtx, probeConfig)
// Wait for ProbeSpec resource to be created
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, "http:"+probeURL,
func(spec *networkres.ProbeSpec, asrt *assert.Assertions) {
asrt.Equal(1*time.Second, spec.TypedSpec().Interval)
asrt.Equal(3, spec.TypedSpec().FailureThreshold)
asrt.Equal(probeURL, spec.TypedSpec().HTTP.URL.String())
asrt.Equal(5*time.Second, spec.TypedSpec().HTTP.Timeout)
asrt.Equal(networkres.ConfigMachineConfiguration, spec.TypedSpec().ConfigLayer)
},
)
// Update the probe config
probeConfig.ProbeFailureThreshold = 5
suite.PatchMachineConfig(nodeCtx, probeConfig)
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, "http:"+probeURL,
func(spec *networkres.ProbeSpec, asrt *assert.Assertions) {
asrt.Equal(5, spec.TypedSpec().FailureThreshold)
},
)
// Give the probe controller time to run at least one probe
time.Sleep(3 * time.Second)
// Verify ProbeStatus is created and has been updated
probeStatuses, err := safe.StateListAll[*networkres.ProbeStatus](nodeCtx, suite.Client.COSI)
suite.Require().NoError(err)
var found bool
for status := range probeStatuses.All() {
if status.Metadata().ID() == "http:"+probeURL {
found = true
suite.T().Logf("HTTP ProbeStatus: success=%v, lastError=%s", status.TypedSpec().Success, status.TypedSpec().LastError)
suite.Assert().True(status.TypedSpec().Success || status.TypedSpec().LastError != "")
break
}
}
suite.Assert().True(found, "expected to find ProbeStatus for http:"+probeURL)
// Remove the ProbeConfig
suite.RemoveMachineConfigDocuments(nodeCtx, network.HTTPProbeKind)
rtestutils.AssertNoResource[*networkres.ProbeSpec](nodeCtx, suite.T(), suite.Client.COSI, "http:"+probeURL)
}
func init() {
allSuites = append(allSuites, &ProbeConfigSuite{})
}

File diff suppressed because it is too large Load Diff

View File

@ -1541,6 +1541,71 @@ func (m *EthernetStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
func (m *HTTPProbeSpec) MarshalVT() (dAtA []byte, err error) {
if m == nil {
return nil, nil
}
size := m.SizeVT()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *HTTPProbeSpec) MarshalToVT(dAtA []byte) (int, error) {
size := m.SizeVT()
return m.MarshalToSizedBufferVT(dAtA[:size])
}
func (m *HTTPProbeSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
if m == nil {
return 0, nil
}
i := len(dAtA)
_ = i
var l int
_ = l
if m.unknownFields != nil {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.Timeout != nil {
size, err := (*durationpb.Duration)(m.Timeout).MarshalToSizedBufferVT(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
i--
dAtA[i] = 0x12
}
if m.Url != nil {
if vtmsg, ok := interface{}(m.Url).(interface {
MarshalToSizedBufferVT([]byte) (int, error)
}); ok {
size, err := vtmsg.MarshalToSizedBufferVT(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
} else {
encoded, err := proto.Marshal(m.Url)
if err != nil {
return 0, err
}
i -= len(encoded)
copy(dAtA[i:], encoded)
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded)))
}
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func (m *HardwareAddrSpec) MarshalVT() (dAtA []byte, err error) {
if m == nil {
return nil, nil
@ -3573,6 +3638,16 @@ func (m *ProbeSpecSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.Http != nil {
size, err := m.Http.MarshalToSizedBufferVT(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
i--
dAtA[i] = 0x2a
}
if m.ConfigLayer != 0 {
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.ConfigLayer))
i--
@ -5707,6 +5782,30 @@ func (m *EthernetStatusSpec) SizeVT() (n int) {
return n
}
func (m *HTTPProbeSpec) SizeVT() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if m.Url != nil {
if size, ok := interface{}(m.Url).(interface {
SizeVT() int
}); ok {
l = size.SizeVT()
} else {
l = proto.Size(m.Url)
}
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
if m.Timeout != nil {
l = (*durationpb.Duration)(m.Timeout).SizeVT()
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
n += len(m.unknownFields)
return n
}
func (m *HardwareAddrSpec) SizeVT() (n int) {
if m == nil {
return 0
@ -6522,6 +6621,10 @@ func (m *ProbeSpecSpec) SizeVT() (n int) {
if m.ConfigLayer != 0 {
n += 1 + protohelpers.SizeOfVarint(uint64(m.ConfigLayer))
}
if m.Http != nil {
l = m.Http.SizeVT()
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
n += len(m.unknownFields)
return n
}
@ -10925,6 +11028,137 @@ func (m *EthernetStatusSpec) UnmarshalVT(dAtA []byte) error {
}
return nil
}
func (m *HTTPProbeSpec) UnmarshalVT(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: HTTPProbeSpec: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: HTTPProbeSpec: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Url == nil {
m.Url = &common.URL{}
}
if unmarshal, ok := interface{}(m.Url).(interface {
UnmarshalVT([]byte) error
}); ok {
if err := unmarshal.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
return err
}
} else {
if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.Url); err != nil {
return err
}
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Timeout", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Timeout == nil {
m.Timeout = &durationpb1.Duration{}
}
if err := (*durationpb.Duration)(m.Timeout).UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return protohelpers.ErrInvalidLength
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *HardwareAddrSpec) UnmarshalVT(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
@ -16033,6 +16267,42 @@ func (m *ProbeSpecSpec) UnmarshalVT(dAtA []byte) error {
break
}
}
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Http", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Http == nil {
m.Http = &HTTPProbeSpec{}
}
if err := m.Http.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])

View File

@ -11,6 +11,7 @@ import (
"github.com/siderolabs/gen/optional"
"github.com/siderolabs/talos/pkg/machinery/cel"
"github.com/siderolabs/talos/pkg/machinery/config/types/meta"
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
)
@ -388,3 +389,10 @@ type NetworkHostDNSConfig interface {
ForwardKubeDNSToHost() bool
ResolveMemberNames() bool
}
// NetworkHTTPProbeConfig defines an HTTP probe configuration.
type NetworkHTTPProbeConfig interface {
NetworkCommonProbeConfig
URL() meta.URL
Timeout() time.Duration
}

View File

@ -2226,6 +2226,75 @@
],
"description": "HCloudVIPConfig is a config document to configure virtual IP using Hetzner Cloud APIs for announcement.\\nVirtual IP configuration should be used only on controlplane nodes to provide virtual IP for Kubernetes API server.\\nAny other use cases are not supported and may lead to unexpected behavior.\\nVirtual IP will be announced from only one node at a time using Hetzner Cloud APIs.\\n"
},
"network.HTTPProbeConfigV1Alpha1": {
"properties": {
"apiVersion": {
"enum": [
"v1alpha1"
],
"title": "apiVersion",
"description": "apiVersion is the API version of the resource.\n",
"markdownDescription": "apiVersion is the API version of the resource.",
"x-intellij-html-description": "\u003cp\u003eapiVersion is the API version of the resource.\u003c/p\u003e\n"
},
"kind": {
"enum": [
"HTTPProbeConfig"
],
"title": "kind",
"description": "kind is the kind of the resource.\n",
"markdownDescription": "kind is the kind of the resource.",
"x-intellij-html-description": "\u003cp\u003ekind is the kind of the resource.\u003c/p\u003e\n"
},
"name": {
"type": "string",
"title": "name",
"description": "Name of the probe.\n",
"markdownDescription": "Name of the probe.",
"x-intellij-html-description": "\u003cp\u003eName of the probe.\u003c/p\u003e\n"
},
"interval": {
"type": "string",
"pattern": "^[-+]?(((\\d+(\\.\\d*)?|\\d*(\\.\\d+)+)([nuµm]?s|m|h))|0)+$",
"title": "interval",
"description": "Interval between probe attempts.\nDefaults to 1s.\n",
"markdownDescription": "Interval between probe attempts.\nDefaults to 1s.",
"x-intellij-html-description": "\u003cp\u003eInterval between probe attempts.\nDefaults to 1s.\u003c/p\u003e\n"
},
"failureThreshold": {
"type": "integer",
"title": "failureThreshold",
"description": "Number of consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 0 (immediately fail on first failure).\n",
"markdownDescription": "Number of consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 0 (immediately fail on first failure).",
"x-intellij-html-description": "\u003cp\u003eNumber of consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 0 (immediately fail on first failure).\u003c/p\u003e\n"
},
"url": {
"type": "string",
"pattern": "^(http|https)://",
"title": "url",
"description": "HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.\n",
"markdownDescription": "HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.",
"x-intellij-html-description": "\u003cp\u003eHTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.\u003c/p\u003e\n"
},
"timeout": {
"type": "string",
"pattern": "^[-+]?(((\\d+(\\.\\d*)?|\\d*(\\.\\d+)+)([nuµm]?s|m|h))|0)+$",
"title": "timeout",
"description": "Timeout for the probe.\nDefaults to 10s.\n",
"markdownDescription": "Timeout for the probe.\nDefaults to 10s.",
"x-intellij-html-description": "\u003cp\u003eTimeout for the probe.\nDefaults to 10s.\u003c/p\u003e\n"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"apiVersion",
"kind",
"name",
"url"
],
"description": "HTTPProbeConfig is a config document to configure network HTTP connectivity probes."
},
"network.HostDNSConfig": {
"properties": {
"enabled": {
@ -5791,6 +5860,9 @@
{
"$ref": "#/$defs/network.HostnameConfigV1Alpha1"
},
{
"$ref": "#/$defs/network.HTTPProbeConfigV1Alpha1"
},
{
"$ref": "#/$defs/network.KubeSpanConfigV1Alpha1"
},

View File

@ -2,12 +2,13 @@
// 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/.
// Code generated by "deep-copy -type BlackholeRouteConfigV1Alpha1 -type BondConfigV1Alpha1 -type BridgeConfigV1Alpha1 -type VRFConfigV1Alpha1 -type DefaultActionConfigV1Alpha1 -type DHCPv4ConfigV1Alpha1 -type DHCPv6ConfigV1Alpha1 -type DummyLinkConfigV1Alpha1 -type EthernetConfigV1Alpha1 -type HCloudVIPConfigV1Alpha1 -type HostnameConfigV1Alpha1 -type KubeSpanConfigV1Alpha1 -type KubespanEndpointsConfigV1Alpha1 -type Layer2VIPConfigV1Alpha1 -type LinkConfigV1Alpha1 -type LinkAliasConfigV1Alpha1 -type ResolverConfigV1Alpha1 -type RoutingRuleConfigV1Alpha1 -type RuleConfigV1Alpha1 -type StaticHostConfigV1Alpha1 -type TCPProbeConfigV1Alpha1 -type TimeSyncConfigV1Alpha1 -type VLANConfigV1Alpha1 -type WireguardConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
// Code generated by "deep-copy -type BlackholeRouteConfigV1Alpha1 -type BondConfigV1Alpha1 -type BridgeConfigV1Alpha1 -type VRFConfigV1Alpha1 -type DefaultActionConfigV1Alpha1 -type DHCPv4ConfigV1Alpha1 -type DHCPv6ConfigV1Alpha1 -type DummyLinkConfigV1Alpha1 -type EthernetConfigV1Alpha1 -type HCloudVIPConfigV1Alpha1 -type HTTPProbeConfigV1Alpha1 -type HostnameConfigV1Alpha1 -type KubeSpanConfigV1Alpha1 -type KubespanEndpointsConfigV1Alpha1 -type Layer2VIPConfigV1Alpha1 -type LinkConfigV1Alpha1 -type LinkAliasConfigV1Alpha1 -type ResolverConfigV1Alpha1 -type RoutingRuleConfigV1Alpha1 -type RuleConfigV1Alpha1 -type StaticHostConfigV1Alpha1 -type TCPProbeConfigV1Alpha1 -type TimeSyncConfigV1Alpha1 -type VLANConfigV1Alpha1 -type WireguardConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
package network
import (
"net/netip"
"net/url"
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
)
@ -402,6 +403,20 @@ func (o *HCloudVIPConfigV1Alpha1) DeepCopy() *HCloudVIPConfigV1Alpha1 {
return &cp
}
// DeepCopy generates a deep copy of *HTTPProbeConfigV1Alpha1.
func (o *HTTPProbeConfigV1Alpha1) DeepCopy() *HTTPProbeConfigV1Alpha1 {
var cp HTTPProbeConfigV1Alpha1 = *o
if o.HTTPEndpoint.URL != nil {
cp.HTTPEndpoint.URL = new(url.URL)
*cp.HTTPEndpoint.URL = *o.HTTPEndpoint.URL
if o.HTTPEndpoint.URL.User != nil {
cp.HTTPEndpoint.URL.User = new(url.Userinfo)
*cp.HTTPEndpoint.URL.User = *o.HTTPEndpoint.URL.User
}
}
return &cp
}
// DeepCopy generates a deep copy of *HostnameConfigV1Alpha1.
func (o *HostnameConfigV1Alpha1) DeepCopy() *HostnameConfigV1Alpha1 {
var cp HostnameConfigV1Alpha1 = *o

View File

@ -0,0 +1,165 @@
// 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 network
//docgen:jsonschema
import (
"errors"
"fmt"
"net/url"
"time"
"github.com/siderolabs/gen/ensure"
"github.com/siderolabs/talos/pkg/machinery/config/config"
"github.com/siderolabs/talos/pkg/machinery/config/internal/registry"
"github.com/siderolabs/talos/pkg/machinery/config/types/meta"
"github.com/siderolabs/talos/pkg/machinery/config/validation"
)
// HTTPProbeKind is a HTTPProbe config document kind.
const HTTPProbeKind = "HTTPProbeConfig"
func init() {
registry.Register(HTTPProbeKind, func(version string) config.Document {
switch version {
case "v1alpha1": //nolint:goconst
return &HTTPProbeConfigV1Alpha1{}
default:
return nil
}
})
}
// Check interfaces.
var (
_ config.NamedDocument = &HTTPProbeConfigV1Alpha1{}
_ config.Validator = &HTTPProbeConfigV1Alpha1{}
_ config.NetworkHTTPProbeConfig = &HTTPProbeConfigV1Alpha1{}
)
// HTTPProbeConfigV1Alpha1 is a config document to configure network HTTP connectivity probes.
//
// examples:
// - value: exampleHTTPProbeConfigV1Alpha1()
// alias: HTTPProbeConfig
// schemaRoot: true
// schemaMeta: v1alpha1/HTTPProbeConfig
type HTTPProbeConfigV1Alpha1 struct {
meta.Meta `yaml:",inline"`
// description: |
// Name of the probe.
// examples:
// - value: >
// "http-check"
// schemaRequired: true
MetaName string `yaml:"name"`
//nolint:embeddedstructfieldcheck
CommonProbeConfig `yaml:",inline"`
// description: |
// HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.
// Probe does not follow redirects.
// examples:
// - value: >
// "https://example.com"
// schema:
// type: string
// pattern: "^(http|https)://"
// schemaRequired: true
HTTPEndpoint meta.URL `yaml:"url"`
// description: |
// Timeout for the probe.
// Defaults to 10s.
// examples:
// - value: >
// 10 * time.Second
// schema:
// type: string
// pattern: ^[-+]?(((\d+(\.\d*)?|\d*(\.\d+)+)([nuµm]?s|m|h))|0)+$
HTTPTimeout time.Duration `yaml:"timeout,omitempty"`
}
// NewHTTPProbeConfigV1Alpha1 creates a new HTTPProbeConfigV1Alpha1 config document.
func NewHTTPProbeConfigV1Alpha1(name string) *HTTPProbeConfigV1Alpha1 {
return &HTTPProbeConfigV1Alpha1{
Meta: meta.Meta{
MetaKind: HTTPProbeKind,
MetaAPIVersion: "v1alpha1",
},
MetaName: name,
}
}
// Name implements config.NamedDocument interface.
func (p *HTTPProbeConfigV1Alpha1) Name() string {
return p.MetaName
}
// Clone implements config.Document interface.
func (p *HTTPProbeConfigV1Alpha1) Clone() config.Document {
return p.DeepCopy()
}
// Validate implements config.Validator interface.
func (p *HTTPProbeConfigV1Alpha1) Validate(validation.RuntimeMode, ...validation.Option) ([]string, error) {
var (
errs error
warnings []string //nolint:prealloc
)
if p.MetaName == "" {
errs = errors.Join(errs, errors.New("probe name is required"))
}
if p.HTTPEndpoint.URL == nil {
errs = errors.Join(errs, errors.New("HTTP probe URL is required"))
} else {
if p.HTTPEndpoint.URL.Scheme != "http" && p.HTTPEndpoint.URL.Scheme != "https" {
errs = errors.Join(errs, fmt.Errorf("HTTP probe URL scheme must be http or https, got %q", p.HTTPEndpoint.URL.Scheme))
} else if p.HTTPEndpoint.URL.Opaque != "" || p.HTTPEndpoint.URL.Hostname() == "" {
errs = errors.Join(errs, errors.New("HTTP probe URL must be an absolute http or https URL with a non-empty host"))
}
}
if p.HTTPTimeout < 0 {
errs = errors.Join(errs, fmt.Errorf("HTTP probe timeout cannot be negative: %s", p.HTTPTimeout))
}
extraWarnings, extraErrs := p.CommonProbeConfig.Validate()
errs = errors.Join(errs, extraErrs)
warnings = append(warnings, extraWarnings...)
return warnings, errs
}
func exampleHTTPProbeConfigV1Alpha1() *HTTPProbeConfigV1Alpha1 {
cfg := NewHTTPProbeConfigV1Alpha1("http-check")
cfg.CommonProbeConfig = CommonProbeConfig{
ProbeInterval: time.Second,
ProbeFailureThreshold: 3,
}
cfg.HTTPEndpoint.URL = ensure.Value(url.Parse("https://example.com"))
cfg.HTTPTimeout = 10 * time.Second
return cfg
}
// URL implements config.NetworkHTTPProbeConfig interface.
func (p *HTTPProbeConfigV1Alpha1) URL() meta.URL {
return p.HTTPEndpoint
}
// Timeout implements config.NetworkHTTPProbeConfig interface.
func (p *HTTPProbeConfigV1Alpha1) Timeout() time.Duration {
if p.HTTPTimeout == 0 {
return 10 * time.Second
}
return p.HTTPTimeout
}

View File

@ -0,0 +1,256 @@
// 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 network_test
import (
_ "embed"
"net/url"
"testing"
"time"
"github.com/siderolabs/gen/ensure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/pkg/machinery/config/configloader"
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
"github.com/siderolabs/talos/pkg/machinery/config/types/meta"
"github.com/siderolabs/talos/pkg/machinery/config/types/network"
)
//go:embed testdata/httpprobeconfig.yaml
var expectedHTTPProbeConfigDocument []byte
const exampleHTTPURL = "https://example.com"
func TestHTTPProbeConfigMarshalStability(t *testing.T) {
t.Parallel()
cfg := network.NewHTTPProbeConfigV1Alpha1("http-check")
cfg.CommonProbeConfig = network.CommonProbeConfig{
ProbeInterval: time.Second,
ProbeFailureThreshold: 3,
}
cfg.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
cfg.HTTPTimeout = 10 * time.Second
marshaled, err := encoder.NewEncoder(cfg, encoder.WithComments(encoder.CommentsDisabled)).Encode()
require.NoError(t, err)
t.Log(string(marshaled))
assert.Equal(t, expectedHTTPProbeConfigDocument, marshaled)
}
//nolint:dupl
func TestHTTPProbeConfigUnmarshal(t *testing.T) {
t.Parallel()
provider, err := configloader.NewFromBytes(expectedHTTPProbeConfigDocument)
require.NoError(t, err)
docs := provider.Documents()
require.Len(t, docs, 1)
assert.Equal(t, &network.HTTPProbeConfigV1Alpha1{
Meta: meta.Meta{
MetaAPIVersion: "v1alpha1",
MetaKind: network.HTTPProbeKind,
},
MetaName: "http-check",
CommonProbeConfig: network.CommonProbeConfig{
ProbeInterval: time.Second,
ProbeFailureThreshold: 3,
},
HTTPEndpoint: meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))},
HTTPTimeout: 10 * time.Second,
}, docs[0])
}
func TestHTTPProbeConfigValidate(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
cfg func() *network.HTTPProbeConfigV1Alpha1
expectedError string
expectedWarnings []string
}{
{
name: "valid config",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("test-probe")
c.CommonProbeConfig = network.CommonProbeConfig{
ProbeInterval: time.Second,
ProbeFailureThreshold: 3,
}
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
return c
},
},
{
name: "valid http url",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("test-probe")
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse("http://proxy.example.com:3128/health"))}
return c
},
},
{
name: "missing name",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("")
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
return c
},
expectedError: "probe name is required",
},
{
name: "missing URL",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("probe44")
return c
},
expectedError: "HTTP probe URL is required",
},
{
name: "invalid scheme",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("probe-scheme")
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse("ftp://example.com"))}
return c
},
expectedError: `HTTP probe URL scheme must be http or https, got "ftp"`,
},
{
name: "negative timeout",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("probe33")
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
c.HTTPTimeout = -5 * time.Second
return c
},
expectedError: "HTTP probe timeout cannot be negative: -5s",
},
{
name: "negative values",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("probe33")
c.CommonProbeConfig.ProbeFailureThreshold = -1
c.CommonProbeConfig.ProbeInterval = -time.Second
c.HTTPTimeout = -5 * time.Second
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
return c
},
expectedError: "HTTP probe timeout cannot be negative: -5s\nprobe interval cannot be negative: -1s\nprobe failure threshold cannot be negative: -1",
},
{
name: "empty",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
return network.NewHTTPProbeConfigV1Alpha1("")
},
expectedError: "probe name is required\nHTTP probe URL is required",
},
{
name: "url without host",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("no-host")
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse("https://"))}
return c
},
expectedError: "HTTP probe URL must be an absolute http or https URL with a non-empty host",
},
{
name: "opaque url",
cfg: func() *network.HTTPProbeConfigV1Alpha1 {
c := network.NewHTTPProbeConfigV1Alpha1("opaque")
c.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse("http:opaque-url"))}
return c
},
expectedError: "HTTP probe URL must be an absolute http or https URL with a non-empty host",
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
warnings, err := test.cfg().Validate(validationMode{})
assert.Equal(t, test.expectedWarnings, warnings)
if test.expectedError != "" {
assert.EqualError(t, err, test.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}
func TestHTTPProbeConfigMethods(t *testing.T) {
t.Parallel()
t.Run("URL", func(t *testing.T) {
t.Parallel()
cfg := network.NewHTTPProbeConfigV1Alpha1("test")
cfg.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
assert.Equal(t, meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}, cfg.URL())
})
t.Run("Name", func(t *testing.T) {
t.Parallel()
probeName := "my-probe"
cfg := network.NewHTTPProbeConfigV1Alpha1(probeName)
assert.Equal(t, probeName, cfg.Name())
})
t.Run("Timeout with default", func(t *testing.T) {
t.Parallel()
cfg := network.NewHTTPProbeConfigV1Alpha1("test")
cfg.HTTPTimeout = 0
assert.Equal(t, 10*time.Second, cfg.Timeout())
})
t.Run("Timeout with custom value", func(t *testing.T) {
t.Parallel()
cfg := network.NewHTTPProbeConfigV1Alpha1("test")
cfg.HTTPTimeout = 5 * time.Second
assert.Equal(t, 5*time.Second, cfg.Timeout())
})
t.Run("Clone", func(t *testing.T) {
t.Parallel()
cfg := network.NewHTTPProbeConfigV1Alpha1("clone-test")
cfg.CommonProbeConfig = network.CommonProbeConfig{
ProbeInterval: 500 * time.Millisecond,
ProbeFailureThreshold: 5,
}
cfg.HTTPEndpoint = meta.URL{URL: ensure.Value(url.Parse(exampleHTTPURL))}
cfg.HTTPTimeout = 15 * time.Second
cloned := cfg.Clone()
assert.Equal(t, cfg, cloned)
assert.NotSame(t, cfg, cloned)
})
}

View File

@ -5,6 +5,6 @@
// Package network provides network machine configuration documents.
package network
//go:generate go tool github.com/siderolabs/talos/tools/docgen -output network_doc.go network.go blackhole_route.go bond.go bridge.go vrf.go default_action_config.go dhcp4.go dhcp6.go dummy.go ethernet.go hcloud_vip.go hostname.go kubespan.go kubespan_endpoints.go layer2_vip.go link.go link_alias.go port_range.go resolver.go routing_rule.go rule_config.go static_host.go tcp_probe.go time_sync.go vlan.go wireguard.go
//go:generate go tool github.com/siderolabs/talos/tools/docgen -output network_doc.go network.go blackhole_route.go bond.go bridge.go vrf.go default_action_config.go dhcp4.go dhcp6.go dummy.go ethernet.go hcloud_vip.go hostname.go http_probe.go kubespan.go kubespan_endpoints.go layer2_vip.go link.go link_alias.go port_range.go resolver.go routing_rule.go rule_config.go static_host.go tcp_probe.go time_sync.go vlan.go wireguard.go
//go:generate go tool github.com/siderolabs/deep-copy -type BlackholeRouteConfigV1Alpha1 -type BondConfigV1Alpha1 -type BridgeConfigV1Alpha1 -type VRFConfigV1Alpha1 -type DefaultActionConfigV1Alpha1 -type DHCPv4ConfigV1Alpha1 -type DHCPv6ConfigV1Alpha1 -type DummyLinkConfigV1Alpha1 -type EthernetConfigV1Alpha1 -type HCloudVIPConfigV1Alpha1 -type HostnameConfigV1Alpha1 -type KubeSpanConfigV1Alpha1 -type KubespanEndpointsConfigV1Alpha1 -type Layer2VIPConfigV1Alpha1 -type LinkConfigV1Alpha1 -type LinkAliasConfigV1Alpha1 -type ResolverConfigV1Alpha1 -type RoutingRuleConfigV1Alpha1 -type RuleConfigV1Alpha1 -type StaticHostConfigV1Alpha1 -type TCPProbeConfigV1Alpha1 -type TimeSyncConfigV1Alpha1 -type VLANConfigV1Alpha1 -type WireguardConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go .
//go:generate go tool github.com/siderolabs/deep-copy -type BlackholeRouteConfigV1Alpha1 -type BondConfigV1Alpha1 -type BridgeConfigV1Alpha1 -type VRFConfigV1Alpha1 -type DefaultActionConfigV1Alpha1 -type DHCPv4ConfigV1Alpha1 -type DHCPv6ConfigV1Alpha1 -type DummyLinkConfigV1Alpha1 -type EthernetConfigV1Alpha1 -type HCloudVIPConfigV1Alpha1 -type HTTPProbeConfigV1Alpha1 -type HostnameConfigV1Alpha1 -type KubeSpanConfigV1Alpha1 -type KubespanEndpointsConfigV1Alpha1 -type Layer2VIPConfigV1Alpha1 -type LinkConfigV1Alpha1 -type LinkAliasConfigV1Alpha1 -type ResolverConfigV1Alpha1 -type RoutingRuleConfigV1Alpha1 -type RuleConfigV1Alpha1 -type StaticHostConfigV1Alpha1 -type TCPProbeConfigV1Alpha1 -type TimeSyncConfigV1Alpha1 -type VLANConfigV1Alpha1 -type WireguardConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go .

View File

@ -997,6 +997,53 @@ func (HostnameConfigV1Alpha1) Doc() *encoder.Doc {
return doc
}
func (HTTPProbeConfigV1Alpha1) Doc() *encoder.Doc {
doc := &encoder.Doc{
Type: "HTTPProbeConfig",
Comments: [3]string{"" /* encoder.HeadComment */, "HTTPProbeConfig is a config document to configure network HTTP connectivity probes." /* encoder.LineComment */, "" /* encoder.FootComment */},
Description: "HTTPProbeConfig is a config document to configure network HTTP connectivity probes.",
Fields: []encoder.Doc{
{
Type: "Meta",
Inline: true,
},
{
Name: "name",
Type: "string",
Note: "",
Description: "Name of the probe.",
Comments: [3]string{"" /* encoder.HeadComment */, "Name of the probe." /* encoder.LineComment */, "" /* encoder.FootComment */},
},
{
Type: "CommonProbeConfig",
Inline: true,
},
{
Name: "url",
Type: "URL",
Note: "",
Description: "HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.",
Comments: [3]string{"" /* encoder.HeadComment */, "HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code." /* encoder.LineComment */, "" /* encoder.FootComment */},
},
{
Name: "timeout",
Type: "Duration",
Note: "",
Description: "Timeout for the probe.\nDefaults to 10s.",
Comments: [3]string{"" /* encoder.HeadComment */, "Timeout for the probe." /* encoder.LineComment */, "" /* encoder.FootComment */},
},
},
}
doc.AddExample("", exampleHTTPProbeConfigV1Alpha1())
doc.Fields[1].AddExample("", "http-check")
doc.Fields[3].AddExample("", "https://example.com")
doc.Fields[4].AddExample("", 10*time.Second)
return doc
}
func (KubeSpanConfigV1Alpha1) Doc() *encoder.Doc {
doc := &encoder.Doc{
Type: "KubeSpanConfig",
@ -1864,6 +1911,10 @@ func (CommonProbeConfig) Doc() *encoder.Doc {
Comments: [3]string{"" /* encoder.HeadComment */, "CommonProbeConfig holds fields common to all probe types." /* encoder.LineComment */, "" /* encoder.FootComment */},
Description: "CommonProbeConfig holds fields common to all probe types.",
AppearsIn: []encoder.Appearance{
{
TypeName: "HTTPProbeConfigV1Alpha1",
FieldName: "",
},
{
TypeName: "TCPProbeConfigV1Alpha1",
FieldName: "",
@ -2190,6 +2241,7 @@ func GetFileDoc() *encoder.FileDoc {
EthernetChannelsConfig{}.Doc(),
HCloudVIPConfigV1Alpha1{}.Doc(),
HostnameConfigV1Alpha1{}.Doc(),
HTTPProbeConfigV1Alpha1{}.Doc(),
KubeSpanConfigV1Alpha1{}.Doc(),
KubeSpanFiltersConfig{}.Doc(),
KubespanEndpointsConfigV1Alpha1{}.Doc(),

View File

@ -40,6 +40,7 @@ func TestTCPProbeConfigMarshalStability(t *testing.T) {
assert.Equal(t, expectedTCPProbeConfigDocument, marshaled)
}
//nolint:dupl
func TestTCPProbeConfigUnmarshal(t *testing.T) {
t.Parallel()

View File

@ -0,0 +1,7 @@
apiVersion: v1alpha1
kind: HTTPProbeConfig
name: http-check
interval: 1s
failureThreshold: 3
url: https://example.com
timeout: 10s

View File

@ -8,6 +8,7 @@ package network
import (
"net/netip"
"net/url"
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
)
@ -569,6 +570,14 @@ func (o PlatformConfigSpec) DeepCopy() PlatformConfigSpec {
// DeepCopy generates a deep copy of ProbeSpecSpec.
func (o ProbeSpecSpec) DeepCopy() ProbeSpecSpec {
var cp ProbeSpecSpec = o
if o.HTTP.URL != nil {
cp.HTTP.URL = new(url.URL)
*cp.HTTP.URL = *o.HTTP.URL
if o.HTTP.URL.User != nil {
cp.HTTP.URL.User = new(url.Userinfo)
*cp.HTTP.URL.User = *o.HTTP.URL.User
}
}
return cp
}

View File

@ -7,6 +7,7 @@ package network
import (
"errors"
"fmt"
"net/url"
"time"
"github.com/cosi-project/runtime/pkg/resource"
@ -31,21 +32,29 @@ type ProbeSpecSpec struct {
Interval time.Duration `yaml:"interval" protobuf:"1"`
// FailureThreshold is the number of consecutive failures for the probe to be considered failed after having succeeded.
FailureThreshold int `yaml:"failureThreshold" protobuf:"2"`
// One of the probe types should be specified, for now it's only TCP.
// TCP is the TCP probe spec. One of TCP or HTTP must be specified.
TCP TCPProbeSpec `yaml:"tcp,omitempty" protobuf:"3"`
// Configuration layer.
ConfigLayer ConfigLayer `yaml:"layer" protobuf:"4"`
// HTTP is the HTTP probe spec. One of TCP or HTTP must be specified.
HTTP HTTPProbeSpec `yaml:"http,omitempty" protobuf:"5"`
}
// ID returns the ID of the resource based on the spec.
func (spec *ProbeSpecSpec) ID() (resource.ID, error) {
var zeroTCP TCPProbeSpec
if spec.TCP == zeroTCP {
return "", errors.New("no probe type specified")
if spec.TCP != zeroTCP {
return fmt.Sprintf("tcp:%s", spec.TCP.Endpoint), nil
}
return fmt.Sprintf("tcp:%s", spec.TCP.Endpoint), nil
var zeroHTTP HTTPProbeSpec
if spec.HTTP != zeroHTTP {
return fmt.Sprintf("http:%s", spec.HTTP.URL.String()), nil
}
return "", errors.New("no probe type specified")
}
// Equal returns true if the specs are equal.
@ -63,6 +72,16 @@ type TCPProbeSpec struct {
Timeout time.Duration `yaml:"timeout" protobuf:"2"`
}
// HTTPProbeSpec describes the HTTP Probe.
//
//gotagsrewrite:gen
type HTTPProbeSpec struct {
// URL to probe: http:// or https:// URL.
URL *url.URL `yaml:"url" protobuf:"1"`
// Timeout for the probe.
Timeout time.Duration `yaml:"timeout" protobuf:"2"`
}
// NewProbeSpec initializes a ProbeSpec resource.
func NewProbeSpec(namespace resource.Namespace, id resource.ID) *ProbeSpec {
return typed.NewResource[ProbeSpecSpec, ProbeSpecExtension](

View File

@ -21,7 +21,6 @@ import (
"text/template"
"github.com/siderolabs/gen/xslices"
"go.yaml.in/yaml/v4"
"mvdan.cc/gofumpt/format"
)

View File

@ -534,6 +534,7 @@ description: Talos gRPC API reference.
- [EthernetSpecSpec](#talos.resource.definitions.network.EthernetSpecSpec)
- [EthernetSpecSpec.FeaturesEntry](#talos.resource.definitions.network.EthernetSpecSpec.FeaturesEntry)
- [EthernetStatusSpec](#talos.resource.definitions.network.EthernetStatusSpec)
- [HTTPProbeSpec](#talos.resource.definitions.network.HTTPProbeSpec)
- [HardwareAddrSpec](#talos.resource.definitions.network.HardwareAddrSpec)
- [HostDNSConfigSpec](#talos.resource.definitions.network.HostDNSConfigSpec)
- [HostnameSpecSpec](#talos.resource.definitions.network.HostnameSpecSpec)
@ -9373,6 +9374,22 @@ EthernetStatusSpec describes status of rendered secrets.
<a name="talos.resource.definitions.network.HTTPProbeSpec"></a>
### HTTPProbeSpec
HTTPProbeSpec describes the HTTP Probe.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| url | [common.URL](#common.URL) | | URL to probe: http:// or https:// URL. |
| timeout | [google.protobuf.Duration](#google.protobuf.Duration) | | Timeout for the probe. |
<a name="talos.resource.definitions.network.HardwareAddrSpec"></a>
### HardwareAddrSpec
@ -9870,8 +9887,9 @@ ProbeSpecSpec describes the Probe.
| ----- | ---- | ----- | ----------- |
| interval | [google.protobuf.Duration](#google.protobuf.Duration) | | Interval between the probes. |
| failure_threshold | [int64](#int64) | | FailureThreshold is the number of consecutive failures for the probe to be considered failed after having succeeded. |
| tcp | [TCPProbeSpec](#talos.resource.definitions.network.TCPProbeSpec) | | One of the probe types should be specified, for now it's only TCP. |
| tcp | [TCPProbeSpec](#talos.resource.definitions.network.TCPProbeSpec) | | TCP is the TCP probe spec. One of TCP or HTTP must be specified. |
| config_layer | [talos.resource.definitions.enums.NetworkConfigLayer](#talos.resource.definitions.enums.NetworkConfigLayer) | | Configuration layer. |
| http | [HTTPProbeSpec](#talos.resource.definitions.network.HTTPProbeSpec) | | HTTP is the HTTP probe spec. One of TCP or HTTP must be specified. |

View File

@ -0,0 +1,49 @@
---
description: HTTPProbeConfig is a config document to configure network HTTP connectivity probes.
title: HTTPProbeConfig
---
<!-- markdownlint-disable -->
{{< highlight yaml >}}
apiVersion: v1alpha1
kind: HTTPProbeConfig
name: http-check # Name of the probe.
interval: 1s # Interval between probe attempts.
failureThreshold: 3 # Number of consecutive failures for the probe to be considered failed after having succeeded.
url: https://example.com # HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.
timeout: 10s # Timeout for the probe.
{{< /highlight >}}
| Field | Type | Description | Value(s) |
|-------|------|-------------|----------|
|`name` |string |Name of the probe. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
name: http-check
{{< /highlight >}}</details> | |
|`interval` |Duration |Interval between probe attempts.<br>Defaults to 1s. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
interval: 1s
{{< /highlight >}}</details> | |
|`failureThreshold` |int |Number of consecutive failures for the probe to be considered failed after having succeeded.<br>Defaults to 0 (immediately fail on first failure). <details><summary>Show example(s)</summary>{{< highlight yaml >}}
failureThreshold: 3
{{< /highlight >}}</details> | |
|`url` |URL |HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.<br>Probe does not follow redirects. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
url: https://example.com
{{< /highlight >}}</details> | |
|`timeout` |Duration |Timeout for the probe.<br>Defaults to 10s. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
timeout: 10s
{{< /highlight >}}</details> | |

View File

@ -2226,6 +2226,75 @@
],
"description": "HCloudVIPConfig is a config document to configure virtual IP using Hetzner Cloud APIs for announcement.\\nVirtual IP configuration should be used only on controlplane nodes to provide virtual IP for Kubernetes API server.\\nAny other use cases are not supported and may lead to unexpected behavior.\\nVirtual IP will be announced from only one node at a time using Hetzner Cloud APIs.\\n"
},
"network.HTTPProbeConfigV1Alpha1": {
"properties": {
"apiVersion": {
"enum": [
"v1alpha1"
],
"title": "apiVersion",
"description": "apiVersion is the API version of the resource.\n",
"markdownDescription": "apiVersion is the API version of the resource.",
"x-intellij-html-description": "\u003cp\u003eapiVersion is the API version of the resource.\u003c/p\u003e\n"
},
"kind": {
"enum": [
"HTTPProbeConfig"
],
"title": "kind",
"description": "kind is the kind of the resource.\n",
"markdownDescription": "kind is the kind of the resource.",
"x-intellij-html-description": "\u003cp\u003ekind is the kind of the resource.\u003c/p\u003e\n"
},
"name": {
"type": "string",
"title": "name",
"description": "Name of the probe.\n",
"markdownDescription": "Name of the probe.",
"x-intellij-html-description": "\u003cp\u003eName of the probe.\u003c/p\u003e\n"
},
"interval": {
"type": "string",
"pattern": "^[-+]?(((\\d+(\\.\\d*)?|\\d*(\\.\\d+)+)([nuµm]?s|m|h))|0)+$",
"title": "interval",
"description": "Interval between probe attempts.\nDefaults to 1s.\n",
"markdownDescription": "Interval between probe attempts.\nDefaults to 1s.",
"x-intellij-html-description": "\u003cp\u003eInterval between probe attempts.\nDefaults to 1s.\u003c/p\u003e\n"
},
"failureThreshold": {
"type": "integer",
"title": "failureThreshold",
"description": "Number of consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 0 (immediately fail on first failure).\n",
"markdownDescription": "Number of consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 0 (immediately fail on first failure).",
"x-intellij-html-description": "\u003cp\u003eNumber of consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 0 (immediately fail on first failure).\u003c/p\u003e\n"
},
"url": {
"type": "string",
"pattern": "^(http|https)://",
"title": "url",
"description": "HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.\n",
"markdownDescription": "HTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.",
"x-intellij-html-description": "\u003cp\u003eHTTP or HTTPS URL to probe. The probe succeeds if the server responds with a 2xx or 3xx status code.\nProbe does not follow redirects.\u003c/p\u003e\n"
},
"timeout": {
"type": "string",
"pattern": "^[-+]?(((\\d+(\\.\\d*)?|\\d*(\\.\\d+)+)([nuµm]?s|m|h))|0)+$",
"title": "timeout",
"description": "Timeout for the probe.\nDefaults to 10s.\n",
"markdownDescription": "Timeout for the probe.\nDefaults to 10s.",
"x-intellij-html-description": "\u003cp\u003eTimeout for the probe.\nDefaults to 10s.\u003c/p\u003e\n"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"apiVersion",
"kind",
"name",
"url"
],
"description": "HTTPProbeConfig is a config document to configure network HTTP connectivity probes."
},
"network.HostDNSConfig": {
"properties": {
"enabled": {
@ -5791,6 +5860,9 @@
{
"$ref": "#/$defs/network.HostnameConfigV1Alpha1"
},
{
"$ref": "#/$defs/network.HTTPProbeConfigV1Alpha1"
},
{
"$ref": "#/$defs/network.KubeSpanConfigV1Alpha1"
},