feat: add support for instance tags on AWS

We can add on other platforms as well as we go.

See https://github.com/siderolabs/omni/issues/1059

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2025-04-04 18:34:28 +04:00
parent e8c3aeb801
commit 60448b516e
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
12 changed files with 323 additions and 43 deletions

View File

@ -116,6 +116,7 @@ message PlatformMetadataSpec {
bool spot = 8;
string internal_dns = 9;
string external_dns = 10;
map<string, string> tags = 11;
}
// SecurityStateSpec describes the security state resource properties.

View File

@ -28,7 +28,7 @@
}
] end),
"extra_tags": {
"Cluster Name": .cluster_name,
"ClusterName": .cluster_name,
"Project": "talos-e2e-ci",
"Environment": "ci"
}

View File

@ -104,6 +104,7 @@ func (a *AWS) ParseMetadata(metadata *MetadataConfig) (*runtime.PlatformNetworkC
Spot: metadata.InstanceLifeCycle == "spot",
InternalDNS: metadata.InternalDNS,
ExternalDNS: metadata.ExternalDNS,
Tags: metadata.Tags,
}
return networkConfig, nil

View File

@ -10,6 +10,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
smithyhttp "github.com/aws/smithy-go/transport/http"
@ -17,16 +18,17 @@ import (
// 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"`
InstanceLifeCycle string `json:"instance-life-cycle,omitempty"`
PublicIPv4 string `json:"public-ipv4,omitempty"`
PublicIPv6 string `json:"ipv6,omitempty"`
InternalDNS string `json:"local-hostname,omitempty"`
ExternalDNS string `json:"public-hostname,omitempty"`
Region string `json:"region,omitempty"`
Zone string `json:"zone,omitempty"`
Hostname string `json:"hostname,omitempty"`
InstanceID string `json:"instance-id,omitempty"`
InstanceType string `json:"instance-type,omitempty"`
InstanceLifeCycle string `json:"instance-life-cycle,omitempty"`
PublicIPv4 string `json:"public-ipv4,omitempty"`
PublicIPv6 string `json:"ipv6,omitempty"`
InternalDNS string `json:"local-hostname,omitempty"`
ExternalDNS string `json:"public-hostname,omitempty"`
Region string `json:"region,omitempty"`
Zone string `json:"zone,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
}
//nolint:gocyclo
@ -96,6 +98,16 @@ func (a *AWS) getMetadata(ctx context.Context) (*MetadataConfig, error) {
return nil, err
}
if tags, err := getMetadataKey("tags/instance"); err == nil {
metadata.Tags = make(map[string]string)
for _, key := range strings.Fields(tags) {
if value, err := getMetadataKey("tags/instance/" + key); err == nil {
metadata.Tags[key] = value
}
}
}
return &metadata, nil
}

View File

@ -21,3 +21,5 @@ metadata:
zone: us-east-1a
instanceId: i-0a0a0a0a0a0a0a0a0
providerId: aws:///us-east-1a/i-0a0a0a0a0a0a0a0a0
tags:
cluster: mycluster

View File

@ -3,5 +3,6 @@
"instance-id": "i-0a0a0a0a0a0a0a0a0",
"public-ipv4": "1.2.3.4",
"region": "us-east-1",
"zone": "us-east-1a"
"zone": "us-east-1a",
"tags": {"cluster": "mycluster"}
}

View File

@ -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/.
//go:build integration_api
package api
import (
"context"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"github.com/siderolabs/talos/internal/integration/base"
"github.com/siderolabs/talos/pkg/machinery/client"
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
)
// PlatformSuite ...
type PlatformSuite struct {
base.APISuite
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
// SuiteName ...
func (suite *PlatformSuite) SuiteName() string {
return "api.PlatformSuite"
}
// SetupTest ...
func (suite *PlatformSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 15*time.Second)
}
// TearDownTest ...
func (suite *PlatformSuite) TearDownTest() {
if suite.ctxCancel != nil {
suite.ctxCancel()
}
}
// TestPlatformMetadata verifies platform metadata.
func (suite *PlatformSuite) TestPlatformMetadata() {
node := suite.RandomDiscoveredNodeInternalIP()
ctx := client.WithNode(suite.ctx, node)
rtestutils.AssertResource(ctx, suite.T(), suite.Client.COSI, runtime.PlatformMetadataID, func(md *runtime.PlatformMetadata, asrt *assert.Assertions) {
marshaled, err := resource.MarshalYAML(md)
suite.Require().NoError(err)
yml, err := yaml.Marshal(marshaled)
suite.Require().NoError(err)
suite.T().Logf("platform metadata:\n%s", string(yml))
if md.TypedSpec().Platform == "aws" {
asrt.NotEmpty(md.TypedSpec().Tags)
}
})
}
func init() {
allSuites = append(allSuites, new(PlatformSuite))
}

View File

@ -879,6 +879,7 @@ type PlatformMetadataSpec struct {
Spot bool `protobuf:"varint,8,opt,name=spot,proto3" json:"spot,omitempty"`
InternalDns string `protobuf:"bytes,9,opt,name=internal_dns,json=internalDns,proto3" json:"internal_dns,omitempty"`
ExternalDns string `protobuf:"bytes,10,opt,name=external_dns,json=externalDns,proto3" json:"external_dns,omitempty"`
Tags map[string]string `protobuf:"bytes,11,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -983,6 +984,13 @@ func (x *PlatformMetadataSpec) GetExternalDns() string {
return ""
}
func (x *PlatformMetadataSpec) GetTags() map[string]string {
if x != nil {
return x.Tags
}
return nil
}
// SecurityStateSpec describes the security state resource properties.
type SecurityStateSpec struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -1326,7 +1334,7 @@ const file_resource_definitions_runtime_runtime_proto_rawDesc = "" +
"\x0ffilesystem_type\x18\x03 \x01(\tR\x0efilesystemType\x12\x18\n" +
"\aoptions\x18\x04 \x03(\tR\aoptions\x12\x1c\n" +
"\tencrypted\x18\x05 \x01(\bR\tencrypted\x121\n" +
"\x14encryption_providers\x18\x06 \x03(\tR\x13encryptionProviders\"\xbb\x02\n" +
"\x14encryption_providers\x18\x06 \x03(\tR\x13encryptionProviders\"\xcc\x03\n" +
"\x14PlatformMetadataSpec\x12\x1a\n" +
"\bplatform\x18\x01 \x01(\tR\bplatform\x12\x1a\n" +
"\bhostname\x18\x02 \x01(\tR\bhostname\x12\x16\n" +
@ -1340,7 +1348,11 @@ const file_resource_definitions_runtime_runtime_proto_rawDesc = "" +
"\x04spot\x18\b \x01(\bR\x04spot\x12!\n" +
"\finternal_dns\x18\t \x01(\tR\vinternalDns\x12!\n" +
"\fexternal_dns\x18\n" +
" \x01(\tR\vexternalDns\"\xb7\x02\n" +
" \x01(\tR\vexternalDns\x12V\n" +
"\x04tags\x18\v \x03(\v2B.talos.resource.definitions.runtime.PlatformMetadataSpec.TagsEntryR\x04tags\x1a7\n" +
"\tTagsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb7\x02\n" +
"\x11SecurityStateSpec\x12\x1f\n" +
"\vsecure_boot\x18\x01 \x01(\bR\n" +
"secureBoot\x12=\n" +
@ -1374,7 +1386,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, 22)
var file_resource_definitions_runtime_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_resource_definitions_runtime_runtime_proto_goTypes = []any{
(*DevicesStatusSpec)(nil), // 0: talos.resource.definitions.runtime.DevicesStatusSpec
(*DiagnosticSpec)(nil), // 1: talos.resource.definitions.runtime.DiagnosticSpec
@ -1398,28 +1410,30 @@ var file_resource_definitions_runtime_runtime_proto_goTypes = []any{
(*UnmetCondition)(nil), // 19: talos.resource.definitions.runtime.UnmetCondition
(*WatchdogTimerConfigSpec)(nil), // 20: talos.resource.definitions.runtime.WatchdogTimerConfigSpec
(*WatchdogTimerStatusSpec)(nil), // 21: talos.resource.definitions.runtime.WatchdogTimerStatusSpec
(*common.URL)(nil), // 22: common.URL
(enums.RuntimeMachineStage)(0), // 23: talos.resource.definitions.enums.RuntimeMachineStage
(*common.NetIP)(nil), // 24: common.NetIP
(enums.RuntimeSELinuxState)(0), // 25: talos.resource.definitions.enums.RuntimeSELinuxState
(*durationpb.Duration)(nil), // 26: google.protobuf.Duration
nil, // 22: talos.resource.definitions.runtime.PlatformMetadataSpec.TagsEntry
(*common.URL)(nil), // 23: common.URL
(enums.RuntimeMachineStage)(0), // 24: talos.resource.definitions.enums.RuntimeMachineStage
(*common.NetIP)(nil), // 25: common.NetIP
(enums.RuntimeSELinuxState)(0), // 26: talos.resource.definitions.enums.RuntimeSELinuxState
(*durationpb.Duration)(nil), // 27: google.protobuf.Duration
}
var file_resource_definitions_runtime_runtime_proto_depIdxs = []int32{
3, // 0: talos.resource.definitions.runtime.ExtensionServiceConfigSpec.files:type_name -> talos.resource.definitions.runtime.ExtensionServiceConfigFile
22, // 1: talos.resource.definitions.runtime.KmsgLogConfigSpec.destinations:type_name -> common.URL
23, // 2: talos.resource.definitions.runtime.MachineStatusSpec.stage:type_name -> talos.resource.definitions.enums.RuntimeMachineStage
23, // 1: talos.resource.definitions.runtime.KmsgLogConfigSpec.destinations:type_name -> common.URL
24, // 2: talos.resource.definitions.runtime.MachineStatusSpec.stage:type_name -> talos.resource.definitions.enums.RuntimeMachineStage
11, // 3: talos.resource.definitions.runtime.MachineStatusSpec.status:type_name -> talos.resource.definitions.runtime.MachineStatusStatus
19, // 4: talos.resource.definitions.runtime.MachineStatusStatus.unmet_conditions:type_name -> talos.resource.definitions.runtime.UnmetCondition
24, // 5: talos.resource.definitions.runtime.MaintenanceServiceConfigSpec.reachable_addresses:type_name -> common.NetIP
25, // 6: talos.resource.definitions.runtime.SecurityStateSpec.se_linux_state:type_name -> talos.resource.definitions.enums.RuntimeSELinuxState
26, // 7: talos.resource.definitions.runtime.WatchdogTimerConfigSpec.timeout:type_name -> google.protobuf.Duration
26, // 8: talos.resource.definitions.runtime.WatchdogTimerStatusSpec.timeout:type_name -> google.protobuf.Duration
26, // 9: talos.resource.definitions.runtime.WatchdogTimerStatusSpec.feed_interval:type_name -> google.protobuf.Duration
10, // [10:10] is the sub-list for method output_type
10, // [10:10] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
25, // 5: talos.resource.definitions.runtime.MaintenanceServiceConfigSpec.reachable_addresses:type_name -> common.NetIP
22, // 6: talos.resource.definitions.runtime.PlatformMetadataSpec.tags:type_name -> talos.resource.definitions.runtime.PlatformMetadataSpec.TagsEntry
26, // 7: talos.resource.definitions.runtime.SecurityStateSpec.se_linux_state:type_name -> talos.resource.definitions.enums.RuntimeSELinuxState
27, // 8: talos.resource.definitions.runtime.WatchdogTimerConfigSpec.timeout:type_name -> google.protobuf.Duration
27, // 9: talos.resource.definitions.runtime.WatchdogTimerStatusSpec.timeout:type_name -> google.protobuf.Duration
27, // 10: talos.resource.definitions.runtime.WatchdogTimerStatusSpec.feed_interval:type_name -> google.protobuf.Duration
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_resource_definitions_runtime_runtime_proto_init() }
@ -1433,7 +1447,7 @@ func file_resource_definitions_runtime_runtime_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_definitions_runtime_runtime_proto_rawDesc), len(file_resource_definitions_runtime_runtime_proto_rawDesc)),
NumEnums: 0,
NumMessages: 22,
NumMessages: 23,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -873,6 +873,25 @@ func (m *PlatformMetadataSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error)
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if len(m.Tags) > 0 {
for k := range m.Tags {
v := m.Tags[k]
baseI := i
i -= len(v)
copy(dAtA[i:], v)
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(v)))
i--
dAtA[i] = 0x12
i -= len(k)
copy(dAtA[i:], k)
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(k)))
i--
dAtA[i] = 0xa
i = protohelpers.EncodeVarint(dAtA, i, uint64(baseI-i))
i--
dAtA[i] = 0x5a
}
}
if len(m.ExternalDns) > 0 {
i -= len(m.ExternalDns)
copy(dAtA[i:], m.ExternalDns)
@ -1570,6 +1589,14 @@ func (m *PlatformMetadataSpec) SizeVT() (n int) {
if l > 0 {
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
if len(m.Tags) > 0 {
for k, v := range m.Tags {
_ = k
_ = v
mapEntrySize := 1 + len(k) + protohelpers.SizeOfVarint(uint64(len(k))) + 1 + len(v) + protohelpers.SizeOfVarint(uint64(len(v)))
n += mapEntrySize + 1 + protohelpers.SizeOfVarint(uint64(mapEntrySize))
}
}
n += len(m.unknownFields)
return n
}
@ -3761,6 +3788,133 @@ func (m *PlatformMetadataSpec) UnmarshalVT(dAtA []byte) error {
}
m.ExternalDns = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 11:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Tags == nil {
m.Tags = make(map[string]string)
}
var mapkey string
var mapvalue string
for iNdEx < postIndex {
entryPreIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
if fieldNum == 1 {
var stringLenmapkey uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLenmapkey |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLenmapkey := int(stringLenmapkey)
if intStringLenmapkey < 0 {
return protohelpers.ErrInvalidLength
}
postStringIndexmapkey := iNdEx + intStringLenmapkey
if postStringIndexmapkey < 0 {
return protohelpers.ErrInvalidLength
}
if postStringIndexmapkey > l {
return io.ErrUnexpectedEOF
}
mapkey = string(dAtA[iNdEx:postStringIndexmapkey])
iNdEx = postStringIndexmapkey
} else if fieldNum == 2 {
var stringLenmapvalue uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLenmapvalue |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLenmapvalue := int(stringLenmapvalue)
if intStringLenmapvalue < 0 {
return protohelpers.ErrInvalidLength
}
postStringIndexmapvalue := iNdEx + intStringLenmapvalue
if postStringIndexmapvalue < 0 {
return protohelpers.ErrInvalidLength
}
if postStringIndexmapvalue > l {
return io.ErrUnexpectedEOF
}
mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue])
iNdEx = postStringIndexmapvalue
} else {
iNdEx = entryPreIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return protohelpers.ErrInvalidLength
}
if (iNdEx + skippy) > postIndex {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
m.Tags[mapkey] = mapvalue
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])

