From 8bfa7ac1d6012746bf7264528eac5cacdd752e2b Mon Sep 17 00:00:00 2001 From: Serge Logvinov Date: Thu, 22 Sep 2022 14:27:20 +0000 Subject: [PATCH] feat: platform metadata resource This resource stores common platform metadata information. Such as: * Hostname * Region * Zone * InstanceType (SKU) * InstanceID * ProviderID (CCM cloud native magic string) Signed-off-by: Serge Logvinov Signed-off-by: Andrey Smirnov --- .../definitions/runtime/runtime.proto | 11 + .../controllers/network/platform_config.go | 34 +- .../network/platform_config_test.go | 46 ++ internal/app/machined/pkg/runtime/platform.go | 3 + .../pkg/runtime/v1alpha1/platform/aws/aws.go | 83 ++-- .../runtime/v1alpha1/platform/aws/aws_test.go | 35 +- .../runtime/v1alpha1/platform/aws/metadata.go | 77 ++++ .../platform/aws/testdata/expected.yaml | 19 + .../platform/aws/testdata/metadata.json | 7 + .../runtime/v1alpha1/platform/azure/azure.go | 51 ++- .../v1alpha1/platform/azure/azure_test.go | 17 +- .../v1alpha1/platform/azure/metadata.go | 66 +++ .../platform/azure/testdata/compute.json | 14 + .../platform/azure/testdata/expected.yaml | 7 + .../{metadata.json => interfaces.json} | 0 .../v1alpha1/platform/container/container.go | 11 +- .../platform/digitalocean/digitalocean.go | 245 +++++++++-- .../digitalocean/digitalocean_test.go | 37 +- .../platform/digitalocean/metadata.go | 81 ++++ .../digitalocean/testdata/expected.yaml | 96 +++++ .../digitalocean/testdata/metadata.json | 70 +++ .../v1alpha1/platform/equinixmetal/equinix.go | 28 +- .../platform/equinixmetal/equinix_test.go | 2 +- .../platform/equinixmetal/metadata.go | 16 + .../equinixmetal/testdata/expected.yaml | 8 + .../v1alpha1/platform/errors/errors.go | 3 + .../pkg/runtime/v1alpha1/platform/gcp/gcp.go | 96 +++-- .../runtime/v1alpha1/platform/gcp/gcp_test.go | 37 +- .../runtime/v1alpha1/platform/gcp/metadata.go | 70 +++ .../platform/gcp/testdata/expected.yaml | 25 ++ .../platform/gcp/testdata/metadata.json | 8 + .../v1alpha1/platform/hcloud/hcloud.go | 78 +--- .../v1alpha1/platform/hcloud/hcloud_test.go | 11 +- .../v1alpha1/platform/hcloud/metadata.go | 99 +++++ .../platform/hcloud/testdata/expected.yaml | 7 +- .../runtime/v1alpha1/platform/metal/match.go | 59 +++ .../runtime/v1alpha1/platform/metal/metal.go | 248 ++--------- .../v1alpha1/platform/metal/metal_test.go | 239 +---------- .../runtime/v1alpha1/platform/metal/url.go | 184 ++++++++ .../v1alpha1/platform/metal/url_test.go | 248 +++++++++++ .../v1alpha1/platform/nocloud/metadata.go | 27 +- .../v1alpha1/platform/nocloud/nocloud.go | 33 +- .../v1alpha1/platform/nocloud/nocloud_test.go | 9 +- .../nocloud/testdata/expected-v1.yaml | 6 +- .../nocloud/testdata/expected-v2.yaml | 9 +- .../nocloud/testdata/metadata-v2.yaml | 2 + .../v1alpha1/platform/openstack/metadata.go | 25 +- .../v1alpha1/platform/openstack/openstack.go | 45 +- .../platform/openstack/openstack_test.go | 6 +- .../platform/openstack/testdata/expected.yaml | 6 + .../v1alpha1/platform/oracle/metadata.go | 51 +++ .../v1alpha1/platform/oracle/oracle.go | 56 +-- .../v1alpha1/platform/oracle/oracle_test.go | 13 +- .../platform/oracle/testdata/expected.yaml | 8 + .../platform/oracle/testdata/metadata.json | 63 ++- .../oracle/testdata/metadatanetwork.json | 12 + .../v1alpha1/platform/scaleway/metadata.go | 36 ++ .../v1alpha1/platform/scaleway/scaleway.go | 66 +-- .../platform/scaleway/scaleway_test.go | 6 +- .../platform/scaleway/testdata/expected.yaml | 6 + .../v1alpha1/platform/upcloud/metadata.go | 63 +++ .../platform/upcloud/testdata/expected.yaml | 5 + .../v1alpha1/platform/upcloud/upcloud.go | 64 +-- .../v1alpha1/platform/upcloud/upcloud_test.go | 6 +- .../v1alpha1/platform/vmware/vmware_amd64.go | 11 + .../v1alpha1/platform/vultr/metadata.go | 38 ++ .../platform/vultr/testdata/expected.yaml | 6 + .../platform/vultr/testdata/metadata.json | 2 +- .../runtime/v1alpha1/platform/vultr/vultr.go | 44 +- .../v1alpha1/platform/vultr/vultr_test.go | 6 +- .../pkg/runtime/v1alpha2/v1alpha2_state.go | 1 + .../definitions/runtime/runtime.pb.go | 163 ++++++- .../definitions/runtime/runtime_vtproto.pb.go | 397 ++++++++++++++++++ .../resources/runtime/deep_copy.generated.go | 8 +- .../resources/runtime/platform_metadata.go | 65 +++ pkg/machinery/resources/runtime/runtime.go | 2 +- .../resources/runtime/runtime_test.go | 1 + pkg/provision/providers/docker/node.go | 5 +- website/content/v1.3/reference/api.md | 22 + 79 files changed, 2954 insertions(+), 916 deletions(-) create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/aws/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/expected.yaml create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/metadata.json create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/compute.json rename internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/{metadata.json => interfaces.json} (100%) create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/expected.yaml create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/metadata.json create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/expected.yaml create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/metadata.json create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/metal/match.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url_test.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadatanetwork.json create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/metadata.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/metadata.go create mode 100644 pkg/machinery/resources/runtime/platform_metadata.go diff --git a/api/resource/definitions/runtime/runtime.proto b/api/resource/definitions/runtime/runtime.proto index b5c1041e2..deb1a0199 100755 --- a/api/resource/definitions/runtime/runtime.proto +++ b/api/resource/definitions/runtime/runtime.proto @@ -45,6 +45,17 @@ message MountStatusSpec { repeated string options = 4; } +// PlatformMetadataSpec describes platform metadata properties. +message PlatformMetadataSpec { + string platform = 1; + string hostname = 2; + string region = 3; + string zone = 4; + string instance_type = 5; + string instance_id = 6; + string provider_id = 7; +} + // UnmetCondition is a failure which prevents machine from being ready at the stage. message UnmetCondition { string name = 1; diff --git a/internal/app/machined/pkg/controllers/network/platform_config.go b/internal/app/machined/pkg/controllers/network/platform_config.go index cd85a7b7b..a08c0fe0a 100644 --- a/internal/app/machined/pkg/controllers/network/platform_config.go +++ b/internal/app/machined/pkg/controllers/network/platform_config.go @@ -92,6 +92,10 @@ func (ctrl *PlatformConfigController) Outputs() []controller.Output { Type: network.OperatorSpecType, Kind: controller.OutputShared, }, + { + Type: runtimeres.PlatformMetadataType, + Kind: controller.OutputExclusive, + }, } } @@ -173,7 +177,7 @@ func (ctrl *PlatformConfigController) Run(ctx context.Context, r controller.Runt if stateMounted && networkConfig != nil { if err := ctrl.storeConfig(filepath.Join(ctrl.StatePath, constants.PlatformNetworkConfigFilename), networkConfig); err != nil { - return fmt.Errorf("error saving config: %w", err) + return fmt.Errorf("error saving platform network config: %w", err) } logger.Debug("stored cached platform network config") @@ -198,6 +202,12 @@ func (ctrl *PlatformConfigController) Run(ctx context.Context, r controller.Runt //nolint:dupl,gocyclo func (ctrl *PlatformConfigController) apply(ctx context.Context, r controller.Runtime, networkConfig *v1alpha1runtime.PlatformNetworkConfig) error { + metadataLength := 0 + + if networkConfig.Metadata != nil { + metadataLength = 1 + } + // handle all network specs in a loop as all specs can be handled in a similar way for _, specType := range []struct { length int @@ -408,6 +418,28 @@ func (ctrl *PlatformConfigController) apply(ctx context.Context, r controller.Ru status.Scope = nethelpers.ScopeGlobal + return nil + } + }, + }, + // Platform metadata + { + length: metadataLength, + getter: func(i int) interface{} { + return networkConfig.Metadata + }, + idBuilder: func(spec interface{}) resource.ID { + return runtimeres.PlatformMetadataID + }, + resourceBuilder: func(id string) resource.Resource { + return runtimeres.NewPlatformMetadataSpec(runtimeres.NamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + metadata := newSpec.(*runtimeres.PlatformMetadataSpec) //nolint:errcheck,forcetypeassert + + *r.(*runtimeres.PlatformMetadata).TypedSpec() = *metadata + return nil } }, diff --git a/internal/app/machined/pkg/controllers/network/platform_config_test.go b/internal/app/machined/pkg/controllers/network/platform_config_test.go index 21bac37fc..a83d3913f 100644 --- a/internal/app/machined/pkg/controllers/network/platform_config_test.go +++ b/internal/app/machined/pkg/controllers/network/platform_config_test.go @@ -477,6 +477,44 @@ func (suite *PlatformConfigSuite) TestPlatformMockExternalIPs() { ) } +func (suite *PlatformConfigSuite) TestPlatformMockMetadata() { + suite.Require().NoError( + suite.runtime.RegisterController( + &netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + metadata: &runtimeres.PlatformMetadataSpec{ + Platform: "mock", + Zone: "mock-zone", + }, + }, + StatePath: suite.statePath, + PlatformState: suite.state, + }, + ), + ) + + suite.startRuntime() + + suite.Assert().NoError( + retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources( + runtimeres.NamespaceName, runtimeres.PlatformMetadataType, []string{ + runtimeres.PlatformMetadataID, + }, func(r resource.Resource) error { + spec := r.(*runtimeres.PlatformMetadata).TypedSpec() + + suite.Assert().Equal("mock", spec.Platform) + suite.Assert().Equal("mock-zone", spec.Zone) + + return nil + }, + ) + }, + ), + ) +} + const sampleStoredConfig = "addresses: []\nlinks: []\nroutes: []\nhostnames:\n - hostname: talos-e2e-897b4e49-gcp-controlplane-jvcnl\n domainname: \"\"\n layer: default\nresolvers: []\ntimeServers: []\noperators: []\nexternalIPs:\n - 10.3.4.5\n - 2001:470:6d:30e:96f4:4219:5733:b860\n" //nolint:lll func (suite *PlatformConfigSuite) TestStoreConfig() { @@ -620,6 +658,8 @@ type platformMock struct { resolvers []netip.Addr timeServers []string dhcp4Links []string + + metadata *runtimeres.PlatformMetadataSpec } func (mock *platformMock) Name() string { @@ -630,6 +670,10 @@ func (mock *platformMock) Configuration(context.Context, state.State) ([]byte, e return nil, nil } +func (mock *platformMock) Metadata(context.Context, state.State) (runtimeres.PlatformMetadataSpec, error) { + return runtimeres.PlatformMetadataSpec{Platform: mock.Name()}, nil +} + func (mock *platformMock) Mode() v1alpha1runtime.Mode { return v1alpha1runtime.ModeCloud } @@ -741,6 +785,8 @@ func (mock *platformMock) NetworkConfiguration( ) } + networkConfig.Metadata = mock.metadata + select { case ch <- networkConfig: case <-ctx.Done(): diff --git a/internal/app/machined/pkg/runtime/platform.go b/internal/app/machined/pkg/runtime/platform.go index 017acfe8e..9d85359f5 100644 --- a/internal/app/machined/pkg/runtime/platform.go +++ b/internal/app/machined/pkg/runtime/platform.go @@ -12,6 +12,7 @@ import ( "github.com/talos-systems/go-procfs/procfs" "github.com/talos-systems/talos/pkg/machinery/resources/network" + "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Platform defines the requirements for a platform. @@ -55,4 +56,6 @@ type PlatformNetworkConfig struct { Operators []network.OperatorSpecSpec `yaml:"operators"` ExternalIPs []netip.Addr `yaml:"externalIPs"` + + Metadata *runtime.PlatformMetadataSpec `yaml:"metadata,omitempty"` } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go index 7e65b57d3..31dfe536c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go @@ -24,6 +24,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // AWS is the concrete type that implements the runtime.Platform interface. @@ -47,6 +48,44 @@ func NewAWS() (*AWS, error) { return a, nil } +// ParseMetadata converts AWS platform metadata into platform network config. +func (a *AWS) ParseMetadata(metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} + + if metadata.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + if metadata.PublicIPv4 != "" { + ip, err := netip.ParseAddr(metadata.PublicIPv4) + if err != nil { + return nil, err + } + + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) + } + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: a.Name(), + Hostname: metadata.Hostname, + Region: metadata.Region, + Zone: metadata.Zone, + InstanceType: metadata.InstanceType, + InstanceID: metadata.InstanceID, + ProviderID: fmt.Sprintf("aws://%s/%s", metadata.Zone, metadata.InstanceID), + } + + return networkConfig, nil +} + // Name implements the runtime.Platform interface. func (a *AWS) Name() string { return "aws" @@ -87,57 +126,19 @@ func (a *AWS) KernelArgs() procfs.Parameters { } // NetworkConfiguration implements the runtime.Platform interface. -// -//nolint:gocyclo func (a *AWS) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { - getMetadataKey := func(key string) (string, error) { - v, err := a.metadataClient.GetMetadataWithContext(ctx, key) - if err != nil { - if awsErr, ok := err.(awserr.RequestFailure); ok { - if awsErr.StatusCode() == http.StatusNotFound { - return "", nil - } - } + log.Printf("fetching aws instance config") - return "", fmt.Errorf("failed to fetch %q from IMDS: %w", key, err) - } - - return v, nil - } - - networkConfig := &runtime.PlatformNetworkConfig{} - - hostname, err := getMetadataKey("hostname") + metadata, err := a.getMetadata(ctx) if err != nil { return err } - if hostname != "" { - hostnameSpec := network.HostnameSpecSpec{ - ConfigLayer: network.ConfigPlatform, - } - - if err = hostnameSpec.ParseFQDN(hostname); err != nil { - return err - } - - networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) - } - - externalIP, err := getMetadataKey("public-ipv4") + networkConfig, err := a.ParseMetadata(metadata) if err != nil { return err } - if externalIP != "" { - ip, err := netip.ParseAddr(externalIP) - if err != nil { - return err - } - - networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) - } - select { case ch <- networkConfig: case <-ctx.Done(): diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws_test.go index ab2cf960f..c3d065f7b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws_test.go @@ -4,11 +4,36 @@ package aws_test -import "testing" +import ( + _ "embed" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/aws" +) + +//go:embed testdata/metadata.json +var rawMetadata []byte + +//go:embed testdata/expected.yaml +var expectedNetworkConfig string func TestEmpty(t *testing.T) { - // added for accurate coverage estimation - // - // please remove it once any unit-test is added - // for this package + p := &aws.AWS{} + + var metadata aws.MetadataConfig + + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) + + networkConfig, err := p.ParseMetadata(&metadata) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/metadata.go new file mode 100644 index 000000000..268cd7f27 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/metadata.go @@ -0,0 +1,77 @@ +// 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 aws + +import ( + "context" + "fmt" + "net/http" + + "github.com/aws/aws-sdk-go/aws/awserr" +) + +// MetadataConfig represents a metadata AWS instance. +type MetadataConfig struct { + Hostname string `json:"hostname,omitempty"` + InstanceID string `json:"instance-id,omitempty"` + InstanceType string `json:"instance-type,omitempty"` + PublicIPv4 string `json:"public-ipv4,omitempty"` + PublicIPv6 string `json:"ipv6,omitempty"` + Region string `json:"region,omitempty"` + Zone string `json:"zone,omitempty"` +} + +//nolint:gocyclo +func (a *AWS) getMetadata(ctx context.Context) (*MetadataConfig, error) { + getMetadataKey := func(key string) (v string, err error) { + v, err = a.metadataClient.GetMetadataWithContext(ctx, key) + if err != nil { + if awsErr, ok := err.(awserr.RequestFailure); ok { + if awsErr.StatusCode() == http.StatusNotFound { + return "", nil + } + } + + return "", fmt.Errorf("failed to fetch %q from IMDS: %w", key, err) + } + + return v, nil + } + + var ( + metadata MetadataConfig + err error + ) + + if metadata.Hostname, err = getMetadataKey("hostname"); err != nil { + return nil, err + } + + if metadata.InstanceType, err = getMetadataKey("instance-type"); err != nil { + return nil, err + } + + if metadata.InstanceID, err = getMetadataKey("instance-id"); err != nil { + return nil, err + } + + if metadata.PublicIPv4, err = getMetadataKey("public-ipv4"); err != nil { + return nil, err + } + + if metadata.PublicIPv6, err = getMetadataKey("ipv6"); err != nil { + return nil, err + } + + if metadata.Region, err = getMetadataKey("placement/region"); err != nil { + return nil, err + } + + if metadata.Zone, err = getMetadataKey("placement/availability-zone"); err != nil { + return nil, err + } + + return &metadata, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/expected.yaml new file mode 100644 index 000000000..d28facea6 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/expected.yaml @@ -0,0 +1,19 @@ +addresses: [] +links: [] +routes: [] +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: [] +timeServers: [] +operators: [] +externalIPs: + - 1.2.3.4 +metadata: + platform: aws + hostname: talos + region: us-east-1 + zone: us-east-1a + instanceId: i-0a0a0a0a0a0a0a0a0 + providerId: aws://us-east-1a/i-0a0a0a0a0a0a0a0a0 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/metadata.json new file mode 100644 index 000000000..a0358b15e --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/testdata/metadata.json @@ -0,0 +1,7 @@ +{ + "hostname": "talos", + "instance-id": "i-0a0a0a0a0a0a0a0a0", + "public-ipv4": "1.2.3.4", + "region": "us-east-1", + "zone": "us-east-1a" +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go index 44834e7a6..eb3da4a3a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go @@ -27,20 +27,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/resources/network" -) - -const ( - // AzureInternalEndpoint is the Azure Internal Channel IP - // https://blogs.msdn.microsoft.com/mast/2015/05/18/what-is-the-ip-address-168-63-129-16/ - AzureInternalEndpoint = "http://168.63.129.16" - // AzureHostnameEndpoint is the local endpoint for the hostname. - AzureHostnameEndpoint = "http://169.254.169.254/metadata/instance/compute/osProfile/computerName?api-version=2021-12-13&format=text" - // AzureInterfacesEndpoint is the local endpoint to get external IPs. - AzureInterfacesEndpoint = "http://169.254.169.254/metadata/instance/network/interface?api-version=2021-12-13&format=json" - // AzureLoadbalancerEndpoint is the local endpoint for load balancer config. - AzureLoadbalancerEndpoint = "http://169.254.169.254/metadata/loadbalancer?api-version=2021-05-01&format=json" - - mnt = "/mnt" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // NetworkConfig holds network interface meta config. @@ -86,7 +73,7 @@ func (a *Azure) Name() string { // ParseMetadata parses Azure network metadata into the platform network config. // //nolint:gocyclo -func (a *Azure) ParseMetadata(interfaceAddresses []NetworkConfig, host []byte) (*runtime.PlatformNetworkConfig, error) { +func (a *Azure) ParseMetadata(metadata *ComputeMetadata, interfaceAddresses []NetworkConfig, host []byte) (*runtime.PlatformNetworkConfig, error) { var networkConfig runtime.PlatformNetworkConfig // hostname @@ -137,6 +124,21 @@ func (a *Azure) ParseMetadata(interfaceAddresses []NetworkConfig, host []byte) ( } } + zone := metadata.FaultDomain + if metadata.Zone != "" { + zone = fmt.Sprintf("%s-%s", metadata.Location, metadata.Zone) + } + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: a.Name(), + Hostname: metadata.OSProfile.ComputerName, + Region: strings.ToLower(metadata.Location), + Zone: strings.ToLower(zone), + InstanceType: metadata.VMSize, + InstanceID: metadata.ResourceID, + ProviderID: fmt.Sprintf("azure://%s", metadata.ResourceID), + } + return &networkConfig, nil } @@ -251,6 +253,13 @@ func (a *Azure) configFromCD() ([]byte, error) { // //nolint:gocyclo func (a *Azure) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching azure instance config from: %q", AzureMetadataEndpoint) + + metadata, err := a.getMetadata(ctx) + if err != nil { + return err + } + log.Printf("fetching network config from %q", AzureInterfacesEndpoint) metadataNetworkConfig, err := download.Download(ctx, AzureInterfacesEndpoint, @@ -265,17 +274,7 @@ func (a *Azure) NetworkConfiguration(ctx context.Context, _ state.State, ch chan return err } - log.Printf("fetching hostname from: %q", AzureHostnameEndpoint) - - host, err := download.Download(ctx, AzureHostnameEndpoint, - download.WithHeaders(map[string]string{"Metadata": "true"}), - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { - return fmt.Errorf("failed to fetch hostname from metadata service: %w", err) - } - - networkConfig, err := a.ParseMetadata(interfaceAddresses, host) + networkConfig, err := a.ParseMetadata(metadata, interfaceAddresses, []byte(metadata.OSProfile.ComputerName)) if err != nil { return fmt.Errorf("failed to parse network metadata: %w", err) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go index 86f306ff4..a77b7c3ed 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go @@ -16,8 +16,11 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/azure" ) -//go:embed testdata/metadata.json -var rawMetadata []byte +//go:embed testdata/interfaces.json +var rawInterfaces []byte + +//go:embed testdata/compute.json +var rawCompute []byte //go:embed testdata/loadbalancer.json var rawLoadBalancerMetadata []byte @@ -28,11 +31,15 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { a := &azure.Azure{} - var m []azure.NetworkConfig + var interfacesMetadata []azure.NetworkConfig - require.NoError(t, json.Unmarshal(rawMetadata, &m)) + require.NoError(t, json.Unmarshal(rawInterfaces, &interfacesMetadata)) - networkConfig, err := a.ParseMetadata(m, []byte("some.fqdn")) + var computeMetadata azure.ComputeMetadata + + require.NoError(t, json.Unmarshal(rawCompute, &computeMetadata)) + + networkConfig, err := a.ParseMetadata(&computeMetadata, interfacesMetadata, []byte("some.fqdn")) require.NoError(t, err) var lb azure.LoadBalancerMetadata diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.go new file mode 100644 index 000000000..ab081f2fb --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.go @@ -0,0 +1,66 @@ +// 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 azure + +import ( + "context" + "encoding/json" + stderrors "errors" + "fmt" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // AzureInternalEndpoint is the Azure Internal Channel IP + // https://blogs.msdn.microsoft.com/mast/2015/05/18/what-is-the-ip-address-168-63-129-16/ + AzureInternalEndpoint = "http://168.63.129.16" + // AzureMetadataEndpoint is the local endpoint for the metadata. + AzureMetadataEndpoint = "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json" + // AzureInterfacesEndpoint is the local endpoint to get external IPs. + AzureInterfacesEndpoint = "http://169.254.169.254/metadata/instance/network/interface?api-version=2021-12-13&format=json" + // AzureLoadbalancerEndpoint is the local endpoint for load balancer config. + AzureLoadbalancerEndpoint = "http://169.254.169.254/metadata/loadbalancer?api-version=2021-05-01&format=json" + + mnt = "/mnt" +) + +// ComputeMetadata represents metadata compute information. +type ComputeMetadata struct { + Environment string `json:"azEnvironment,omitempty"` + SKU string `json:"sku,omitempty"` + Name string `json:"name,omitempty"` + Zone string `json:"zone,omitempty"` + VMSize string `json:"vmSize,omitempty"` + OSType string `json:"osType,omitempty"` + OSProfile struct { + ComputerName string `json:"computerName,omitempty"` + } `json:"osProfile,omitempty"` + Location string `json:"location,omitempty"` + FaultDomain string `json:"platformFaultDomain,omitempty"` + PlatformSubFaultDomain string `json:"platformSubFaultDomain,omitempty"` + UpdateDomain string `json:"platformUpdateDomain,omitempty"` + ResourceGroup string `json:"resourceGroupName,omitempty"` + ResourceID string `json:"resourceId,omitempty"` + VMScaleSetName string `json:"vmScaleSetName,omitempty"` + SubscriptionID string `json:"subscriptionId,omitempty"` +} + +func (a *Azure) getMetadata(ctx context.Context) (*ComputeMetadata, error) { + metadataDl, err := download.Download(ctx, AzureMetadataEndpoint, + download.WithHeaders(map[string]string{"Metadata": "true"})) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return nil, fmt.Errorf("error fetching metadata: %w", err) + } + + var metadata ComputeMetadata + + if err = json.Unmarshal(metadataDl, &metadata); err != nil { + return nil, fmt.Errorf("failed to parse compute metadata: %w", err) + } + + return &metadata, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/compute.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/compute.json new file mode 100644 index 000000000..b57a05c91 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/compute.json @@ -0,0 +1,14 @@ +{ + "location": "CentralUS", + "name": "IMDSCanary", + "offer": "RHEL", + "osType": "Linux", + "platformFaultDomain": "0", + "platformUpdateDomain": "0", + "publisher": "RedHat", + "resourceId": "000-000-000-000-000", + "sku": "7.2", + "version": "7.2.20161026", + "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", + "vmSize": "Standard_DS2" +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/expected.yaml index 10fc7fe4d..fff77a4b0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/expected.yaml @@ -18,3 +18,10 @@ externalIPs: - 1.2.3.4 - 2603:1020:10:5::34 - 20.10.5.34 +metadata: + platform: azure + region: centralus + zone: "0" + instanceType: Standard_DS2 + instanceId: 000-000-000-000-000 + providerId: azure://000-000-000-000-000 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/interfaces.json similarity index 100% rename from internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/metadata.json rename to internal/app/machined/pkg/runtime/v1alpha1/platform/azure/testdata/interfaces.json diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go index 60b7afecc..5e14e76b5 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go @@ -18,6 +18,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Container is a platform for installing Talos via an Container image. @@ -64,16 +65,24 @@ func (c *Container) NetworkConfiguration(ctx context.Context, _ state.State, ch return err } + hostname = bytes.TrimSpace(hostname) + hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(string(bytes.TrimSpace(hostname))); err != nil { + if err := hostnameSpec.ParseFQDN(string(hostname)); err != nil { return err } networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: c.Name(), + Hostname: string(hostname), + InstanceType: os.Getenv("TALOSSKU"), + } + select { case ch <- networkConfig: case <-ctx.Done(): diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go index c1b53d430..4bbf2cec7 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go @@ -7,26 +7,21 @@ package digitalocean import ( "context" - stderrors "errors" + "fmt" "log" "net/netip" + "strconv" "github.com/cosi-project/runtime/pkg/state" "github.com/talos-systems/go-procfs/procfs" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/utils" "github.com/talos-systems/talos/pkg/download" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" -) - -const ( - // DigitalOceanExternalIPEndpoint displays all external addresses associated with the instance. - DigitalOceanExternalIPEndpoint = "http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address" - // DigitalOceanHostnameEndpoint is the local endpoint for the hostname. - DigitalOceanHostnameEndpoint = "http://169.254.169.254/metadata/v1/hostname" - // DigitalOceanUserDataEndpoint is the local endpoint for the config. - DigitalOceanUserDataEndpoint = "http://169.254.169.254/metadata/v1/user-data" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // DigitalOcean is the concrete type that implements the platform.Platform interface. @@ -37,6 +32,200 @@ func (d *DigitalOcean) Name() string { return "digital-ocean" } +// ParseMetadata converts DigitalOcean platform metadata into platform network config. +// +//nolint:gocyclo,cyclop +func (d *DigitalOcean) ParseMetadata(metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} + + if metadata.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + if len(metadata.DNS.Nameservers) > 0 { + var dnsIPs []netip.Addr + + for _, dnsIP := range metadata.DNS.Nameservers { + if ip, err := netip.ParseAddr(dnsIP); err == nil { + dnsIPs = append(dnsIPs, ip) + } + } + + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + DNSServers: dnsIPs, + ConfigLayer: network.ConfigPlatform, + }) + } + + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: "eth0", + Up: true, + ConfigLayer: network.ConfigPlatform, + }) + + for _, iface := range metadata.Interfaces["public"] { + if iface.IPv4 != nil { + ifAddr, err := utils.IPPrefixFrom(iface.IPv4.IPAddress, iface.IPv4.Netmask) + if err != nil { + return nil, fmt.Errorf("failed to parse ip address: %w", err) + } + + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ifAddr.Addr()) + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: "eth0", + Address: ifAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: nethelpers.FamilyInet4, + }, + ) + + if iface.IPv4.Gateway != "" { + gw, err := netip.ParseAddr(iface.IPv4.Gateway) + if err != nil { + return nil, fmt.Errorf("failed to parse gateway ip: %w", err) + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: "eth0", + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet4, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + + metaServer, _ := netip.ParsePrefix("169.254.169.254/32") //nolint:errcheck + + networkConfig.Routes = append(networkConfig.Routes, network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + OutLinkName: "eth0", + Destination: metaServer, + Gateway: gw, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet4, + Priority: 512, + }) + } + } + + if iface.IPv6 != nil { + ifAddr, err := utils.IPPrefixFrom(iface.IPv6.IPAddress, strconv.Itoa(iface.IPv6.CIDR)) + if err != nil { + return nil, fmt.Errorf("failed to parse ip address: %w", err) + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: "eth0", + Address: ifAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: nethelpers.FamilyInet6, + }, + ) + + if iface.IPv6.Gateway != "" { + gw, err := netip.ParseAddr(iface.IPv6.Gateway) + if err != nil { + return nil, fmt.Errorf("failed to parse gateway ip: %w", err) + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: "eth0", + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet6, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + } + + if iface.AnchorIPv4 != nil { + ifAddr, err := utils.IPPrefixFrom(iface.AnchorIPv4.IPAddress, iface.AnchorIPv4.Netmask) + if err != nil { + return nil, fmt.Errorf("failed to parse ip address: %w", err) + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: "eth0", + Address: ifAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: nethelpers.FamilyInet4, + }, + ) + } + } + + for idx, iface := range metadata.Interfaces["private"] { + ifName := fmt.Sprintf("eth%d", idx+1) + + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: ifName, + Up: true, + ConfigLayer: network.ConfigPlatform, + }) + + if iface.IPv4 != nil { + ifAddr, err := utils.IPPrefixFrom(iface.IPv4.IPAddress, iface.IPv4.Netmask) + if err != nil { + return nil, fmt.Errorf("failed to parse ip address: %w", err) + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: ifName, + Address: ifAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: nethelpers.FamilyInet4, + }, + ) + } + } + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: d.Name(), + Hostname: metadata.Hostname, + Region: metadata.Region, + InstanceID: strconv.Itoa(metadata.DropletID), + ProviderID: fmt.Sprintf("digitalocean://%d", metadata.DropletID), + } + + return networkConfig, nil +} + // Configuration implements the platform.Platform interface. func (d *DigitalOcean) Configuration(ctx context.Context, r state.State) ([]byte, error) { log.Printf("fetching machine config from: %q", DigitalOceanUserDataEndpoint) @@ -59,43 +248,19 @@ func (d *DigitalOcean) KernelArgs() procfs.Parameters { } // NetworkConfiguration implements the runtime.Platform interface. -// -//nolint:gocyclo func (d *DigitalOcean) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { - host, err := download.Download(ctx, DigitalOceanHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + log.Printf("fetching DigitalOcean instance config from: %q ", DigitalOceanMetadataEndpoint) + + metadata, err := d.getMetadata(ctx) + if err != nil { return err } - extIP, err := download.Download(ctx, DigitalOceanExternalIPEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { + networkConfig, err := d.ParseMetadata(metadata) + if err != nil { return err } - networkConfig := &runtime.PlatformNetworkConfig{} - - if len(host) > 0 { - hostnameSpec := network.HostnameSpecSpec{ - ConfigLayer: network.ConfigPlatform, - } - - if err := hostnameSpec.ParseFQDN(string(host)); err != nil { - return err - } - - networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) - } - - if len(extIP) > 0 { - if ip, err := netip.ParseAddr(string(extIP)); err == nil { - networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) - } - } - select { case ch <- networkConfig: case <-ctx.Done(): diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean_test.go index b0e5d9094..5f26c06db 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean_test.go @@ -4,11 +4,36 @@ package digitalocean_test -import "testing" +import ( + _ "embed" + "encoding/json" + "testing" -func TestEmpty(t *testing.T) { - // added for accurate coverage estimation - // - // please remove it once any unit-test is added - // for this package + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean" +) + +//go:embed testdata/metadata.json +var rawMetadata []byte + +//go:embed testdata/expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { + p := &digitalocean.DigitalOcean{} + + var metadata digitalocean.MetadataConfig + + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) + + networkConfig, err := p.ParseMetadata(&metadata) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/metadata.go new file mode 100644 index 000000000..886697094 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/metadata.go @@ -0,0 +1,81 @@ +// 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 digitalocean + +import ( + "context" + "encoding/json" + stderrors "errors" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // DigitalOceanExternalIPEndpoint displays all external addresses associated with the instance. + DigitalOceanExternalIPEndpoint = "http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address" + // DigitalOceanMetadataEndpoint is the local endpoint for the platform metadata. + DigitalOceanMetadataEndpoint = "http://169.254.169.254/metadata/v1.json" + // DigitalOceanUserDataEndpoint is the local endpoint for the config. + DigitalOceanUserDataEndpoint = "http://169.254.169.254/metadata/v1/user-data" +) + +// MetadataConfig represents a metadata Digital Ocean instance. +type MetadataConfig struct { + Hostname string `json:"hostname,omitempty"` + DropletID int `json:"droplet_id,omitempty"` + Region string `json:"region,omitempty"` + PublicIPv4 string `json:"public-ipv4,omitempty"` + Tags []string `json:"tags,omitempty"` + + DNS struct { + Nameservers []string `json:"nameservers,omitempty"` + } `json:"dns,omitempty"` + Interfaces map[string][]struct { + MACAddress string `json:"mac,omitempty"` + Type string `json:"type,omitempty"` + + IPv4 *struct { + IPAddress string `json:"ip_address,omitempty"` + Netmask string `json:"netmask,omitempty"` + Gateway string `json:"gateway,omitempty"` + } `json:"ipv4,omitempty"` + IPv6 *struct { + IPAddress string `json:"ip_address,omitempty"` + CIDR int `json:"cidr,omitempty"` + Gateway string `json:"gateway,omitempty"` + } `json:"ipv6,omitempty"` + AnchorIPv4 *struct { + IPAddress string `json:"ip_address,omitempty"` + Netmask string `json:"netmask,omitempty"` + Gateway string `json:"gateway,omitempty"` + } `json:"anchor_ipv4,omitempty"` + } `json:"interfaces,omitempty"` +} + +func (d *DigitalOcean) getMetadata(ctx context.Context) (*MetadataConfig, error) { + metaConfigDl, err := download.Download(ctx, DigitalOceanMetadataEndpoint, + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return nil, err + } + + var metadata MetadataConfig + if err = json.Unmarshal(metaConfigDl, &metadata); err != nil { + return nil, err + } + + extIP, err := download.Download(ctx, DigitalOceanExternalIPEndpoint, + download.WithErrorOnNotFound(errors.ErrNoExternalIPs), + download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) + if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { + return nil, err + } + + metadata.PublicIPv4 = string(extIP) + + return &metadata, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/expected.yaml new file mode 100644 index 000000000..cb75870cd --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/expected.yaml @@ -0,0 +1,96 @@ +addresses: +- address: 128.199.52.32/19 + linkName: eth0 + family: inet4 + scope: global + flags: permanent + layer: platform +- address: 2a03:b0c0:2:d0::1478:3001/64 + linkName: eth0 + family: inet6 + scope: global + flags: permanent + layer: platform +- address: 10.18.0.5/16 + linkName: eth0 + family: inet4 + scope: global + flags: permanent + layer: platform +- address: 10.133.0.2/16 + linkName: eth1 + family: inet4 + scope: global + flags: permanent + layer: platform +links: +- name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +- name: eth1 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +routes: +- family: inet4 + dst: "" + src: "" + gateway: 128.199.32.1 + outLinkName: eth0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +- family: inet4 + dst: 169.254.169.254/32 + src: "" + gateway: 128.199.32.1 + outLinkName: eth0 + table: main + priority: 512 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +- family: inet6 + dst: "" + src: "" + gateway: 2a03:b0c0:2:d0::1 + outLinkName: eth0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: +- hostname: debian-s-1vcpu-512mb-10gb-ams3-01 + domainname: "" + layer: platform +resolvers: +- dnsServers: + - 67.207.67.2 + - 67.207.67.3 + layer: platform +timeServers: [] +operators: [] +externalIPs: +- 128.199.52.32 +metadata: + platform: digital-ocean + hostname: debian-s-1vcpu-512mb-10gb-ams3-01 + region: ams3 + instanceId: "320206672" + providerId: digitalocean://320206672 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/metadata.json new file mode 100644 index 000000000..3667ab958 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/testdata/metadata.json @@ -0,0 +1,70 @@ +{ + "droplet_id": 320206672, + "hostname": "debian-s-1vcpu-512mb-10gb-ams3-01", + "user_data": "", + "vendor_data": "", + "public_keys": [], + "auth_key": "490eac0a2fc04503267ef85064407f2f", + "region": "ams3", + "interfaces": { + "private": [ + { + "ipv4": { + "ip_address": "10.133.0.2", + "netmask": "255.255.0.0", + "gateway": "10.133.0.1" + }, + "mac": "2a:3c:79:3d:f3:b7", + "type": "private" + } + ], + "public": [ + { + "ipv4": { + "ip_address": "128.199.52.32", + "netmask": "255.255.224.0", + "gateway": "128.199.32.1" + }, + "ipv6": { + "ip_address": "2A03:B0C0:0002:00D0:0000:0000:1478:3001", + "cidr": 64, + "gateway": "2a03:b0c0:2:d0::1" + }, + "anchor_ipv4": { + "ip_address": "10.18.0.5", + "netmask": "255.255.0.0", + "gateway": "10.18.0.1" + }, + "mac": "12:2f:49:0c:eb:c0", + "type": "public" + } + ] + }, + "floating_ip": { + "ipv4": { + "active": false + } + }, + "reserved_ip": { + "ipv4": { + "active": false + } + }, + "dns": { + "nameservers": [ + "67.207.67.2", + "67.207.67.3" + ] + }, + "tags": [ + "label123" + ], + "features": { + "dhcp_enabled": false + }, + "modify_index": 113261986, + "dotty_status": "running", + "ssh_info": { + "port": 22 + } +} \ No newline at end of file diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix.go index f98ce5133..3a2a6541b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix.go @@ -30,6 +30,7 @@ import ( "github.com/talos-systems/talos/pkg/machinery/constants" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Event holds data to pass to the Equinix Metal event URL. @@ -38,13 +39,6 @@ type Event struct { Message string `json:"msg"` } -// Metadata holds equinixmetal metadata info. -type Metadata struct { - Hostname string `json:"hostname"` - Network Network `json:"network"` - PrivateSubnets []string `json:"private_subnets"` -} - // Network holds network info from the equinixmetal metadata. type Network struct { Bonding Bonding `json:"bonding"` @@ -116,7 +110,7 @@ func (p *EquinixMetal) KernelArgs() procfs.Parameters { // ParseMetadata converts Equinix Metal metadata into Talos network configuration. // //nolint:gocyclo,cyclop -func (p *EquinixMetal) ParseMetadata(ctx context.Context, equinixMetadata *Metadata, st state.State) (*runtime.PlatformNetworkConfig, error) { +func (p *EquinixMetal) ParseMetadata(ctx context.Context, equinixMetadata *MetadataConfig, st state.State) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} // 1. Links @@ -325,6 +319,18 @@ func (p *EquinixMetal) ParseMetadata(ctx context.Context, equinixMetadata *Metad networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } + // 5. platform metadata + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: p.Name(), + Hostname: equinixMetadata.Hostname, + Region: equinixMetadata.Metro, + Zone: equinixMetadata.Facility, + InstanceType: equinixMetadata.Plan, + InstanceID: equinixMetadata.ID, + ProviderID: fmt.Sprintf("equinixmetal://%s", equinixMetadata.ID), + } + return networkConfig, nil } @@ -337,12 +343,12 @@ func (p *EquinixMetal) NetworkConfiguration(ctx context.Context, st state.State, return err } - var equinixMetadata Metadata - if err = json.Unmarshal(metadataConfig, &equinixMetadata); err != nil { + var meta MetadataConfig + if err = json.Unmarshal(metadataConfig, &meta); err != nil { return err } - networkConfig, err := p.ParseMetadata(ctx, &equinixMetadata, st) + networkConfig, err := p.ParseMetadata(ctx, &meta, st) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix_test.go index 73040cf3a..a0b46d555 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/equinix_test.go @@ -31,7 +31,7 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { p := &equinixmetal.EquinixMetal{} - var m equinixmetal.Metadata + var m equinixmetal.MetadataConfig require.NoError(t, json.Unmarshal(rawMetadata, &m)) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/metadata.go new file mode 100644 index 000000000..60dd8b147 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/metadata.go @@ -0,0 +1,16 @@ +// 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 equinixmetal + +// MetadataConfig holds equinixmetal metadata info. +type MetadataConfig struct { + ID string `json:"id"` + Hostname string `json:"hostname"` + Plan string `json:"plan"` + Metro string `json:"metro"` + Facility string `json:"facility"` + Network Network `json:"network"` + PrivateSubnets []string `json:"private_subnets"` +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/testdata/expected.yaml index 68a8f4ef7..4607b41e8 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/equinixmetal/testdata/expected.yaml @@ -103,3 +103,11 @@ resolvers: [] timeServers: [] operators: [] externalIPs: [] +metadata: + platform: equinixMetal + hostname: infra-green-ci + region: ny + zone: ny5 + instanceType: c3.medium.x86 + instanceId: X + providerId: equinixmetal://X diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/errors/errors.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/errors/errors.go index b7a9c968e..2c726a086 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/errors/errors.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/errors/errors.go @@ -18,3 +18,6 @@ var ErrNoExternalIPs = errors.New("failed to fetch external addresses from metad // ErrNoEventURL indicates that the platform does not have an expected events URL in the kernel params. var ErrNoEventURL = errors.New("no event URL") + +// ErrMetadataNotReady indicates that the platform does not have metadata yet. +var ErrMetadataNotReady = errors.New("platform metadata is not ready") diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go index d8566d562..b3f6c2381 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go @@ -7,6 +7,8 @@ package gcp import ( "context" + "fmt" + "log" "net/netip" "strings" @@ -17,6 +19,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // GCP is the concrete type that implements the platform.Platform interface. @@ -27,6 +30,62 @@ func (g *GCP) Name() string { return "gcp" } +// ParseMetadata converts GCP platform metadata into platform network config. +func (g *GCP) ParseMetadata(metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} + + if metadata.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + if metadata.PublicIPv4 != "" { + ip, err := netip.ParseAddr(metadata.PublicIPv4) + if err != nil { + return nil, err + } + + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) + } + + dns, _ := netip.ParseAddr(gcpResolverServer) //nolint:errcheck + + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + DNSServers: []netip.Addr{dns}, + ConfigLayer: network.ConfigPlatform, + }) + + networkConfig.TimeServers = append(networkConfig.TimeServers, network.TimeServerSpecSpec{ + NTPServers: []string{gcpTimeServer}, + ConfigLayer: network.ConfigPlatform, + }) + + region := metadata.Zone + + if idx := strings.LastIndex(region, "-"); idx != -1 { + region = region[:idx] + } + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: g.Name(), + Hostname: metadata.Hostname, + Region: region, + Zone: metadata.Zone, + InstanceType: metadata.InstanceType, + InstanceID: metadata.InstanceID, + ProviderID: fmt.Sprintf("gce://%s/%s/%s", metadata.ProjectID, metadata.Zone, metadata.Name), + } + + return networkConfig, nil +} + // Configuration implements the platform.Platform interface. func (g *GCP) Configuration(ctx context.Context, r state.State) ([]byte, error) { userdata, err := metadata.InstanceAttributeValue("user-data") @@ -59,41 +118,18 @@ func (g *GCP) KernelArgs() procfs.Parameters { // NetworkConfiguration implements the runtime.Platform interface. func (g *GCP) NetworkConfiguration(ctx context.Context, st state.State, ch chan<- *runtime.PlatformNetworkConfig) error { - networkConfig := &runtime.PlatformNetworkConfig{} + log.Printf("fetching gcp instance config") - hostname, err := metadata.Hostname() + metadata, err := g.getMetadata(ctx) + if err != nil { + return fmt.Errorf("failed to receive GCP metadata: %w", err) + } + + networkConfig, err := g.ParseMetadata(metadata) if err != nil { return err } - if hostname != "" { - hostnameSpec := network.HostnameSpecSpec{ - ConfigLayer: network.ConfigPlatform, - } - - if err = hostnameSpec.ParseFQDN(hostname); err != nil { - return err - } - - networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) - } - - externalIP, err := metadata.ExternalIP() - if err != nil { - if _, ok := err.(metadata.NotDefinedError); !ok { - return err - } - } - - if externalIP != "" { - ip, err := netip.ParseAddr(externalIP) - if err != nil { - return err - } - - networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) - } - select { case ch <- networkConfig: case <-ctx.Done(): diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp_test.go index 661dde2d6..b7d47138a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp_test.go @@ -4,11 +4,36 @@ package gcp_test -import "testing" +import ( + _ "embed" + "encoding/json" + "testing" -func TestEmpty(t *testing.T) { - // added for accurate coverage estimation - // - // please remove it once any unit-test is added - // for this package + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp" +) + +//go:embed testdata/metadata.json +var rawMetadata []byte + +//go:embed testdata/expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { + p := &gcp.GCP{} + + var metadata gcp.MetadataConfig + + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) + + networkConfig, err := p.ParseMetadata(&metadata) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/metadata.go new file mode 100644 index 000000000..46de9afa5 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/metadata.go @@ -0,0 +1,70 @@ +// 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 gcp + +import ( + "context" + "strings" + + "cloud.google.com/go/compute/metadata" +) + +const ( + gcpResolverServer = "169.254.169.254" + gcpTimeServer = "metadata.google.internal" +) + +// MetadataConfig holds meta info. +type MetadataConfig struct { + ProjectID string `json:"project-id"` + Name string `json:"name,omitempty"` + Hostname string `json:"hostname,omitempty"` + Zone string `json:"zone,omitempty"` + InstanceType string `json:"machine-type"` + InstanceID string `json:"id"` + PublicIPv4 string `json:"external-ip"` +} + +func (g *GCP) getMetadata(context.Context) (*MetadataConfig, error) { + var ( + meta MetadataConfig + err error + ) + + if meta.ProjectID, err = metadata.ProjectID(); err != nil { + return nil, err + } + + if meta.Name, err = metadata.InstanceName(); err != nil { + return nil, err + } + + instanceType, err := metadata.Get("instance/machine-type") + if err != nil { + return nil, err + } + + meta.InstanceType = strings.TrimSpace(instanceType[strings.LastIndex(instanceType, "/")+1:]) + + if meta.InstanceID, err = metadata.InstanceID(); err != nil { + return nil, err + } + + if meta.Hostname, err = metadata.Hostname(); err != nil { + return nil, err + } + + if meta.Zone, err = metadata.Zone(); err != nil { + return nil, err + } + + if meta.PublicIPv4, err = metadata.ExternalIP(); err != nil { + if _, ok := err.(metadata.NotDefinedError); !ok { + return nil, err + } + } + + return &meta, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/expected.yaml new file mode 100644 index 000000000..105e77f3a --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/expected.yaml @@ -0,0 +1,25 @@ +addresses: [] +links: [] +routes: [] +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: + - dnsServers: + - 169.254.169.254 + layer: platform +timeServers: + - timeServers: + - metadata.google.internal + layer: platform +operators: [] +externalIPs: [] +metadata: + platform: gcp + hostname: talos + region: us-central1 + zone: us-central1-a + instanceType: n1-standard-1 + instanceId: "0" + providerId: gce://123/us-central1-a/my-server diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/metadata.json new file mode 100644 index 000000000..b579ce73d --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/testdata/metadata.json @@ -0,0 +1,8 @@ +{ + "project-id": "123", + "hostname": "talos", + "id": "0", + "zone": "us-central1-a", + "name": "my-server", + "machine-type": "n1-standard-1" +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go index 230a6e383..80c740d43 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go @@ -7,7 +7,6 @@ package hcloud import ( "context" - stderrors "errors" "fmt" "log" "net/netip" @@ -21,40 +20,9 @@ import ( "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) -const ( - // HCloudExternalIPEndpoint is the local hcloud endpoint for the external IP. - HCloudExternalIPEndpoint = "http://169.254.169.254/hetzner/v1/metadata/public-ipv4" - - // HCloudNetworkEndpoint is the local hcloud endpoint for the network-config. - HCloudNetworkEndpoint = "http://169.254.169.254/hetzner/v1/metadata/network-config" - - // HCloudHostnameEndpoint is the local hcloud endpoint for the hostname. - HCloudHostnameEndpoint = "http://169.254.169.254/hetzner/v1/metadata/hostname" - - // HCloudUserDataEndpoint is the local hcloud endpoint for the config. - HCloudUserDataEndpoint = "http://169.254.169.254/hetzner/v1/userdata" -) - -// NetworkConfig holds hcloud network-config info. -type NetworkConfig struct { - Version int `yaml:"version"` - Config []struct { - Mac string `yaml:"mac_address"` - Interfaces string `yaml:"name"` - Subnets []struct { - NameServers []string `yaml:"dns_nameservers,omitempty"` - Address string `yaml:"address,omitempty"` - Gateway string `yaml:"gateway,omitempty"` - Ipv4 bool `yaml:"ipv4,omitempty"` - Ipv6 bool `yaml:"ipv6,omitempty"` - Type string `yaml:"type"` - } `yaml:"subnets"` - Type string `yaml:"type"` - } `yaml:"config"` -} - // Hcloud is the concrete type that implements the runtime.Platform interface. type Hcloud struct{} @@ -66,23 +34,23 @@ func (h *Hcloud) Name() string { // ParseMetadata converts HCloud metadata to platform network configuration. // //nolint:gocyclo -func (h *Hcloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, host, extIP []byte) (*runtime.PlatformNetworkConfig, error) { +func (h *Hcloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} - if len(host) > 0 { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(string(host)); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if len(extIP) > 0 { - if ip, err := netip.ParseAddr(string(extIP)); err == nil { + if metadata.PublicIPv4 != "" { + if ip, err := netip.ParseAddr(metadata.PublicIPv4); err == nil { networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) } } @@ -156,6 +124,13 @@ func (h *Hcloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, host, e } } + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: h.Name(), + Hostname: metadata.Hostname, + InstanceID: metadata.InstanceID, + ProviderID: fmt.Sprintf("hcloud://%s", metadata.InstanceID), + } + return networkConfig, nil } @@ -181,9 +156,12 @@ func (h *Hcloud) KernelArgs() procfs.Parameters { } // NetworkConfiguration implements the runtime.Platform interface. -// -//nolint:gocyclo func (h *Hcloud) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { + metadata, err := h.getMetadata(ctx) + if err != nil { + return err + } + log.Printf("fetching hcloud network config from: %q", HCloudNetworkEndpoint) metadataNetworkConfig, err := download.Download(ctx, HCloudNetworkEndpoint) @@ -201,25 +179,7 @@ func (h *Hcloud) NetworkConfiguration(ctx context.Context, _ state.State, ch cha return fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version) } - log.Printf("fetching hostname from: %q", HCloudHostnameEndpoint) - - host, err := download.Download(ctx, HCloudHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { - return err - } - - log.Printf("fetching externalIP from: %q", HCloudExternalIPEndpoint) - - extIP, err := download.Download(ctx, HCloudExternalIPEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { - return err - } - - networkConfig, err := h.ParseMetadata(&unmarshalledNetworkConfig, host, extIP) + networkConfig, err := h.ParseMetadata(&unmarshalledNetworkConfig, metadata) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go index e13a90075..741f8f135 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go @@ -6,7 +6,6 @@ package hcloud_test import ( _ "embed" - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -25,17 +24,21 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { h := &hcloud.Hcloud{} + metadata := &hcloud.MetadataConfig{ + Hostname: "talos.fqdn", + PublicIPv4: "1.2.3.4", + InstanceID: "0", + } + var m hcloud.NetworkConfig require.NoError(t, yaml.Unmarshal(rawMetadata, &m)) - networkConfig, err := h.ParseMetadata(&m, []byte("some.fqdn"), []byte("1.2.3.4")) + networkConfig, err := h.ParseMetadata(&m, metadata) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) require.NoError(t, err) - fmt.Print(string(marshaled)) - assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/metadata.go new file mode 100644 index 000000000..fe8d3ce79 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/metadata.go @@ -0,0 +1,99 @@ +// 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 hcloud + +import ( + "context" + stderrors "errors" + "log" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // HCloudExternalIPEndpoint is the local hcloud endpoint for the external IP. + HCloudExternalIPEndpoint = "http://169.254.169.254/hetzner/v1/metadata/public-ipv4" + + // HCloudNetworkEndpoint is the local hcloud endpoint for the network-config. + HCloudNetworkEndpoint = "http://169.254.169.254/hetzner/v1/metadata/network-config" + + // HCloudHostnameEndpoint is the local hcloud endpoint for the hostname. + HCloudHostnameEndpoint = "http://169.254.169.254/hetzner/v1/metadata/hostname" + + // HCloudInstanceIDEndpoint is the local hcloud endpoint for the instance-id. + HCloudInstanceIDEndpoint = "http://169.254.169.254/hetzner/v1/metadata/instance-id" + + // HCloudRegionEndpoint is the local hcloud endpoint for the region. + HCloudRegionEndpoint = "http://169.254.169.254/hetzner/v1/metadata/region" + + // HCloudZoneEndpoint is the local hcloud endpoint for the zone. + HCloudZoneEndpoint = "http://169.254.169.254/hetzner/v1/metadata/availability-zone" + + // HCloudUserDataEndpoint is the local hcloud endpoint for the config. + HCloudUserDataEndpoint = "http://169.254.169.254/hetzner/v1/userdata" +) + +// MetadataConfig holds meta info. +type MetadataConfig struct { + Hostname string `yaml:"hostname,omitempty"` + Region string `yaml:"region,omitempty"` + AvailabilityZone string `json:"availability-zone,omitempty"` + InstanceID string `yaml:"instance-id,omitempty"` + PublicIPv4 string `yaml:"public-ipv4,omitempty"` +} + +// NetworkConfig holds hcloud network-config info. +type NetworkConfig struct { + Version int `yaml:"version"` + Config []struct { + Mac string `yaml:"mac_address"` + Interfaces string `yaml:"name"` + Subnets []struct { + NameServers []string `yaml:"dns_nameservers,omitempty"` + Address string `yaml:"address,omitempty"` + Gateway string `yaml:"gateway,omitempty"` + Ipv4 bool `yaml:"ipv4,omitempty"` + Ipv6 bool `yaml:"ipv6,omitempty"` + Type string `yaml:"type"` + } `yaml:"subnets"` + Type string `yaml:"type"` + } `yaml:"config"` +} + +func (h *Hcloud) getMetadata(ctx context.Context) (*MetadataConfig, error) { + log.Printf("fetching hostname from: %q", HCloudHostnameEndpoint) + + host, err := download.Download(ctx, HCloudHostnameEndpoint, + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return nil, err + } + + log.Printf("fetching instance-id from: %q", HCloudInstanceIDEndpoint) + + instanceID, err := download.Download(ctx, HCloudInstanceIDEndpoint, + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return nil, err + } + + log.Printf("fetching externalIP from: %q", HCloudExternalIPEndpoint) + + extIP, err := download.Download(ctx, HCloudExternalIPEndpoint, + download.WithErrorOnNotFound(errors.ErrNoExternalIPs), + download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) + if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { + return nil, err + } + + return &MetadataConfig{ + Hostname: string(host), + InstanceID: string(instanceID), + PublicIPv4: string(extIP), + }, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml index b6ae357c3..7fa86026e 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml @@ -26,7 +26,7 @@ routes: protocol: static layer: platform hostnames: - - hostname: some + - hostname: talos domainname: fqdn layer: platform resolvers: [] @@ -40,3 +40,8 @@ operators: layer: platform externalIPs: - 1.2.3.4 +metadata: + platform: hcloud + hostname: talos.fqdn + instanceId: "0" + providerId: hcloud://0 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/match.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/match.go new file mode 100644 index 000000000..2bb028347 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/match.go @@ -0,0 +1,59 @@ +// 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 metal + +import "regexp" + +func keyToVar(key string) string { + return `${` + key + `}` +} + +type matcher struct { + Key string + Regexp *regexp.Regexp +} + +func newMatcher(key string) *matcher { + return &matcher{ + Key: keyToVar(key), + Regexp: regexp.MustCompile(`(?i)` + regexp.QuoteMeta(keyToVar(key))), + } +} + +type replacer struct { + original string + Regexp *regexp.Regexp + Matches [][]int +} + +func (m *matcher) process(original string) *replacer { + var r replacer + r.Regexp = m.Regexp + r.original = original + + r.Matches = m.Regexp.FindAllStringIndex(original, -1) + + return &r +} + +func (r *replacer) ReplaceMatches(replacement string) string { + var res string + + if len(r.Matches) < 1 { + return res + } + + res += r.original[:r.Matches[0][0]] + res += replacement + + for i := 0; i < len(r.Matches)-1; i++ { + res += r.original[r.Matches[i][1]:r.Matches[i+1][0]] + res += replacement + } + + res += r.original[r.Matches[len(r.Matches)-1][1]:] + + return res +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go index 8b4d87c4d..e6b7ec56a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go @@ -9,15 +9,9 @@ import ( "context" "fmt" "log" - "net/url" "os" "path/filepath" - "regexp" - "strings" - "time" - "github.com/cosi-project/runtime/pkg/resource" - "github.com/cosi-project/runtime/pkg/safe" "github.com/cosi-project/runtime/pkg/state" "github.com/siderolabs/go-blockdevice/blockdevice/filesystem" "github.com/siderolabs/go-blockdevice/blockdevice/probe" @@ -28,8 +22,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/constants" - hardwareResource "github.com/talos-systems/talos/pkg/machinery/resources/hardware" - "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) const ( @@ -74,221 +67,6 @@ func (m *Metal) Configuration(ctx context.Context, r state.State) ([]byte, error } } -func keyToVar(key string) string { - return `${` + key + `}` -} - -type matcher struct { - Key string - Regexp *regexp.Regexp -} - -func newMatcher(key string) *matcher { - return &matcher{ - Key: keyToVar(key), - Regexp: regexp.MustCompile(`(?i)` + regexp.QuoteMeta(keyToVar(key))), - } -} - -type replacer struct { - original string - Regexp *regexp.Regexp - Matches [][]int -} - -func (m *matcher) process(original string) *replacer { - var r replacer - r.Regexp = m.Regexp - r.original = original - - r.Matches = m.Regexp.FindAllStringIndex(original, -1) - - return &r -} - -func (r *replacer) ReplaceMatches(replacement string) string { - var res string - - if len(r.Matches) < 1 { - return res - } - - res += r.original[:r.Matches[0][0]] - res += replacement - - for i := 0; i < len(r.Matches)-1; i++ { - res += r.original[r.Matches[i][1]:r.Matches[i+1][0]] - res += replacement - } - - res += r.original[r.Matches[len(r.Matches)-1][1]:] - - return res -} - -// PopulateURLParameters fills in empty parameters in the download URL. -// -//nolint:gocyclo -func PopulateURLParameters(ctx context.Context, downloadURL string, r state.State) (string, error) { - populatedURL := downloadURL - - genErr := func(varOfKey string, errToWrap error) error { - return fmt.Errorf("error while substituting %s: %w", varOfKey, errToWrap) - } - - u, err := url.Parse(populatedURL) - if err != nil { - return "", fmt.Errorf("failed to parse %s: %w", populatedURL, err) - } - - values := u.Query() - - substitute := func(varToSubstitute string, getFunc func(ctx context.Context, r state.State) (string, error)) error { - m := newMatcher(varToSubstitute) - - for qKey, qValues := range values { - if len(qValues) == 0 { - continue - } - - qVal := qValues[0] - - // backwards compatible behavior for the uuid key - if qKey == constants.UUIDKey && !(len(qValues) == 1 && len(strings.TrimSpace(qVal)) > 0) { - uid, err := getSystemUUID(ctx, r) - if err != nil { - return fmt.Errorf("error while substituting UUID: %w", err) - } - - values.Set(constants.UUIDKey, uid) - - continue - } - - replacer := m.process(qVal) - - if len(replacer.Matches) < 1 { - continue - } - - val, err := getFunc(ctx, r) - if err != nil { - return genErr(m.Key, err) - } - - qVal = replacer.ReplaceMatches(val) - - values.Set(qKey, qVal) - } - - return nil - } - - if err := substitute(constants.UUIDKey, getSystemUUID); err != nil { - return "", err - } - - if err := substitute(constants.SerialNumberKey, getSystemSerialNumber); err != nil { - return "", err - } - - if err := substitute(constants.MacKey, getMACAddress); err != nil { - return "", err - } - - if err := substitute(constants.HostnameKey, getHostname); err != nil { - return "", err - } - - u.RawQuery = values.Encode() - - return u.String(), nil -} - -func getResource[T resource.Resource](ctx context.Context, r state.State, namespace, typ, valName string, isReadyFunc func(T) bool, checkAndGetFunc func(T) string) (string, error) { - metadata := resource.NewMetadata(namespace, typ, "", resource.VersionUndefined) - - watchCtx, cancel := context.WithTimeout(ctx, 1*time.Minute) - defer cancel() - - events := make(chan safe.WrappedStateEvent[T]) - - err := safe.StateWatchKind[T](watchCtx, r, metadata, events, state.WithBootstrapContents(true)) - if err != nil { - return "", fmt.Errorf("failed to watch %s resources: %w", typ, err) - } - - var watchErr error - - for { - select { - case <-watchCtx.Done(): - err := fmt.Errorf("failed to determine %s of %s: %w", valName, typ, watchCtx.Err()) - err = fmt.Errorf("%s; %w", err.Error(), watchErr) - - return "", err - case event := <-events: - eventResource, err := event.Resource() - if err != nil { - watchErr = fmt.Errorf("%s; invalid resource in wrapped event: %w", watchErr.Error(), err) - } - - if !isReadyFunc(eventResource) { - continue - } - - val := checkAndGetFunc(eventResource) - if val == "" { - return "", fmt.Errorf("%s property of resource %s is empty", valName, typ) - } - - return val, nil - } - } -} - -func getUUIDProperty(r *hardwareResource.SystemInformation) string { - return r.TypedSpec().UUID -} - -func getSerialNumberProperty(r *hardwareResource.SystemInformation) string { - return r.TypedSpec().SerialNumber -} - -func getSystemUUID(ctx context.Context, r state.State) (string, error) { - return getResource(ctx, r, hardwareResource.NamespaceName, hardwareResource.SystemInformationType, "UUID", func(*hardwareResource.SystemInformation) bool { return true }, getUUIDProperty) -} - -func getSystemSerialNumber(ctx context.Context, r state.State) (string, error) { - return getResource(ctx, - r, - hardwareResource.NamespaceName, - hardwareResource.SystemInformationType, - "Serial Number", - func(*hardwareResource.SystemInformation) bool { return true }, - getSerialNumberProperty) -} - -func getMACAddressProperty(r *network.LinkStatus) string { - return r.TypedSpec().HardwareAddr.String() -} - -func checkLinkUp(r *network.LinkStatus) bool { - return r.TypedSpec().LinkState -} - -func getMACAddress(ctx context.Context, r state.State) (string, error) { - return getResource(ctx, r, network.NamespaceName, network.LinkStatusType, "MAC Address", checkLinkUp, getMACAddressProperty) -} - -func getHostnameProperty(r *network.HostnameSpec) string { - return r.TypedSpec().Hostname -} - -func getHostname(ctx context.Context, r state.State) (string, error) { - return getResource(ctx, r, network.NamespaceName, network.HostnameSpecType, "Hostname", func(*network.HostnameSpec) bool { return true }, getHostnameProperty) -} - // Mode implements the platform.Platform interface. func (m *Metal) Mode() runtime.Mode { return runtime.ModeMetal @@ -336,6 +114,28 @@ func (m *Metal) KernelArgs() procfs.Parameters { } // NetworkConfiguration implements the runtime.Platform interface. -func (m *Metal) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { +func (m *Metal) NetworkConfiguration(ctx context.Context, st state.State, ch chan<- *runtime.PlatformNetworkConfig) error { + var metadata runtimeres.PlatformMetadataSpec + + metadata.Platform = m.Name() + + if option := procfs.ProcCmdline().Get(constants.KernelParamHostname).First(); option != nil { + metadata.Hostname = *option + } + + if uuid, err := getSystemUUID(ctx, st); err == nil { + metadata.InstanceID = uuid + } + + networkConfig := &runtime.PlatformNetworkConfig{ + Metadata: &metadata, + } + + select { + case <-ctx.Done(): + return ctx.Err() + case ch <- networkConfig: + } + return nil } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal_test.go index b7c7ce6c1..48cd29f11 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal_test.go @@ -6,243 +6,42 @@ package metal_test import ( "context" - "fmt" - "net" - "net/http" - "net/http/httptest" - "net/url" "testing" "time" - "github.com/cosi-project/runtime/pkg/resource" "github.com/cosi-project/runtime/pkg/state" "github.com/cosi-project/runtime/pkg/state/impl/inmem" "github.com/cosi-project/runtime/pkg/state/impl/namespaced" "github.com/stretchr/testify/assert" - "github.com/talos-systems/go-procfs/procfs" + "github.com/stretchr/testify/require" + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal" - "github.com/talos-systems/talos/pkg/machinery/constants" - "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/hardware" - "github.com/talos-systems/talos/pkg/machinery/resources/network" ) -func createOrUpdate(ctx context.Context, st state.State, r resource.Resource) error { - oldRes, err := st.Get(ctx, r.Metadata()) - if err != nil && !state.IsNotFoundError(err) { - return err - } +func TestNetworkConfig(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) - if oldRes == nil { - err = st.Create(ctx, r) - if err != nil { - return err - } - } else { - r.Metadata().SetVersion(oldRes.Metadata().Version()) + p := &metal.Metal{} - err = st.Update(ctx, r) - if err != nil { - return err - } - } + ch := make(chan *runtime.PlatformNetworkConfig, 1) - return nil -} - -func setup(ctx context.Context, t *testing.T, st state.State, mockUUID, mockSerialNumber, mockHostname, mockMAC string) { - testID := "testID" - sysInfo := hardware.NewSystemInformation(testID) - sysInfo.TypedSpec().UUID = mockUUID - sysInfo.TypedSpec().SerialNumber = mockSerialNumber - assert.NoError(t, createOrUpdate(ctx, st, sysInfo)) - - hostnameSpec := network.NewHostnameSpec(network.NamespaceName, testID) - hostnameSpec.TypedSpec().Hostname = mockHostname - assert.NoError(t, createOrUpdate(ctx, st, hostnameSpec)) - - linkStatusSpec := network.NewLinkStatus(network.NamespaceName, testID) - parsedMockMAC, err := net.ParseMAC(mockMAC) - assert.NoError(t, err) - - linkStatusSpec.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(parsedMockMAC) - linkStatusSpec.TypedSpec().LinkState = true - assert.NoError(t, createOrUpdate(ctx, st, linkStatusSpec)) -} - -func TestPopulateURLParameters(t *testing.T) { - mockUUID := "40dcbd19-3b10-444e-bfff-aaee44a51fda" - - mockMAC := "52:2f:fd:df:fc:c0" - - mockSerialNumber := "0OCZJ19N65" - - mockHostname := "myTestHostname" - - for _, tt := range []struct { - name string - url string - expectedURL string - expectedError string - }{ - { - name: "no uuid", - url: "http://example.com/metadata", - expectedURL: "http://example.com/metadata", - }, - { - name: "empty uuid", - url: "http://example.com/metadata?uuid=", - expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID), - }, - { - name: "uuid present", - url: "http://example.com/metadata?uuid=xyz", - expectedURL: "http://example.com/metadata?uuid=xyz", - }, - { - name: "multiple uuids in one query parameter", - url: "http://example.com/metadata?u=this-${uuid}-equals-${uuid}-exactly", - expectedURL: fmt.Sprintf("http://example.com/metadata?u=this-%s-equals-%s-exactly", mockUUID, mockUUID), - }, - { - name: "uuid and mac in one query parameter", - url: "http://example.com/metadata?u=this-${uuid}-and-${mac}-together", - expectedURL: fmt.Sprintf("http://example.com/metadata?u=this-%s-and-%s-together", mockUUID, mockMAC), - }, - { - name: "other parameters", - url: "http://example.com/metadata?foo=a", - expectedURL: "http://example.com/metadata?foo=a", - }, - { - name: "multiple uuids", - url: "http://example.com/metadata?uuid=xyz&uuid=foo", - expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID), - }, - { - name: "single serial number", - url: "http://example.com/metadata?serial=${serial}", - expectedURL: fmt.Sprintf("http://example.com/metadata?serial=%s", mockSerialNumber), - }, - { - name: "single MAC", - url: "http://example.com/metadata?mac=${mac}", - expectedURL: fmt.Sprintf("http://example.com/metadata?mac=%s", mockMAC), - }, - { - name: "single hostname", - url: "http://example.com/metadata?host=${hostname}", - expectedURL: fmt.Sprintf("http://example.com/metadata?host=%s", mockHostname), - }, - { - name: "serial number, MAC and hostname", - url: "http://example.com/metadata?h=${hostname}&m=${mac}&s=${serial}", - expectedURL: fmt.Sprintf("http://example.com/metadata?h=%s&m=%s&s=%s", mockHostname, mockMAC, mockSerialNumber), - }, - { - name: "uuid, serial number, MAC and hostname; case-insensitive", - url: "http://example.com/metadata?h=${HOSTname}&m=${mAC}&s=${SERIAL}&u=${uUid}", - expectedURL: fmt.Sprintf("http://example.com/metadata?h=%s&m=%s&s=%s&u=%s", mockHostname, mockMAC, mockSerialNumber, mockUUID), - }, - { - name: "MAC and UUID without variable", - url: "http://example.com/metadata?macaddr=${mac}&uuid=", - expectedURL: fmt.Sprintf("http://example.com/metadata?macaddr=%s&uuid=%s", mockMAC, mockUUID), - }, - { - name: "serial number and UUID without variable, order is not preserved", - url: "http://example.com/metadata?uuid=&ser=${serial}", - expectedURL: fmt.Sprintf("http://example.com/metadata?ser=%s&uuid=%s", mockSerialNumber, mockUUID), - }, - { - name: "UUID variable", - url: "http://example.com/metadata?uuid=${uuid}", - expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID), - }, - { - name: "serial number and UUID with variable, order is not preserved", - url: "http://example.com/metadata?uuid=${uuid}&ser=${serial}", - expectedURL: fmt.Sprintf("http://example.com/metadata?ser=%s&uuid=%s", mockSerialNumber, mockUUID), - }, - } { - tt := tt - - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - - st := state.WrapCore(namespaced.NewState(inmem.Build)) - - setup(ctx, t, st, mockUUID, mockSerialNumber, mockHostname, mockMAC) - - output, err := metal.PopulateURLParameters(ctx, tt.url, st) - - if tt.expectedError != "" { - assert.EqualError(t, err, tt.expectedError) - } else { - u, err := url.Parse(tt.expectedURL) - assert.NoError(t, err) - u.RawQuery = u.Query().Encode() - assert.Equal(t, u.String(), output) - } - }) - } -} - -func TestRepopulateOnRetry(t *testing.T) { st := state.WrapCore(namespaced.NewState(inmem.Build)) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + uuid := hardware.NewSystemInformation("test") + uuid.TypedSpec().UUID = "0123-4567-89ab-cdef" + require.NoError(t, st.Create(ctx, uuid)) - nCalls := 0 + err := p.NetworkConfiguration(ctx, st, ch) + require.NoError(t, err) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch nCalls { - case 0: - assert.Equal(t, "h=myTestHostname&m=52%3A2f%3Afd%3Adf%3Afc%3Ac0&s=0OCZJ19N65&u=40dcbd19-3b10-444e-bfff-aaee44a51fda", r.URL.RawQuery) - w.WriteHeader(http.StatusNotFound) - - // After the first call we change the resources that should be substituted in the next call. - uuid2 := "9fba530f-767d-40f9-9410-bb1fed5d2134" - mac2 := "aa:aa:bb:bb:cc:cc" - serialNumber2 := "111AAA9N65" - hostname2 := "anotherHostname" - - setup(ctx, t, st, uuid2, serialNumber2, hostname2, mac2) - case 1: - // Before the second call Configuration() should have resubstituted all the new parameters in the URL. - assert.Equal(t, "h=anotherHostname&m=aa%3Aaa%3Abb%3Abb%3Acc%3Acc&s=111AAA9N65&u=9fba530f-767d-40f9-9410-bb1fed5d2134", r.URL.RawQuery) - w.WriteHeader(http.StatusOK) - } - - nCalls++ - })) - defer server.Close() - - uuid1 := "40dcbd19-3b10-444e-bfff-aaee44a51fda" - mac1 := "52:2f:fd:df:fc:c0" - serialNumber1 := "0OCZJ19N65" - hostname1 := "myTestHostname" - - setup(ctx, t, st, uuid1, serialNumber1, hostname1, mac1) - - downloadURL := server.URL + "/metadata?h=${hostname}&m=${mac}&s=${serial}&u=${uuid}" - - param := procfs.NewParameter(constants.KernelParamConfig) - param.Append(downloadURL) - - procfs.ProcCmdline().Set(constants.KernelParamConfig, param) - defer procfs.ProcCmdline().Set(constants.KernelParamConfig, nil) - - go func() { - testObj := metal.Metal{} - _, err := testObj.Configuration(ctx, st) - assert.NoError(t, err) - - cancel() - }() - - <-ctx.Done() + select { + case <-ctx.Done(): + t.Error("timeout") + case cfg := <-ch: + assert.Equal(t, "metal", cfg.Metadata.Platform) + assert.Equal(t, uuid.TypedSpec().UUID, cfg.Metadata.InstanceID) + } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url.go new file mode 100644 index 000000000..4dec9578c --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url.go @@ -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 metal + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + + "github.com/talos-systems/talos/pkg/machinery/constants" + hardwareResource "github.com/talos-systems/talos/pkg/machinery/resources/hardware" + "github.com/talos-systems/talos/pkg/machinery/resources/network" +) + +// PopulateURLParameters fills in empty parameters in the download URL. +// +//nolint:gocyclo +func PopulateURLParameters(ctx context.Context, downloadURL string, r state.State) (string, error) { + populatedURL := downloadURL + + genErr := func(varOfKey string, errToWrap error) error { + return fmt.Errorf("error while substituting %s: %w", varOfKey, errToWrap) + } + + u, err := url.Parse(populatedURL) + if err != nil { + return "", fmt.Errorf("failed to parse %s: %w", populatedURL, err) + } + + values := u.Query() + + substitute := func(varToSubstitute string, getFunc func(ctx context.Context, r state.State) (string, error)) error { + m := newMatcher(varToSubstitute) + + for qKey, qValues := range values { + if len(qValues) == 0 { + continue + } + + qVal := qValues[0] + + // backwards compatible behavior for the uuid key + if qKey == constants.UUIDKey && !(len(qValues) == 1 && len(strings.TrimSpace(qVal)) > 0) { + uid, err := getSystemUUID(ctx, r) + if err != nil { + return fmt.Errorf("error while substituting UUID: %w", err) + } + + values.Set(constants.UUIDKey, uid) + + continue + } + + replacer := m.process(qVal) + + if len(replacer.Matches) < 1 { + continue + } + + val, err := getFunc(ctx, r) + if err != nil { + return genErr(m.Key, err) + } + + qVal = replacer.ReplaceMatches(val) + + values.Set(qKey, qVal) + } + + return nil + } + + if err := substitute(constants.UUIDKey, getSystemUUID); err != nil { + return "", err + } + + if err := substitute(constants.SerialNumberKey, getSystemSerialNumber); err != nil { + return "", err + } + + if err := substitute(constants.MacKey, getMACAddress); err != nil { + return "", err + } + + if err := substitute(constants.HostnameKey, getHostname); err != nil { + return "", err + } + + u.RawQuery = values.Encode() + + return u.String(), nil +} + +func getResource[T resource.Resource](ctx context.Context, r state.State, namespace, typ, valName string, isReadyFunc func(T) bool, checkAndGetFunc func(T) string) (string, error) { + metadata := resource.NewMetadata(namespace, typ, "", resource.VersionUndefined) + + watchCtx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + events := make(chan safe.WrappedStateEvent[T]) + + err := safe.StateWatchKind(watchCtx, r, metadata, events, state.WithBootstrapContents(true)) + if err != nil { + return "", fmt.Errorf("failed to watch %s resources: %w", typ, err) + } + + var watchErr error + + for { + select { + case <-watchCtx.Done(): + err := fmt.Errorf("failed to determine %s of %s: %w", valName, typ, watchCtx.Err()) + err = fmt.Errorf("%s; %w", err.Error(), watchErr) + + return "", err + case event := <-events: + eventResource, err := event.Resource() + if err != nil { + watchErr = fmt.Errorf("%s; invalid resource in wrapped event: %w", watchErr.Error(), err) + } + + if !isReadyFunc(eventResource) { + continue + } + + val := checkAndGetFunc(eventResource) + if val == "" { + return "", fmt.Errorf("%s property of resource %s is empty", valName, typ) + } + + return val, nil + } + } +} + +func getUUIDProperty(r *hardwareResource.SystemInformation) string { + return r.TypedSpec().UUID +} + +func getSerialNumberProperty(r *hardwareResource.SystemInformation) string { + return r.TypedSpec().SerialNumber +} + +func getSystemUUID(ctx context.Context, r state.State) (string, error) { + return getResource(ctx, r, hardwareResource.NamespaceName, hardwareResource.SystemInformationType, "UUID", func(*hardwareResource.SystemInformation) bool { return true }, getUUIDProperty) +} + +func getSystemSerialNumber(ctx context.Context, r state.State) (string, error) { + return getResource(ctx, + r, + hardwareResource.NamespaceName, + hardwareResource.SystemInformationType, + "Serial Number", + func(*hardwareResource.SystemInformation) bool { return true }, + getSerialNumberProperty) +} + +func getMACAddressProperty(r *network.LinkStatus) string { + return r.TypedSpec().HardwareAddr.String() +} + +func checkLinkUp(r *network.LinkStatus) bool { + return r.TypedSpec().LinkState +} + +func getMACAddress(ctx context.Context, r state.State) (string, error) { + return getResource(ctx, r, network.NamespaceName, network.LinkStatusType, "MAC Address", checkLinkUp, getMACAddressProperty) +} + +func getHostnameProperty(r *network.HostnameSpec) string { + return r.TypedSpec().Hostname +} + +func getHostname(ctx context.Context, r state.State) (string, error) { + return getResource(ctx, r, network.NamespaceName, network.HostnameSpecType, "Hostname", func(*network.HostnameSpec) bool { return true }, getHostnameProperty) +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url_test.go new file mode 100644 index 000000000..b7c7ce6c1 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/url_test.go @@ -0,0 +1,248 @@ +// 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 metal_test + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "github.com/cosi-project/runtime/pkg/state/impl/inmem" + "github.com/cosi-project/runtime/pkg/state/impl/namespaced" + "github.com/stretchr/testify/assert" + "github.com/talos-systems/go-procfs/procfs" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/hardware" + "github.com/talos-systems/talos/pkg/machinery/resources/network" +) + +func createOrUpdate(ctx context.Context, st state.State, r resource.Resource) error { + oldRes, err := st.Get(ctx, r.Metadata()) + if err != nil && !state.IsNotFoundError(err) { + return err + } + + if oldRes == nil { + err = st.Create(ctx, r) + if err != nil { + return err + } + } else { + r.Metadata().SetVersion(oldRes.Metadata().Version()) + + err = st.Update(ctx, r) + if err != nil { + return err + } + } + + return nil +} + +func setup(ctx context.Context, t *testing.T, st state.State, mockUUID, mockSerialNumber, mockHostname, mockMAC string) { + testID := "testID" + sysInfo := hardware.NewSystemInformation(testID) + sysInfo.TypedSpec().UUID = mockUUID + sysInfo.TypedSpec().SerialNumber = mockSerialNumber + assert.NoError(t, createOrUpdate(ctx, st, sysInfo)) + + hostnameSpec := network.NewHostnameSpec(network.NamespaceName, testID) + hostnameSpec.TypedSpec().Hostname = mockHostname + assert.NoError(t, createOrUpdate(ctx, st, hostnameSpec)) + + linkStatusSpec := network.NewLinkStatus(network.NamespaceName, testID) + parsedMockMAC, err := net.ParseMAC(mockMAC) + assert.NoError(t, err) + + linkStatusSpec.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(parsedMockMAC) + linkStatusSpec.TypedSpec().LinkState = true + assert.NoError(t, createOrUpdate(ctx, st, linkStatusSpec)) +} + +func TestPopulateURLParameters(t *testing.T) { + mockUUID := "40dcbd19-3b10-444e-bfff-aaee44a51fda" + + mockMAC := "52:2f:fd:df:fc:c0" + + mockSerialNumber := "0OCZJ19N65" + + mockHostname := "myTestHostname" + + for _, tt := range []struct { + name string + url string + expectedURL string + expectedError string + }{ + { + name: "no uuid", + url: "http://example.com/metadata", + expectedURL: "http://example.com/metadata", + }, + { + name: "empty uuid", + url: "http://example.com/metadata?uuid=", + expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID), + }, + { + name: "uuid present", + url: "http://example.com/metadata?uuid=xyz", + expectedURL: "http://example.com/metadata?uuid=xyz", + }, + { + name: "multiple uuids in one query parameter", + url: "http://example.com/metadata?u=this-${uuid}-equals-${uuid}-exactly", + expectedURL: fmt.Sprintf("http://example.com/metadata?u=this-%s-equals-%s-exactly", mockUUID, mockUUID), + }, + { + name: "uuid and mac in one query parameter", + url: "http://example.com/metadata?u=this-${uuid}-and-${mac}-together", + expectedURL: fmt.Sprintf("http://example.com/metadata?u=this-%s-and-%s-together", mockUUID, mockMAC), + }, + { + name: "other parameters", + url: "http://example.com/metadata?foo=a", + expectedURL: "http://example.com/metadata?foo=a", + }, + { + name: "multiple uuids", + url: "http://example.com/metadata?uuid=xyz&uuid=foo", + expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID), + }, + { + name: "single serial number", + url: "http://example.com/metadata?serial=${serial}", + expectedURL: fmt.Sprintf("http://example.com/metadata?serial=%s", mockSerialNumber), + }, + { + name: "single MAC", + url: "http://example.com/metadata?mac=${mac}", + expectedURL: fmt.Sprintf("http://example.com/metadata?mac=%s", mockMAC), + }, + { + name: "single hostname", + url: "http://example.com/metadata?host=${hostname}", + expectedURL: fmt.Sprintf("http://example.com/metadata?host=%s", mockHostname), + }, + { + name: "serial number, MAC and hostname", + url: "http://example.com/metadata?h=${hostname}&m=${mac}&s=${serial}", + expectedURL: fmt.Sprintf("http://example.com/metadata?h=%s&m=%s&s=%s", mockHostname, mockMAC, mockSerialNumber), + }, + { + name: "uuid, serial number, MAC and hostname; case-insensitive", + url: "http://example.com/metadata?h=${HOSTname}&m=${mAC}&s=${SERIAL}&u=${uUid}", + expectedURL: fmt.Sprintf("http://example.com/metadata?h=%s&m=%s&s=%s&u=%s", mockHostname, mockMAC, mockSerialNumber, mockUUID), + }, + { + name: "MAC and UUID without variable", + url: "http://example.com/metadata?macaddr=${mac}&uuid=", + expectedURL: fmt.Sprintf("http://example.com/metadata?macaddr=%s&uuid=%s", mockMAC, mockUUID), + }, + { + name: "serial number and UUID without variable, order is not preserved", + url: "http://example.com/metadata?uuid=&ser=${serial}", + expectedURL: fmt.Sprintf("http://example.com/metadata?ser=%s&uuid=%s", mockSerialNumber, mockUUID), + }, + { + name: "UUID variable", + url: "http://example.com/metadata?uuid=${uuid}", + expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID), + }, + { + name: "serial number and UUID with variable, order is not preserved", + url: "http://example.com/metadata?uuid=${uuid}&ser=${serial}", + expectedURL: fmt.Sprintf("http://example.com/metadata?ser=%s&uuid=%s", mockSerialNumber, mockUUID), + }, + } { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + st := state.WrapCore(namespaced.NewState(inmem.Build)) + + setup(ctx, t, st, mockUUID, mockSerialNumber, mockHostname, mockMAC) + + output, err := metal.PopulateURLParameters(ctx, tt.url, st) + + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + } else { + u, err := url.Parse(tt.expectedURL) + assert.NoError(t, err) + u.RawQuery = u.Query().Encode() + assert.Equal(t, u.String(), output) + } + }) + } +} + +func TestRepopulateOnRetry(t *testing.T) { + st := state.WrapCore(namespaced.NewState(inmem.Build)) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + nCalls := 0 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch nCalls { + case 0: + assert.Equal(t, "h=myTestHostname&m=52%3A2f%3Afd%3Adf%3Afc%3Ac0&s=0OCZJ19N65&u=40dcbd19-3b10-444e-bfff-aaee44a51fda", r.URL.RawQuery) + w.WriteHeader(http.StatusNotFound) + + // After the first call we change the resources that should be substituted in the next call. + uuid2 := "9fba530f-767d-40f9-9410-bb1fed5d2134" + mac2 := "aa:aa:bb:bb:cc:cc" + serialNumber2 := "111AAA9N65" + hostname2 := "anotherHostname" + + setup(ctx, t, st, uuid2, serialNumber2, hostname2, mac2) + case 1: + // Before the second call Configuration() should have resubstituted all the new parameters in the URL. + assert.Equal(t, "h=anotherHostname&m=aa%3Aaa%3Abb%3Abb%3Acc%3Acc&s=111AAA9N65&u=9fba530f-767d-40f9-9410-bb1fed5d2134", r.URL.RawQuery) + w.WriteHeader(http.StatusOK) + } + + nCalls++ + })) + defer server.Close() + + uuid1 := "40dcbd19-3b10-444e-bfff-aaee44a51fda" + mac1 := "52:2f:fd:df:fc:c0" + serialNumber1 := "0OCZJ19N65" + hostname1 := "myTestHostname" + + setup(ctx, t, st, uuid1, serialNumber1, hostname1, mac1) + + downloadURL := server.URL + "/metadata?h=${hostname}&m=${mac}&s=${serial}&u=${uuid}" + + param := procfs.NewParameter(constants.KernelParamConfig) + param.Append(downloadURL) + + procfs.ProcCmdline().Set(constants.KernelParamConfig, param) + defer procfs.ProcCmdline().Set(constants.KernelParamConfig, nil) + + go func() { + testObj := metal.Metal{} + _, err := testObj.Configuration(ctx, st) + assert.NoError(t, err) + + cancel() + }() + + <-ctx.Done() +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go index 1f2c0d259..e96fa7bf1 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go @@ -19,6 +19,7 @@ import ( "github.com/siderolabs/go-blockdevice/blockdevice/filesystem" "github.com/siderolabs/go-blockdevice/blockdevice/probe" "golang.org/x/sys/unix" + yaml "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" @@ -59,8 +60,8 @@ type NetworkConfig struct { // Ethernet holds network interface info. type Ethernet struct { Match struct { - Name []string `yaml:"name,omitempty"` - HWAddr []string `yaml:"macaddress,omitempty"` + Name string `yaml:"name,omitempty"` + HWAddr string `yaml:"macaddress,omitempty"` } `yaml:"match,omitempty"` DHCPv4 bool `yaml:"dhcp4,omitempty"` DHCPv6 bool `yaml:"dhcp6,omitempty"` @@ -179,14 +180,16 @@ func (n *Nocloud) configFromCD() (metaConfig []byte, networkConfig []byte, machi } //nolint:gocyclo -func (n *Nocloud) acquireConfig(ctx context.Context) (metadataConfigDl, metadataNetworkConfigDl, machineConfigDl []byte, hostname string, err error) { +func (n *Nocloud) acquireConfig(ctx context.Context) (metadataConfigDl, metadataNetworkConfigDl, machineConfigDl []byte, metadata *MetadataConfig, err error) { s, err := smbios.GetSMBIOSInfo() if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, err } - metaBaseURL := "" - networkSource := false + var ( + metaBaseURL, hostname string + networkSource bool + ) options := strings.Split(s.SystemInformation.SerialNumber, ";") for _, option := range options { @@ -220,7 +223,17 @@ func (n *Nocloud) acquireConfig(ctx context.Context) (metadataConfigDl, metadata metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromCD() } - return metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, hostname, err + metadata = &MetadataConfig{} + + if metadataConfigDl != nil { + _ = yaml.Unmarshal(metadataConfigDl, metadata) //nolint:errcheck + } + + if hostname != "" { + metadata.Hostname = hostname + } + + return metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, metadata, err } //nolint:gocyclo diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go index 72ae5b5f5..a12a08b33 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go @@ -17,6 +17,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Nocloud is the concrete type that implements the runtime.Platform interface. @@ -28,15 +29,15 @@ func (n *Nocloud) Name() string { } // ParseMetadata converts nocloud metadata to platform network config. -func (n *Nocloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, hostname string) (*runtime.PlatformNetworkConfig, error) { +func (n *Nocloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} - if hostname != "" { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(hostname); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } @@ -56,6 +57,12 @@ func (n *Nocloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, hostna return nil, fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version) } + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: n.Name(), + Hostname: metadata.Hostname, + InstanceID: metadata.InstanceID, + } + return networkConfig, nil } @@ -89,7 +96,7 @@ func (n *Nocloud) KernelArgs() procfs.Parameters { // //nolint:gocyclo func (n *Nocloud) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { - metadataConfigDl, metadataNetworkConfigDl, _, hostname, err := n.acquireConfig(ctx) + metadataConfigDl, metadataNetworkConfigDl, _, metadata, err := n.acquireConfig(ctx) if stderrors.Is(err, errors.ErrNoConfigSource) { err = nil } @@ -98,31 +105,19 @@ func (n *Nocloud) NetworkConfiguration(ctx context.Context, _ state.State, ch ch return err } - if metadataConfigDl == nil && metadataNetworkConfigDl == nil && hostname == "" { + if metadataConfigDl == nil && metadataNetworkConfigDl == nil { // no data, use cached network configuration if available return nil } - var ( - unmarshalledMetadataConfig MetadataConfig - unmarshalledNetworkConfig NetworkConfig - ) - - if metadataConfigDl != nil { - _ = yaml.Unmarshal(metadataConfigDl, &unmarshalledMetadataConfig) //nolint:errcheck - } - + var unmarshalledNetworkConfig NetworkConfig if metadataNetworkConfigDl != nil { if err = yaml.Unmarshal(metadataNetworkConfigDl, &unmarshalledNetworkConfig); err != nil { return err } } - if hostname == "" { - hostname = unmarshalledMetadataConfig.Hostname - } - - networkConfig, err := n.ParseMetadata(&unmarshalledNetworkConfig, hostname) + networkConfig, err := n.ParseMetadata(&unmarshalledNetworkConfig, metadata) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go index 41695afe1..9858b99fa 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go @@ -32,13 +32,11 @@ func TestParseMetadata(t *testing.T) { for _, tt := range []struct { name string raw []byte - hostname string expected string }{ { name: "V1", raw: rawMetadataV1, - hostname: "talos", expected: expectedNetworkConfigV1, }, { @@ -56,7 +54,12 @@ func TestParseMetadata(t *testing.T) { require.NoError(t, yaml.Unmarshal(tt.raw, &m)) - networkConfig, err := n.ParseMetadata(&m, tt.hostname) + mc := nocloud.MetadataConfig{ + Hostname: "talos.fqdn", + InstanceID: "0", + } + + networkConfig, err := n.ParseMetadata(&m, &mc) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml index 85c365bfe..33162ea50 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml @@ -46,7 +46,7 @@ routes: layer: platform hostnames: - hostname: talos - domainname: "" + domainname: fqdn layer: platform resolvers: - dnsServers: @@ -55,3 +55,7 @@ resolvers: timeServers: [] operators: [] externalIPs: [] +metadata: + platform: nocloud + hostname: talos.fqdn + instanceId: "0" diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml index 568ca1b57..ea28b2ab7 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml @@ -44,7 +44,10 @@ routes: flags: "" protocol: static layer: platform -hostnames: [] +hostnames: + - hostname: talos + domainname: fqdn + layer: platform resolvers: - dnsServers: - 8.8.8.8 @@ -58,3 +61,7 @@ operators: routeMetric: 1024 layer: platform externalIPs: [] +metadata: + platform: nocloud + hostname: talos.fqdn + instanceId: "0" diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml index 0467d3af5..a6ed77fff 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml @@ -1,6 +1,8 @@ version: 2 ethernets: eth0: + match: + macaddress: '00:20:6e:1f:f9:a8' dhcp4: true addresses: - 192.168.14.2/24 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go index 74f72489f..ec5d78a58 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go @@ -32,8 +32,8 @@ const ( // OpenstackExternalIPEndpoint is the local Openstack endpoint for the external IP. OpenstackExternalIPEndpoint = "http://169.254.169.254/latest/meta-data/public-ipv4" - // OpenstackHostnameEndpoint is the local Openstack endpoint for the hostname. - OpenstackHostnameEndpoint = "http://169.254.169.254/latest/meta-data/hostname" + // OpenstackInstanceTypeEndpoint is the local Openstack endpoint for the instance-type. + OpenstackInstanceTypeEndpoint = "http://169.254.169.254/latest/meta-data/instance-type" // OpenstackMetaDataEndpoint is the local Openstack endpoint for the meta config. OpenstackMetaDataEndpoint = "http://169.254.169.254/" + configMetadataPath // OpenstackNetworkDataEndpoint is the local Openstack endpoint for the network config. @@ -71,7 +71,11 @@ type NetworkConfig struct { // MetadataConfig holds meta info. type MetadataConfig struct { - Hostname string `json:"hostname,omitempty"` + UUID string `json:"uuid,omitempty"` + Hostname string `json:"hostname,omitempty"` + AvailabilityZone string `json:"availability_zone,omitempty"` + ProjectID string `json:"project_id"` + InstanceType string `json:"instance_type"` } func (o *Openstack) configFromNetwork(ctx context.Context) (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { @@ -158,20 +162,15 @@ func (o *Openstack) configFromCD() (metaConfig []byte, networkConfig []byte, mac return metaConfig, networkConfig, machineConfig, nil } -func (o *Openstack) hostname(ctx context.Context) []byte { - log.Printf("fetching hostname from: %q", OpenstackHostnameEndpoint) +func (o *Openstack) instanceType(ctx context.Context) string { + log.Printf("fetching instance-type from: %q", OpenstackInstanceTypeEndpoint) - hostname, err := download.Download(ctx, OpenstackHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + sku, err := download.Download(ctx, OpenstackInstanceTypeEndpoint) if err != nil { - // Platform cannot support this endpoint, or returns timeout. - log.Printf("failed to fetch hostname, ignored: %s", err) - - return nil + return "" } - return hostname + return string(sku) } func (o *Openstack) externalIPs(ctx context.Context) (addrs []netip.Addr) { diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go index 9988dec7d..51b041cb8 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go @@ -22,6 +22,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/utils" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Openstack is the concrete type that implements the runtime.Platform interface. @@ -35,19 +36,15 @@ func (o *Openstack) Name() string { // ParseMetadata converts OpenStack metadata to platform network configuration. // //nolint:gocyclo,cyclop -func (o *Openstack) ParseMetadata(unmarshalledMetadataConfig *MetadataConfig, unmarshalledNetworkConfig *NetworkConfig, hostname string, extIPs []netip.Addr) (*runtime.PlatformNetworkConfig, error) { +func (o *Openstack) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, extIPs []netip.Addr, metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} - if hostname == "" { - hostname = unmarshalledMetadataConfig.Hostname - } - - if hostname != "" { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(hostname); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } @@ -210,6 +207,15 @@ func (o *Openstack) ParseMetadata(unmarshalledMetadataConfig *MetadataConfig, un } } + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: o.Name(), + Hostname: metadata.Hostname, + Zone: metadata.AvailabilityZone, + InstanceID: metadata.UUID, + InstanceType: metadata.InstanceType, + ProviderID: fmt.Sprintf("openstack:///%s", metadata.UUID), + } + return networkConfig, nil } @@ -246,6 +252,8 @@ func (o *Openstack) KernelArgs() procfs.Parameters { // NetworkConfiguration implements the runtime.Platform interface. func (o *Openstack) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { + networkSource := false + metadataConfigDl, metadataNetworkConfigDl, _, err := o.configFromCD() if err != nil { metadataConfigDl, metadataNetworkConfigDl, _, err = o.configFromNetwork(ctx) @@ -256,21 +264,30 @@ func (o *Openstack) NetworkConfiguration(ctx context.Context, _ state.State, ch if err != nil { return err } + + networkSource = true } - hostname := o.hostname(ctx) - extIPs := o.externalIPs(ctx) - var ( - unmarshalledMetadataConfig MetadataConfig - unmarshalledNetworkConfig NetworkConfig + meta MetadataConfig + unmarshalledNetworkConfig NetworkConfig ) // ignore errors unmarshaling, empty configs work just fine as empty default - _ = json.Unmarshal(metadataConfigDl, &unmarshalledMetadataConfig) //nolint:errcheck + _ = json.Unmarshal(metadataConfigDl, &meta) //nolint:errcheck _ = json.Unmarshal(metadataNetworkConfigDl, &unmarshalledNetworkConfig) //nolint:errcheck - networkConfig, err := o.ParseMetadata(&unmarshalledMetadataConfig, &unmarshalledNetworkConfig, string(hostname), extIPs) + var extIPs []netip.Addr + + if networkSource { + extIPs = o.externalIPs(ctx) + + if meta.InstanceType == "" { + meta.InstanceType = o.instanceType(ctx) + } + } + + networkConfig, err := o.ParseMetadata(&unmarshalledNetworkConfig, extIPs, &meta) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go index a5780487e..7d6e46023 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go @@ -29,15 +29,15 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { o := &openstack.Openstack{} - var m openstack.MetadataConfig + var metadata openstack.MetadataConfig - require.NoError(t, json.Unmarshal(rawMetadata, &m)) + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) var n openstack.NetworkConfig require.NoError(t, json.Unmarshal(rawNetwork, &n)) - networkConfig, err := o.ParseMetadata(&m, &n, "", []netip.Addr{netip.MustParseAddr("1.2.3.4")}) + networkConfig, err := o.ParseMetadata(&n, []netip.Addr{netip.MustParseAddr("1.2.3.4")}, &metadata) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/testdata/expected.yaml index 162b9d17d..5b74e9fdf 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/testdata/expected.yaml @@ -133,3 +133,9 @@ operators: layer: platform externalIPs: - 1.2.3.4 +metadata: + platform: openstack + hostname: talos + zone: nova + instanceId: 39073b0a-1234-1234-1234-5e76a4bd64b2 + providerId: openstack:///39073b0a-1234-1234-1234-5e76a4bd64b2 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/metadata.go new file mode 100644 index 000000000..1f2c42f44 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/metadata.go @@ -0,0 +1,51 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package oracle + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/download" +) + +// Ref: https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm +const ( + // OracleMetadataEndpoint is the local metadata endpoint for the hostname. + OracleMetadataEndpoint = "http://169.254.169.254/opc/v2/instance/" + // OracleUserDataEndpoint is the local metadata endpoint inside of Oracle Cloud. + OracleUserDataEndpoint = "http://169.254.169.254/opc/v2/instance/metadata/user_data" + // OracleNetworkEndpoint is the local network metadata endpoint inside of Oracle Cloud. + OracleNetworkEndpoint = "http://169.254.169.254/opc/v2/vnics/" +) + +// MetadataConfig represents a metadata Oracle instance. +type MetadataConfig struct { + Hostname string `json:"hostname,omitempty"` + ID string `json:"id,omitempty"` + Region string `json:"region,omitempty"` + AvailabilityDomain string `json:"availabilityDomain,omitempty"` + FaultDomain string `json:"faultDomain,omitempty"` + Shape string `json:"shape,omitempty"` +} + +func (o *Oracle) getMetadata(ctx context.Context) (*MetadataConfig, error) { + metaConfigDl, err := download.Download(ctx, OracleMetadataEndpoint, //nolint:errcheck + download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"}), + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil { + return nil, fmt.Errorf("error fetching metadata: %w", err) + } + + var meta MetadataConfig + if err = json.Unmarshal(metaConfigDl, &meta); err != nil { + return nil, err + } + + return &meta, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go index 37f9d5915..a86370d08 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "log" + "strings" "github.com/cosi-project/runtime/pkg/state" "github.com/talos-systems/go-procfs/procfs" @@ -19,16 +20,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/resources/network" -) - -// Ref: https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm -const ( - // OracleHostnameEndpoint is the local metadata endpoint for the hostname. - OracleHostnameEndpoint = "http://169.254.169.254/opc/v2/instance/hostname" - // OracleUserDataEndpoint is the local metadata endpoint inside of Oracle Cloud. - OracleUserDataEndpoint = "http://169.254.169.254/opc/v2/instance/metadata/user_data" - // OracleNetworkEndpoint is the local network metadata endpoint inside of Oracle Cloud. - OracleNetworkEndpoint = "http://169.254.169.254/opc/v2/vnics/" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // NetworkConfig holds network interface meta config. @@ -50,15 +42,15 @@ func (o *Oracle) Name() string { } // ParseMetadata converts Oracle Cloud metadata into platform network configuration. -func (o *Oracle) ParseMetadata(interfaceAddresses []NetworkConfig, hostname string) (*runtime.PlatformNetworkConfig, error) { +func (o *Oracle) ParseMetadata(interfaceAddresses []NetworkConfig, metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} - if hostname != "" { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(hostname); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } @@ -81,6 +73,22 @@ func (o *Oracle) ParseMetadata(interfaceAddresses []NetworkConfig, hostname stri } } + zone := metadata.AvailabilityDomain + + if idx := strings.LastIndex(zone, ":"); idx != -1 { + zone = zone[:idx] + } + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: o.Name(), + Hostname: metadata.Hostname, + Region: strings.ToLower(metadata.Region), + Zone: strings.ToLower(zone), + InstanceType: metadata.Shape, + InstanceID: metadata.ID, + ProviderID: fmt.Sprintf("oci://%s", metadata.ID), + } + return networkConfig, nil } @@ -118,28 +126,28 @@ func (o *Oracle) KernelArgs() procfs.Parameters { // NetworkConfiguration implements the runtime.Platform interface. func (o *Oracle) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching oracle metadata from: %q", OracleMetadataEndpoint) + + metadata, err := o.getMetadata(ctx) + if err != nil { + return err + } + log.Printf("fetching network config from %q", OracleNetworkEndpoint) - metadataNetworkConfig, err := download.Download(ctx, OracleNetworkEndpoint, + metadataNetworkConfigDl, err := download.Download(ctx, OracleNetworkEndpoint, download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"})) if err != nil { - return fmt.Errorf("failed to fetch network config from metadata service: %w", err) + return fmt.Errorf("failed to fetch network config from: %w", err) } var interfaceAddresses []NetworkConfig - if err = json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil { + if err = json.Unmarshal(metadataNetworkConfigDl, &interfaceAddresses); err != nil { return err } - log.Printf("fetching hostname from: %q", OracleHostnameEndpoint) - - hostname, _ := download.Download(ctx, OracleHostnameEndpoint, //nolint:errcheck - download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"}), - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - - networkConfig, err := o.ParseMetadata(interfaceAddresses, string(hostname)) + networkConfig, err := o.ParseMetadata(interfaceAddresses, metadata) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go index a1efd6834..bc73ef77a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go @@ -19,17 +19,24 @@ import ( //go:embed testdata/metadata.json var rawMetadata []byte +//go:embed testdata/metadatanetwork.json +var rawMetadataNetwork []byte + //go:embed testdata/expected.yaml var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { - o := &oracle.Oracle{} + p := &oracle.Oracle{} + + var metadata oracle.MetadataConfig + + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) var m []oracle.NetworkConfig - require.NoError(t, json.Unmarshal(rawMetadata, &m)) + require.NoError(t, json.Unmarshal(rawMetadataNetwork, &m)) - networkConfig, err := o.ParseMetadata(m, "talos") + networkConfig, err := p.ParseMetadata(m, &metadata) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml index 9ffa81af4..7ab777957 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml @@ -15,3 +15,11 @@ operators: routeMetric: 1024 layer: platform externalIPs: [] +metadata: + platform: oracle + hostname: talos + region: phx + zone: emir + instanceType: VM.Standard.E3.Flex + instanceId: ocid1.instance.oc1.phx.exampleuniqueID + providerId: oci://ocid1.instance.oc1.phx.exampleuniqueID diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json index 4dc7fcd01..37ebee9d4 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json @@ -1,12 +1,53 @@ -[ - { - "vnicId": "ocid1.vnic.oc1.eu-amsterdam-1.asdasd", - "privateIp": "172.16.1.11", - "vlanTag": 1, - "macAddr": "02:00:17:00:00:00", - "virtualRouterIp": "172.16.1.1", - "subnetCidrBlock": "172.16.1.0/24", - "ipv6SubnetCidrBlock": "2603:a:b:c::/64", - "ipv6VirtualRouterIp": "fe80::a:b:c:d" +{ + "availabilityDomain": "EMIr:PHX-AD-1", + "faultDomain": "FAULT-DOMAIN-3", + "compartmentId": "ocid1.tenancy.oc1..exampleuniqueID", + "displayName": "talos-instance", + "hostname": "talos", + "id": "ocid1.instance.oc1.phx.exampleuniqueID", + "image": "ocid1.image.oc1.phx.exampleuniqueID", + "metadata": {}, + "region": "phx", + "canonicalRegionName": "us-phoenix-1", + "ociAdName": "phx-ad-1", + "regionInfo": { + "realmKey": "oc1", + "realmDomainComponent": "oraclecloud.com", + "regionKey": "PHX", + "regionIdentifier": "us-phoenix-1" + }, + "shape": "VM.Standard.E3.Flex", + "state": "Running", + "timeCreated": 1600381928581, + "agentConfig": { + "monitoringDisabled": false, + "managementDisabled": false, + "allPluginsDisabled": false, + "pluginsConfig": [ + { + "name": "OS Management Service Agent", + "desiredState": "ENABLED" + }, + { + "name": "Custom Logs Monitoring", + "desiredState": "ENABLED" + }, + { + "name": "Compute Instance Run Command", + "desiredState": "ENABLED" + }, + { + "name": "Compute Instance Monitoring", + "desiredState": "ENABLED" + } + ] + }, + "freeformTags": { + "Department": "Finance" + }, + "definedTags": { + "Operations": { + "CostCenter": "42" + } } -] +} \ No newline at end of file diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadatanetwork.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadatanetwork.json new file mode 100644 index 000000000..c5470e24c --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadatanetwork.json @@ -0,0 +1,12 @@ +[ + { + "vnicId": "ocid1.vnic.oc1.eu-amsterdam-1.asdasd", + "privateIp": "172.16.1.11", + "vlanTag": 1, + "macAddr": "02:00:17:00:00:00", + "virtualRouterIp": "172.16.1.1", + "subnetCidrBlock": "172.16.1.0/24", + "ipv6SubnetCidrBlock": "2603:a:b:c::/64", + "ipv6VirtualRouterIp": "fe80::a:b:c:d" + } +] \ No newline at end of file diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/metadata.go new file mode 100644 index 000000000..c3bd132f8 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/metadata.go @@ -0,0 +1,36 @@ +// 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 scaleway + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/scaleway/scaleway-sdk-go/api/instance/v1" + + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // ScalewayMetadataEndpoint is the local Scaleway endpoint. + ScalewayMetadataEndpoint = "http://169.254.42.42/conf?format=json" + // ScalewayUserDataEndpoint is the local Scaleway endpoint for the config. + ScalewayUserDataEndpoint = "http://169.254.42.42/user_data/cloud-init" +) + +func (u *Scaleway) getMetadata(ctx context.Context) (*instance.Metadata, error) { + metaConfigDl, err := download.Download(ctx, ScalewayMetadataEndpoint) + if err != nil { + return nil, fmt.Errorf("error fetching metadata: %w", err) + } + + var meta instance.Metadata + if err = json.Unmarshal(metaConfigDl, &meta); err != nil { + return nil, err + } + + return &meta, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go index c39748179..b28f27de6 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go @@ -7,10 +7,11 @@ package scaleway import ( "context" - "encoding/json" + "fmt" "log" "net/netip" "strconv" + "strings" "github.com/cosi-project/runtime/pkg/state" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" @@ -21,13 +22,7 @@ import ( "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" -) - -const ( - // ScalewayMetadataEndpoint is the local Scaleway endpoint. - ScalewayMetadataEndpoint = "http://169.254.42.42/conf?format=json" - // ScalewayUserDataEndpoint is the local Scaleway endpoint for the config. - ScalewayUserDataEndpoint = "http://169.254.42.42/user_data/cloud-init" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Scaleway is the concrete type that implements the runtime.Platform interface. @@ -38,24 +33,24 @@ func (s *Scaleway) Name() string { return "scaleway" } -// ParseMetadata converts Scaleway met. -func (s *Scaleway) ParseMetadata(metadataConfig *instance.Metadata) (*runtime.PlatformNetworkConfig, error) { +// ParseMetadata converts Scaleway platform metadata into platform network config. +func (s *Scaleway) ParseMetadata(metadata *instance.Metadata) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} - if metadataConfig.Hostname != "" { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(metadataConfig.Hostname); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if metadataConfig.PublicIP.Address != "" { - ip, err := netip.ParseAddr(metadataConfig.PublicIP.Address) + if metadata.PublicIP.Address != "" { + ip, err := netip.ParseAddr(metadata.PublicIP.Address) if err != nil { return nil, err } @@ -94,13 +89,13 @@ func (s *Scaleway) ParseMetadata(metadataConfig *instance.Metadata) (*runtime.Pl ConfigLayer: network.ConfigPlatform, }) - if metadataConfig.IPv6.Address != "" { - bits, err := strconv.Atoi(metadataConfig.IPv6.Netmask) + if metadata.IPv6.Address != "" { + bits, err := strconv.Atoi(metadata.IPv6.Netmask) if err != nil { return nil, err } - ip, err := netip.ParseAddr(metadataConfig.IPv6.Address) + ip, err := netip.ParseAddr(metadata.IPv6.Address) if err != nil { return nil, err } @@ -118,7 +113,7 @@ func (s *Scaleway) ParseMetadata(metadataConfig *instance.Metadata) (*runtime.Pl }, ) - gw, err := netip.ParseAddr(metadataConfig.IPv6.Gateway) + gw, err := netip.ParseAddr(metadata.IPv6.Gateway) if err != nil { return nil, err } @@ -139,20 +134,34 @@ func (s *Scaleway) ParseMetadata(metadataConfig *instance.Metadata) (*runtime.Pl networkConfig.Routes = append(networkConfig.Routes, route) } + zoneParts := strings.Split(metadata.Location.ZoneID, "-") + if len(zoneParts) > 2 { + zoneParts = zoneParts[:2] + } + + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: s.Name(), + Hostname: metadata.Hostname, + Region: strings.Join(zoneParts, "-"), + Zone: metadata.Location.ZoneID, + InstanceType: metadata.CommercialType, + InstanceID: metadata.ID, + ProviderID: fmt.Sprintf("scaleway://instance/%s/%s", metadata.Location.ZoneID, metadata.ID), + } + return networkConfig, nil } // Configuration implements the runtime.Platform interface. +// +//nolint:stylecheck func (s *Scaleway) Configuration(ctx context.Context, r state.State) ([]byte, error) { log.Printf("fetching machine config from %q", ScalewayUserDataEndpoint) - machineConfigDl, err := download.Download(ctx, ScalewayUserDataEndpoint, - download.WithLowSrcPort()) - if err != nil { - return nil, errors.ErrNoConfigSource - } - - return machineConfigDl, nil + return download.Download(ctx, ScalewayUserDataEndpoint, + download.WithLowSrcPort(), + download.WithErrorOnNotFound(errors.ErrNoConfigSource), + download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) } // Mode implements the runtime.Platform interface. @@ -171,16 +180,11 @@ func (s *Scaleway) KernelArgs() procfs.Parameters { func (s *Scaleway) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { log.Printf("fetching scaleway instance config from: %q", ScalewayMetadataEndpoint) - metadataDl, err := download.Download(ctx, ScalewayMetadataEndpoint) + metadata, err := s.getMetadata(ctx) if err != nil { return err } - metadata := &instance.Metadata{} - if err = json.Unmarshal(metadataDl, metadata); err != nil { - return err - } - networkConfig, err := s.ParseMetadata(metadata) if err != nil { return err diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go index 780b54b63..f3e799acf 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go @@ -26,11 +26,11 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { p := &scaleway.Scaleway{} - var m instance.Metadata + var metadata instance.Metadata - require.NoError(t, json.Unmarshal(rawMetadata, &m)) + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) - networkConfig, err := p.ParseMetadata(&m) + networkConfig, err := p.ParseMetadata(&metadata) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml index 350c38281..39b137ff0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml @@ -53,3 +53,9 @@ operators: layer: platform externalIPs: - 11.22.222.222 +metadata: + platform: scaleway + hostname: scw-talos + instanceType: DEV1-S + instanceId: 11111111-1111-1111-1111-111111111111 + providerId: scaleway://instance//11111111-1111-1111-1111-111111111111 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/metadata.go new file mode 100644 index 000000000..790b27862 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/metadata.go @@ -0,0 +1,63 @@ +// 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 upcloud + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // UpCloudMetadataEndpoint is the local UpCloud endpoint. + UpCloudMetadataEndpoint = "http://169.254.169.254/metadata/v1.json" + + // UpCloudUserDataEndpoint is the local UpCloud endpoint for the config. + UpCloudUserDataEndpoint = "http://169.254.169.254/metadata/v1/user_data" +) + +// MetadataConfig represents a metadata Upcloud instance. +type MetadataConfig struct { + Hostname string `json:"hostname,omitempty"` + InstanceID string `json:"instance_id,omitempty"` + PublicKeys []string `json:"public_keys,omitempty"` + Zone string `json:"region,omitempty"` + Tags []string `json:"tags,omitempty"` + + Network struct { + Interfaces []struct { + Index int `json:"index,omitempty"` + IPAddresses []struct { + Address string `json:"address,omitempty"` + DHCP bool `json:"dhcp,omitempty"` + DNS []string `json:"dns,omitempty"` + Family string `json:"family,omitempty"` + Floating bool `json:"floating,omitempty"` + Gateway string `json:"gateway,omitempty"` + Network string `json:"network,omitempty"` + } `json:"ip_addresses,omitempty"` + MAC string `json:"mac,omitempty"` + NetworkType string `json:"type,omitempty"` + NetworkID string `json:"network_id,omitempty"` + } `json:"interfaces,omitempty"` + DNS []string `json:"dns,omitempty"` + } `json:"network,omitempty"` +} + +func (u *UpCloud) getMetadata(ctx context.Context) (*MetadataConfig, error) { + metaConfigDl, err := download.Download(ctx, UpCloudMetadataEndpoint) + if err != nil { + return nil, fmt.Errorf("error fetching metadata: %w", err) + } + + var meta MetadataConfig + if err = json.Unmarshal(metaConfigDl, &meta); err != nil { + return nil, err + } + + return &meta, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml index 4c5e2838a..778b31e84 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml @@ -73,3 +73,8 @@ operators: layer: platform externalIPs: - 185.70.197.2 +metadata: + platform: upcloud + hostname: talos + instanceId: 00123456-1111-2222-3333-123456789012 + providerId: upcloud://00123456-1111-2222-3333-123456789012 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go index 931994440..fdc398588 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go @@ -7,7 +7,6 @@ package upcloud import ( "context" - "encoding/json" "fmt" "log" "net/netip" @@ -20,43 +19,9 @@ import ( "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) -const ( - // UpCloudMetadataEndpoint is the local UpCloud endpoint. - UpCloudMetadataEndpoint = "http://169.254.169.254/metadata/v1.json" - - // UpCloudUserDataEndpoint is the local UpCloud endpoint for the config. - UpCloudUserDataEndpoint = "http://169.254.169.254/metadata/v1/user_data" -) - -// MetaData represents a metadata Upcloud interface. -type MetaData struct { - Hostname string `json:"hostname,omitempty"` - InstanceID string `json:"instance_id,omitempty"` - PublicKeys []string `json:"public_keys,omitempty"` - Region string `json:"region,omitempty"` - - Network struct { - Interfaces []struct { - Index int `json:"index,omitempty"` - IPAddresses []struct { - Address string `json:"address,omitempty"` - DHCP bool `json:"dhcp,omitempty"` - DNS []string `json:"dns,omitempty"` - Family string `json:"family,omitempty"` - Floating bool `json:"floating,omitempty"` - Gateway string `json:"gateway,omitempty"` - Network string `json:"network,omitempty"` - } `json:"ip_addresses,omitempty"` - MAC string `json:"mac,omitempty"` - NetworkType string `json:"type,omitempty"` - NetworkID string `json:"network_id,omitempty"` - } `json:"interfaces,omitempty"` - DNS []string `json:"dns,omitempty"` - } `json:"network,omitempty"` -} - // UpCloud is the concrete type that implements the runtime.Platform interface. type UpCloud struct{} @@ -68,15 +33,15 @@ func (u *UpCloud) Name() string { // ParseMetadata converts Upcloud metadata into platform network configuration. // //nolint:gocyclo -func (u *UpCloud) ParseMetadata(meta *MetaData) (*runtime.PlatformNetworkConfig, error) { +func (u *UpCloud) ParseMetadata(metadata *MetadataConfig) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} - if meta.Hostname != "" { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(meta.Hostname); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } @@ -87,7 +52,7 @@ func (u *UpCloud) ParseMetadata(meta *MetaData) (*runtime.PlatformNetworkConfig, firstIP := true - for _, addr := range meta.Network.Interfaces { + for _, addr := range metadata.Network.Interfaces { if addr.Index <= 0 { // protect from negative interface name continue } @@ -192,6 +157,14 @@ func (u *UpCloud) ParseMetadata(meta *MetaData) (*runtime.PlatformNetworkConfig, }) } + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: u.Name(), + Hostname: metadata.Hostname, + Zone: metadata.Zone, + InstanceID: metadata.InstanceID, + ProviderID: fmt.Sprintf("upcloud://%s", metadata.InstanceID), + } + return networkConfig, nil } @@ -216,19 +189,14 @@ func (u *UpCloud) KernelArgs() procfs.Parameters { // NetworkConfiguration implements the runtime.Platform interface. func (u *UpCloud) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { - log.Printf("fetching UpCloud instance config from: %q ", UpCloudMetadataEndpoint) + log.Printf("fetching UpCloud instance config from: %q", UpCloudMetadataEndpoint) - metaConfigDl, err := download.Download(ctx, UpCloudMetadataEndpoint) + metadata, err := u.getMetadata(ctx) if err != nil { - return fmt.Errorf("failed to fetch network config from metadata service: %w", err) - } - - meta := &MetaData{} - if err = json.Unmarshal(metaConfigDl, meta); err != nil { return err } - networkConfig, err := u.ParseMetadata(meta) + networkConfig, err := u.ParseMetadata(metadata) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go index e74a37362..27ea58c9c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go @@ -25,11 +25,11 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { p := &upcloud.UpCloud{} - var m upcloud.MetaData + var metadata upcloud.MetadataConfig - require.NoError(t, json.Unmarshal(rawMetadata, &m)) + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) - networkConfig, err := p.ParseMetadata(&m) + networkConfig, err := p.ParseMetadata(&metadata) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go index d268f4ec4..10348dd72 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go @@ -24,6 +24,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" platformerrors "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/machinery/constants" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // VMware is the concrete type that implements the platform.Platform interface. @@ -202,5 +203,15 @@ func (v *VMware) KernelArgs() procfs.Parameters { // NetworkConfiguration implements the runtime.Platform interface. func (v *VMware) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { + networkConfig := &runtime.PlatformNetworkConfig{ + Metadata: &runtimeres.PlatformMetadataSpec{Platform: v.Name()}, + } + + select { + case <-ctx.Done(): + return ctx.Err() + case ch <- networkConfig: + } + return nil } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/metadata.go new file mode 100644 index 000000000..4f56dbbc4 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/metadata.go @@ -0,0 +1,38 @@ +// 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 vultr + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/vultr/metadata" + + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // VultrMetadataEndpoint is the local Vultr endpoint fot the instance metadata. + VultrMetadataEndpoint = "http://169.254.169.254/v1.json" + // VultrExternalIPEndpoint is the local Vultr endpoint for the external IP. + VultrExternalIPEndpoint = "http://169.254.169.254/latest/meta-data/public-ipv4" + // VultrUserDataEndpoint is the local Vultr endpoint for the config. + VultrUserDataEndpoint = "http://169.254.169.254/latest/user-data" +) + +func (g *Vultr) getMetadata(ctx context.Context) (*metadata.MetaData, error) { + metaConfigDl, err := download.Download(ctx, VultrMetadataEndpoint) + if err != nil { + return nil, fmt.Errorf("error fetching metadata: %w", err) + } + + var meta metadata.MetaData + if err = json.Unmarshal(metaConfigDl, &meta); err != nil { + return nil, err + } + + return &meta, nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml index 910c45952..b07ee51d2 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml @@ -36,3 +36,9 @@ operators: layer: platform externalIPs: - 1.2.3.4 +metadata: + platform: vultr + hostname: talos + region: AMS + instanceId: 91b07056-af72-4551-b15b-d57d34071be9 + providerId: vultr://91b07056-af72-4551-b15b-d57d34071be9 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json index fd4097564..92803809e 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json @@ -58,4 +58,4 @@ "regioncode": "AMS" }, "user-defined": [] -} +} \ No newline at end of file diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go index 827b97006..9938dbfe1 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go @@ -7,7 +7,6 @@ package vultr import ( "context" - "encoding/json" stderrors "errors" "fmt" "log" @@ -23,17 +22,7 @@ import ( "github.com/talos-systems/talos/pkg/download" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" -) - -const ( - // VultrMetadataEndpoint is the local Vultr endpoint fot the instance metadata. - VultrMetadataEndpoint = "http://169.254.169.254/v1.json" - // VultrExternalIPEndpoint is the local Vultr endpoint for the external IP. - VultrExternalIPEndpoint = "http://169.254.169.254/latest/meta-data/public-ipv4" - // VultrHostnameEndpoint is the local Vultr endpoint for the hostname. - VultrHostnameEndpoint = "http://169.254.169.254/latest/meta-data/hostname" - // VultrUserDataEndpoint is the local Vultr endpoint for the config. - VultrUserDataEndpoint = "http://169.254.169.254/latest/user-data" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" ) // Vultr is the concrete type that implements the runtime.Platform interface. @@ -45,26 +34,28 @@ func (v *Vultr) Name() string { } // ParseMetadata converts Vultr platform metadata into platform network config. -func (v *Vultr) ParseMetadata(meta *metadata.MetaData, extIP []byte) (*runtime.PlatformNetworkConfig, error) { +// +//nolint:gocyclo +func (v *Vultr) ParseMetadata(extIP []byte, metadata *metadata.MetaData) (*runtime.PlatformNetworkConfig, error) { networkConfig := &runtime.PlatformNetworkConfig{} if ip, err := netip.ParseAddr(string(extIP)); err == nil { networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) } - if meta.Hostname != "" { + if metadata.Hostname != "" { hostnameSpec := network.HostnameSpecSpec{ ConfigLayer: network.ConfigPlatform, } - if err := hostnameSpec.ParseFQDN(meta.Hostname); err != nil { + if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil { return nil, err } networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - for i, addr := range meta.Interfaces { + for i, addr := range metadata.Interfaces { iface := fmt.Sprintf("eth%d", i) link := network.LinkSpecSpec{ @@ -119,10 +110,20 @@ func (v *Vultr) ParseMetadata(meta *metadata.MetaData, extIP []byte) (*runtime.P } } + networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{ + Platform: v.Name(), + Hostname: metadata.Hostname, + Region: metadata.Region.RegionCode, + InstanceID: metadata.InstanceV2ID, + ProviderID: fmt.Sprintf("vultr://%s", metadata.InstanceV2ID), + } + return networkConfig, nil } // Configuration implements the runtime.Platform interface. +// +//nolint:stylecheck func (v *Vultr) Configuration(ctx context.Context, r state.State) ([]byte, error) { log.Printf("fetching machine config from: %q", VultrUserDataEndpoint) @@ -143,15 +144,10 @@ func (v *Vultr) KernelArgs() procfs.Parameters { // NetworkConfiguration implements the runtime.Platform interface. func (v *Vultr) NetworkConfiguration(ctx context.Context, _ state.State, ch chan<- *runtime.PlatformNetworkConfig) error { - log.Printf("fetching Vultr instance config from: %q ", VultrMetadataEndpoint) + log.Printf("fetching Vultr instance metadata from: %q", VultrMetadataEndpoint) - metaConfigDl, err := download.Download(ctx, VultrMetadataEndpoint) + metadata, err := v.getMetadata(ctx) if err != nil { - return fmt.Errorf("error fetching metadata: %w", err) - } - - meta := &metadata.MetaData{} - if err = json.Unmarshal(metaConfigDl, meta); err != nil { return err } @@ -162,7 +158,7 @@ func (v *Vultr) NetworkConfiguration(ctx context.Context, _ state.State, ch chan return err } - networkConfig, err := v.ParseMetadata(meta, extIP) + networkConfig, err := v.ParseMetadata(extIP, metadata) if err != nil { return err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go index 00fd3acc0..26137ba90 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go @@ -26,11 +26,11 @@ var expectedNetworkConfig string func TestParseMetadata(t *testing.T) { p := &vultr.Vultr{} - var m metadata.MetaData + var metadata metadata.MetaData - require.NoError(t, json.Unmarshal(rawMetadata, &m)) + require.NoError(t, json.Unmarshal(rawMetadata, &metadata)) - networkConfig, err := p.ParseMetadata(&m, []byte("1.2.3.4")) + networkConfig, err := p.ParseMetadata([]byte("1.2.3.4"), &metadata) require.NoError(t, err) marshaled, err := yaml.Marshal(networkConfig) diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index 36196b425..0168fd965 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -156,6 +156,7 @@ func NewState() (*State, error) { &runtime.KernelParamStatus{}, &runtime.MachineStatus{}, &runtime.MountStatus{}, + &runtime.PlatformMetadata{}, &secrets.API{}, &secrets.CertSAN{}, &secrets.Etcd{}, diff --git a/pkg/machinery/api/resource/definitions/runtime/runtime.pb.go b/pkg/machinery/api/resource/definitions/runtime/runtime.pb.go index 70e956463..34f10635d 100644 --- a/pkg/machinery/api/resource/definitions/runtime/runtime.pb.go +++ b/pkg/machinery/api/resource/definitions/runtime/runtime.pb.go @@ -383,6 +383,102 @@ func (x *MountStatusSpec) GetOptions() []string { return nil } +// PlatformMetadataSpec describes platform metadata properties. +type PlatformMetadataSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + Region string `protobuf:"bytes,3,opt,name=region,proto3" json:"region,omitempty"` + Zone string `protobuf:"bytes,4,opt,name=zone,proto3" json:"zone,omitempty"` + InstanceType string `protobuf:"bytes,5,opt,name=instance_type,json=instanceType,proto3" json:"instance_type,omitempty"` + InstanceId string `protobuf:"bytes,6,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + ProviderId string `protobuf:"bytes,7,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` +} + +func (x *PlatformMetadataSpec) Reset() { + *x = PlatformMetadataSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_definitions_runtime_runtime_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PlatformMetadataSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlatformMetadataSpec) ProtoMessage() {} + +func (x *PlatformMetadataSpec) ProtoReflect() protoreflect.Message { + mi := &file_resource_definitions_runtime_runtime_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlatformMetadataSpec.ProtoReflect.Descriptor instead. +func (*PlatformMetadataSpec) Descriptor() ([]byte, []int) { + return file_resource_definitions_runtime_runtime_proto_rawDescGZIP(), []int{6} +} + +func (x *PlatformMetadataSpec) GetPlatform() string { + if x != nil { + return x.Platform + } + return "" +} + +func (x *PlatformMetadataSpec) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *PlatformMetadataSpec) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *PlatformMetadataSpec) GetZone() string { + if x != nil { + return x.Zone + } + return "" +} + +func (x *PlatformMetadataSpec) GetInstanceType() string { + if x != nil { + return x.InstanceType + } + return "" +} + +func (x *PlatformMetadataSpec) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *PlatformMetadataSpec) GetProviderId() string { + if x != nil { + return x.ProviderId + } + return "" +} + // UnmetCondition is a failure which prevents machine from being ready at the stage. type UnmetCondition struct { state protoimpl.MessageState @@ -396,7 +492,7 @@ type UnmetCondition struct { func (x *UnmetCondition) Reset() { *x = UnmetCondition{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_runtime_runtime_proto_msgTypes[6] + mi := &file_resource_definitions_runtime_runtime_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -409,7 +505,7 @@ func (x *UnmetCondition) String() string { func (*UnmetCondition) ProtoMessage() {} func (x *UnmetCondition) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_runtime_runtime_proto_msgTypes[6] + mi := &file_resource_definitions_runtime_runtime_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -422,7 +518,7 @@ func (x *UnmetCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use UnmetCondition.ProtoReflect.Descriptor instead. func (*UnmetCondition) Descriptor() ([]byte, []int) { - return file_resource_definitions_runtime_runtime_proto_rawDescGZIP(), []int{6} + return file_resource_definitions_runtime_runtime_proto_rawDescGZIP(), []int{7} } func (x *UnmetCondition) GetName() string { @@ -494,17 +590,31 @@ var file_resource_definitions_runtime_runtime_proto_rawDesc = []byte{ 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3c, 0x0a, - 0x0e, 0x55, 0x6e, 0x6d, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x4f, 0x5a, 0x4d, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2d, - 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70, 0x6b, - 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xe1, 0x01, + 0x0a, 0x14, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, + 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, + 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, + 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, + 0x64, 0x22, 0x3c, 0x0a, 0x0e, 0x55, 0x6e, 0x6d, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, + 0x4f, 0x5a, 0x4d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x61, + 0x6c, 0x6f, 0x73, 0x2d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, + 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x79, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -519,7 +629,7 @@ func file_resource_definitions_runtime_runtime_proto_rawDescGZIP() []byte { return file_resource_definitions_runtime_runtime_proto_rawDescData } -var file_resource_definitions_runtime_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_resource_definitions_runtime_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_resource_definitions_runtime_runtime_proto_goTypes = []interface{}{ (*KernelModuleSpecSpec)(nil), // 0: talos.resource.definitions.runtime.KernelModuleSpecSpec (*KernelParamSpecSpec)(nil), // 1: talos.resource.definitions.runtime.KernelParamSpecSpec @@ -527,13 +637,14 @@ var file_resource_definitions_runtime_runtime_proto_goTypes = []interface{}{ (*MachineStatusSpec)(nil), // 3: talos.resource.definitions.runtime.MachineStatusSpec (*MachineStatusStatus)(nil), // 4: talos.resource.definitions.runtime.MachineStatusStatus (*MountStatusSpec)(nil), // 5: talos.resource.definitions.runtime.MountStatusSpec - (*UnmetCondition)(nil), // 6: talos.resource.definitions.runtime.UnmetCondition - (enums.RuntimeMachineStage)(0), // 7: talos.resource.definitions.enums.RuntimeMachineStage + (*PlatformMetadataSpec)(nil), // 6: talos.resource.definitions.runtime.PlatformMetadataSpec + (*UnmetCondition)(nil), // 7: talos.resource.definitions.runtime.UnmetCondition + (enums.RuntimeMachineStage)(0), // 8: talos.resource.definitions.enums.RuntimeMachineStage } var file_resource_definitions_runtime_runtime_proto_depIdxs = []int32{ - 7, // 0: talos.resource.definitions.runtime.MachineStatusSpec.stage:type_name -> talos.resource.definitions.enums.RuntimeMachineStage + 8, // 0: talos.resource.definitions.runtime.MachineStatusSpec.stage:type_name -> talos.resource.definitions.enums.RuntimeMachineStage 4, // 1: talos.resource.definitions.runtime.MachineStatusSpec.status:type_name -> talos.resource.definitions.runtime.MachineStatusStatus - 6, // 2: talos.resource.definitions.runtime.MachineStatusStatus.unmet_conditions:type_name -> talos.resource.definitions.runtime.UnmetCondition + 7, // 2: talos.resource.definitions.runtime.MachineStatusStatus.unmet_conditions:type_name -> talos.resource.definitions.runtime.UnmetCondition 3, // [3:3] is the sub-list for method output_type 3, // [3:3] is the sub-list for method input_type 3, // [3:3] is the sub-list for extension type_name @@ -620,6 +731,18 @@ func file_resource_definitions_runtime_runtime_proto_init() { } } file_resource_definitions_runtime_runtime_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PlatformMetadataSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_definitions_runtime_runtime_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UnmetCondition); i { case 0: return &v.state @@ -638,7 +761,7 @@ func file_resource_definitions_runtime_runtime_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_definitions_runtime_runtime_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/machinery/api/resource/definitions/runtime/runtime_vtproto.pb.go b/pkg/machinery/api/resource/definitions/runtime/runtime_vtproto.pb.go index 766ff5f0e..2cda5ba49 100644 --- a/pkg/machinery/api/resource/definitions/runtime/runtime_vtproto.pb.go +++ b/pkg/machinery/api/resource/definitions/runtime/runtime_vtproto.pb.go @@ -343,6 +343,88 @@ func (m *MountStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *PlatformMetadataSpec) 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 *PlatformMetadataSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *PlatformMetadataSpec) 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 len(m.ProviderId) > 0 { + i -= len(m.ProviderId) + copy(dAtA[i:], m.ProviderId) + i = encodeVarint(dAtA, i, uint64(len(m.ProviderId))) + i-- + dAtA[i] = 0x3a + } + if len(m.InstanceId) > 0 { + i -= len(m.InstanceId) + copy(dAtA[i:], m.InstanceId) + i = encodeVarint(dAtA, i, uint64(len(m.InstanceId))) + i-- + dAtA[i] = 0x32 + } + if len(m.InstanceType) > 0 { + i -= len(m.InstanceType) + copy(dAtA[i:], m.InstanceType) + i = encodeVarint(dAtA, i, uint64(len(m.InstanceType))) + i-- + dAtA[i] = 0x2a + } + if len(m.Zone) > 0 { + i -= len(m.Zone) + copy(dAtA[i:], m.Zone) + i = encodeVarint(dAtA, i, uint64(len(m.Zone))) + i-- + dAtA[i] = 0x22 + } + if len(m.Region) > 0 { + i -= len(m.Region) + copy(dAtA[i:], m.Region) + i = encodeVarint(dAtA, i, uint64(len(m.Region))) + i-- + dAtA[i] = 0x1a + } + if len(m.Hostname) > 0 { + i -= len(m.Hostname) + copy(dAtA[i:], m.Hostname) + i = encodeVarint(dAtA, i, uint64(len(m.Hostname))) + i-- + dAtA[i] = 0x12 + } + if len(m.Platform) > 0 { + i -= len(m.Platform) + copy(dAtA[i:], m.Platform) + i = encodeVarint(dAtA, i, uint64(len(m.Platform))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *UnmetCondition) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -535,6 +617,46 @@ func (m *MountStatusSpec) SizeVT() (n int) { return n } +func (m *PlatformMetadataSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Platform) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.Hostname) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.Region) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.Zone) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.InstanceType) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.InstanceId) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.ProviderId) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + if m.unknownFields != nil { + n += len(m.unknownFields) + } + return n +} + func (m *UnmetCondition) SizeVT() (n int) { if m == nil { return 0 @@ -1304,6 +1426,281 @@ func (m *MountStatusSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *PlatformMetadataSpec) 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 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: PlatformMetadataSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PlatformMetadataSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Platform", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Platform = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hostname", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Hostname = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Region", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Region = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Zone", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Zone = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InstanceType", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.InstanceType = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InstanceId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.InstanceId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ProviderId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ProviderId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return 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 *UnmetCondition) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/pkg/machinery/resources/runtime/deep_copy.generated.go b/pkg/machinery/resources/runtime/deep_copy.generated.go index e46fe10fa..af44917fa 100644 --- a/pkg/machinery/resources/runtime/deep_copy.generated.go +++ b/pkg/machinery/resources/runtime/deep_copy.generated.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "deep-copy -type KernelModuleSpecSpec -type KernelParamSpecSpec -type KernelParamStatusSpec -type MachineStatusSpec -type MountStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "deep-copy -type KernelModuleSpecSpec -type KernelParamSpecSpec -type KernelParamStatusSpec -type MachineStatusSpec -type MountStatusSpec -type PlatformMetadataSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. package runtime @@ -47,3 +47,9 @@ func (o MountStatusSpec) DeepCopy() MountStatusSpec { } return cp } + +// DeepCopy generates a deep copy of PlatformMetadataSpec. +func (o PlatformMetadataSpec) DeepCopy() PlatformMetadataSpec { + var cp PlatformMetadataSpec = o + return cp +} diff --git a/pkg/machinery/resources/runtime/platform_metadata.go b/pkg/machinery/resources/runtime/platform_metadata.go new file mode 100644 index 000000000..7d3ee1c4c --- /dev/null +++ b/pkg/machinery/resources/runtime/platform_metadata.go @@ -0,0 +1,65 @@ +// 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 runtime + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/talos-systems/talos/pkg/machinery/proto" +) + +// PlatformMetadataType is type of Metadata resource. +const PlatformMetadataType = resource.Type("PlatformMetadatas.talos.dev") + +// PlatformMetadataID is the ID for Metadata resource platform. +const PlatformMetadataID resource.ID = "platformmetadata" + +// PlatformMetadata resource holds. +type PlatformMetadata = typed.Resource[PlatformMetadataSpec, PlatformMetadataRD] + +// PlatformMetadataSpec describes platform metadata properties. +// +//gotagsrewrite:gen +type PlatformMetadataSpec struct { + Platform string `yaml:"platform,omitempty" protobuf:"1"` + Hostname string `yaml:"hostname,omitempty" protobuf:"2"` + Region string `yaml:"region,omitempty" protobuf:"3"` + Zone string `yaml:"zone,omitempty" protobuf:"4"` + InstanceType string `yaml:"instanceType,omitempty" protobuf:"5"` + InstanceID string `yaml:"instanceId,omitempty" protobuf:"6"` + ProviderID string `yaml:"providerId,omitempty" protobuf:"7"` +} + +// NewPlatformMetadataSpec initializes a MetadataSpec resource. +func NewPlatformMetadataSpec(namespace resource.Namespace, id resource.ID) *PlatformMetadata { + return typed.NewResource[PlatformMetadataSpec, PlatformMetadataRD]( + resource.NewMetadata(namespace, PlatformMetadataType, PlatformMetadataID, resource.VersionUndefined), + PlatformMetadataSpec{}, + ) +} + +// PlatformMetadataRD provides auxiliary methods for PlatformMetadata. +type PlatformMetadataRD struct{} + +// ResourceDefinition implements typed.ResourceDefinition interface. +func (PlatformMetadataRD) ResourceDefinition(resource.Metadata, PlatformMetadataSpec) meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: PlatformMetadataType, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +func init() { + proto.RegisterDefaultTypes() + + err := protobuf.RegisterDynamic[PlatformMetadataSpec](PlatformMetadataType, &PlatformMetadata{}) + if err != nil { + panic(err) + } +} diff --git a/pkg/machinery/resources/runtime/runtime.go b/pkg/machinery/resources/runtime/runtime.go index 86cf5539a..ea23f36f6 100644 --- a/pkg/machinery/resources/runtime/runtime.go +++ b/pkg/machinery/resources/runtime/runtime.go @@ -5,4 +5,4 @@ package runtime //nolint:lll -//go:generate deep-copy -type KernelModuleSpecSpec -type KernelParamSpecSpec -type KernelParamStatusSpec -type MachineStatusSpec -type MountStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . +//go:generate deep-copy -type KernelModuleSpecSpec -type KernelParamSpecSpec -type KernelParamStatusSpec -type MachineStatusSpec -type MountStatusSpec -type PlatformMetadataSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . diff --git a/pkg/machinery/resources/runtime/runtime_test.go b/pkg/machinery/resources/runtime/runtime_test.go index 6720e94e7..0aa1984a6 100644 --- a/pkg/machinery/resources/runtime/runtime_test.go +++ b/pkg/machinery/resources/runtime/runtime_test.go @@ -31,6 +31,7 @@ func TestRegisterResource(t *testing.T) { &runtime.KernelParamStatus{}, &runtime.MachineStatus{}, &runtime.MountStatus{}, + &runtime.PlatformMetadata{}, } { assert.NoError(t, resourceRegistry.Register(ctx, resource)) } diff --git a/pkg/provision/providers/docker/node.go b/pkg/provision/providers/docker/node.go index 518fb50b1..a6ad429a9 100644 --- a/pkg/provision/providers/docker/node.go +++ b/pkg/provision/providers/docker/node.go @@ -64,7 +64,10 @@ func (p *provisioner) createNodes(ctx context.Context, clusterReq provision.Clus //nolint:gocyclo func (p *provisioner) createNode(ctx context.Context, clusterReq provision.ClusterRequest, nodeReq provision.NodeRequest, options *provision.Options) (provision.NodeInfo, error) { - env := []string{"PLATFORM=container"} + env := []string{ + "PLATFORM=container", + fmt.Sprintf("TALOSSKU=%dCPU-%dRAM", nodeReq.NanoCPUs/(1000*1000*1000), nodeReq.Memory/(1024*1024)), + } if !nodeReq.SkipInjectingConfig { cfg, err := nodeReq.Config.EncodeString() diff --git a/website/content/v1.3/reference/api.md b/website/content/v1.3/reference/api.md index f005ffe30..c4f0728d3 100644 --- a/website/content/v1.3/reference/api.md +++ b/website/content/v1.3/reference/api.md @@ -173,6 +173,7 @@ description: Talos gRPC API reference. - [MachineStatusSpec](#talos.resource.definitions.runtime.MachineStatusSpec) - [MachineStatusStatus](#talos.resource.definitions.runtime.MachineStatusStatus) - [MountStatusSpec](#talos.resource.definitions.runtime.MountStatusSpec) + - [PlatformMetadataSpec](#talos.resource.definitions.runtime.PlatformMetadataSpec) - [UnmetCondition](#talos.resource.definitions.runtime.UnmetCondition) - [resource/definitions/secrets/secrets.proto](#resource/definitions/secrets/secrets.proto) @@ -3133,6 +3134,27 @@ MountStatusSpec describes status of the defined sysctls. + + +### PlatformMetadataSpec +PlatformMetadataSpec describes platform metadata properties. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| platform | [string](#string) | | | +| hostname | [string](#string) | | | +| region | [string](#string) | | | +| zone | [string](#string) | | | +| instance_type | [string](#string) | | | +| instance_id | [string](#string) | | | +| provider_id | [string](#string) | | | + + + + + + ### UnmetCondition