View File

@ -150,6 +150,12 @@ func (o MountStatusSpec) DeepCopy() MountStatusSpec {
// DeepCopy generates a deep copy of PlatformMetadataSpec.
func (o PlatformMetadataSpec) DeepCopy() PlatformMetadataSpec {
var cp PlatformMetadataSpec = o
if o.Tags != nil {
cp.Tags = make(map[string]string, len(o.Tags))
for k2, v2 := range o.Tags {
cp.Tags[k2] = v2
}
}
return cp
}

View File

@ -26,16 +26,17 @@ type PlatformMetadata = typed.Resource[PlatformMetadataSpec, PlatformMetadataExt
//
//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"`
Spot bool `yaml:"spot,omitempty" protobuf:"8"`
InternalDNS string `yaml:"internalDNS,omitempty" protobuf:"9"`
ExternalDNS string `yaml:"externalDNS,omitempty" protobuf:"10"`
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"`
Spot bool `yaml:"spot,omitempty" protobuf:"8"`
InternalDNS string `yaml:"internalDNS,omitempty" protobuf:"9"`
ExternalDNS string `yaml:"externalDNS,omitempty" protobuf:"10"`
Tags map[string]string `yaml:"tags,omitempty" protobuf:"11"`
}
// NewPlatformMetadataSpec initializes a MetadataSpec resource.

View File

@ -285,6 +285,7 @@ description: Talos gRPC API reference.
- [MetaLoadedSpec](#talos.resource.definitions.runtime.MetaLoadedSpec)
- [MountStatusSpec](#talos.resource.definitions.runtime.MountStatusSpec)
- [PlatformMetadataSpec](#talos.resource.definitions.runtime.PlatformMetadataSpec)
- [PlatformMetadataSpec.TagsEntry](#talos.resource.definitions.runtime.PlatformMetadataSpec.TagsEntry)
- [SecurityStateSpec](#talos.resource.definitions.runtime.SecurityStateSpec)
- [UniqueMachineTokenSpec](#talos.resource.definitions.runtime.UniqueMachineTokenSpec)
- [UnmetCondition](#talos.resource.definitions.runtime.UnmetCondition)
@ -5241,6 +5242,23 @@ PlatformMetadataSpec describes platform metadata properties.
| spot | [bool](#bool) | | |
| internal_dns | [string](#string) | | |
| external_dns | [string](#string) | | |
| tags | [PlatformMetadataSpec.TagsEntry](#talos.resource.definitions.runtime.PlatformMetadataSpec.TagsEntry) | repeated | |
<a name="talos.resource.definitions.runtime.PlatformMetadataSpec.TagsEntry"></a>
### PlatformMetadataSpec.TagsEntry
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| key | [string](#string) | | |
| value | [string](#string) | | |