diff --git a/client/cloudinit/cloudinit.go b/client/cloudinit/cloudinit.go new file mode 100644 index 00000000..99295f26 --- /dev/null +++ b/client/cloudinit/cloudinit.go @@ -0,0 +1,10 @@ +package cloudinit + +const ( + // InstanceDataKey is the metdata key name to use for instance data. + InstanceDataKey = "meta-data" + // UserdataKey is the metadata key name to use for user data. + UserdataKey = "user-data" + // VendorDataKey is the metadata key name to use for vendor data. + VendorDataKey = "vendor-data" +) diff --git a/client/cloudinit/instance/const.go b/client/cloudinit/instance/const.go new file mode 100644 index 00000000..d4c6fc18 --- /dev/null +++ b/client/cloudinit/instance/const.go @@ -0,0 +1,19 @@ +package instance + +// These constants represent standard instance metadata key names. +const ( + // CloudNameKey is the instance metdata key name representing the cloud name. + CloudNameKey = "cloud_name" + // InstanceIDKey is the instance metdata key name representing the unique instance id mof the instance. + InstanceIDKey = "instance_id" + // LocalHostnameKey is the instance metdata key name representing the host name of the instance. + LocalHostnameKey = "local_hostname" + // PlatformKey is the instance metdata key name representing the hosting platform of the instance. + PlatformKey = "platform" +) + +// These constants represents custom instance metadata names +const ( + // ClusterNameKey is the instance metdata key name representing the cluster name of the instance. + ClusterNameKey = "cluster_name" +) diff --git a/client/cloudinit/instance/metadata.go b/client/cloudinit/instance/metadata.go new file mode 100644 index 00000000..b361d2cc --- /dev/null +++ b/client/cloudinit/instance/metadata.go @@ -0,0 +1,80 @@ +package instance + +// New creates a new instance metadata +func New(opts ...MetadataOption) Metadata { + m := map[string]string{} + + for _, opt := range opts { + opt(m) + } + + return m +} + +// Metadata represents the cloud-init instance metadata. +// See https://cloudinit.readthedocs.io/en/latest/topics/instancedata.html +type Metadata map[string]string + +// HasItem returns true/false if the specific metadata item exists. +func (m Metadata) HasItem(name string) bool { + if len(m) == 0 { + return false + } + _, ok := m[name] + + return ok +} + +// MetadataOption is an option when creating an instance of Metadata +type MetadataOption func(Metadata) + +// WithInstanceID will set the instance id metadata. +func WithInstanceID(instanceID string) MetadataOption { + return func(im Metadata) { + im[InstanceIDKey] = instanceID + } +} + +// WithCloudName will set the cloud name metadata. +func WithCloudName(name string) MetadataOption { + return func(im Metadata) { + im[CloudNameKey] = name + } +} + +// WithLocalHostname will set the local hostname metadata. +func WithLocalHostname(name string) MetadataOption { + return func(im Metadata) { + im[LocalHostnameKey] = name + } +} + +// WithPlatform will set the platform metadata. +func WithPlatform(name string) MetadataOption { + return func(im Metadata) { + im[PlatformKey] = name + } +} + +// WithClusterName will set the cluster name metadata. +func WithClusterName(name string) MetadataOption { + return func(im Metadata) { + im[ClusterNameKey] = name + } +} + +// WithExisting will set the metadata keys/values based on an existing Metadata. +func WithExisting(existing Metadata) MetadataOption { + return func(im Metadata) { + for k, v := range existing { + im[k] = v + } + } +} + +// WithKeyValue will set the metadata with the specified key and value. +func WithKeyValue(key, value string) MetadataOption { + return func(im Metadata) { + im[key] = value + } +} diff --git a/client/cloudinit/instance/metadata_test.go b/client/cloudinit/instance/metadata_test.go new file mode 100644 index 00000000..d5c2a995 --- /dev/null +++ b/client/cloudinit/instance/metadata_test.go @@ -0,0 +1,71 @@ +package instance_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/weaveworks/flintlock/client/cloudinit/instance" +) + +const ( + testCloudName = "nocloud" + testClusterName = "cluster1" + testInstanceID = "i-123456" + testHostName = "host1" + testPlatformName = "liquidmetal" +) + +func TestInstanceMetadata_NewNoOptions(t *testing.T) { + RegisterTestingT(t) + + m := instance.New() + Expect(m).NotTo(BeNil()) + Expect(m).To(HaveLen(0)) +} + +func TestInstanceMetadata_NewWithOptions(t *testing.T) { + RegisterTestingT(t) + + m := instance.New( + instance.WithCloudName(testCloudName), + instance.WithClusterName(testClusterName), + instance.WithInstanceID(testInstanceID), + instance.WithLocalHostname(testHostName), + instance.WithPlatform(testPlatformName), + ) + Expect(m).NotTo(BeNil()) + Expect(m).To(HaveLen(5)) + + Expect(m[instance.CloudNameKey]).To(Equal(testCloudName)) + Expect(m[instance.ClusterNameKey]).To(Equal(testClusterName)) + Expect(m[instance.InstanceIDKey]).To(Equal(testInstanceID)) + Expect(m[instance.LocalHostnameKey]).To(Equal(testHostName)) + Expect(m[instance.PlatformKey]).To(Equal(testPlatformName)) +} + +func TestInstanceMetadata_NewOptionsExisting(t *testing.T) { + RegisterTestingT(t) + + existing := instance.New( + instance.WithCloudName(testCloudName), + instance.WithClusterName(testClusterName), + instance.WithInstanceID(testInstanceID), + instance.WithLocalHostname(testHostName), + instance.WithPlatform(testPlatformName), + ) + + m := instance.New( + instance.WithExisting(existing), + instance.WithInstanceID("changed"), + ) + + Expect(m).NotTo(BeNil()) + Expect(m).To(HaveLen(5)) + + Expect(m[instance.CloudNameKey]).To(Equal(testCloudName)) + Expect(m[instance.ClusterNameKey]).To(Equal(testClusterName)) + Expect(m[instance.InstanceIDKey]).To(Equal("changed")) + Expect(m[instance.LocalHostnameKey]).To(Equal(testHostName)) + Expect(m[instance.PlatformKey]).To(Equal(testPlatformName)) +} diff --git a/client/cloudinit/metadata.go b/client/cloudinit/metadata.go deleted file mode 100644 index 939d25fe..00000000 --- a/client/cloudinit/metadata.go +++ /dev/null @@ -1,8 +0,0 @@ -package cloudinit - -type Metadata struct { - InstanceID string `yaml:"instance_id",json:"instance_id"` - LocalHostname string `yaml:"local_hostname",json:"local_hostname"` - Platform string `yaml:"platform",json:"platform"` - ClusterName string `yaml:"cluster_name",json:"cluster_name"` -} diff --git a/client/cloudinit/network.go b/client/cloudinit/network/network.go similarity index 97% rename from client/cloudinit/network.go rename to client/cloudinit/network/network.go index ae393a78..77879a65 100644 --- a/client/cloudinit/network.go +++ b/client/cloudinit/network/network.go @@ -1,4 +1,4 @@ -package cloudinit +package network type Network struct { Version int `yaml:"version"` diff --git a/client/cloudinit/userdata.go b/client/cloudinit/userdata/userdata.go similarity index 98% rename from client/cloudinit/userdata.go rename to client/cloudinit/userdata/userdata.go index ca906554..c34a5c1c 100644 --- a/client/cloudinit/userdata.go +++ b/client/cloudinit/userdata/userdata.go @@ -1,4 +1,4 @@ -package cloudinit +package userdata type UserData struct { HostName string `yaml:"hostname,omitempty"` diff --git a/client/go.mod b/client/go.mod index c1c9b75d..0165a87a 100644 --- a/client/go.mod +++ b/client/go.mod @@ -10,4 +10,5 @@ require ( google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.44.0 // indirect google.golang.org/protobuf v1.25.0 // indirect + github.com/onsi/gomega v1.18.1 // indirect ) diff --git a/core/application/app_test.go b/core/application/app_test.go index 928b31fb..297fbd8f 100644 --- a/core/application/app_test.go +++ b/core/application/app_test.go @@ -2,6 +2,7 @@ package application_test import ( "context" + "encoding/base64" "errors" "testing" "time" @@ -9,8 +10,10 @@ import ( "github.com/golang/mock/gomock" . "github.com/onsi/gomega" "github.com/spf13/afero" + "sigs.k8s.io/yaml" "github.com/weaveworks/flintlock/api/events" + "github.com/weaveworks/flintlock/client/cloudinit/instance" "github.com/weaveworks/flintlock/core/application" "github.com/weaveworks/flintlock/core/models" "github.com/weaveworks/flintlock/core/ports" @@ -55,7 +58,7 @@ func TestApp_CreateMicroVM(t *testing.T) { }), ).Return(nil, nil) - expectedCreatedSpec := createTestSpec("id1234", defaults.MicroVMNamespace, testUID) + expectedCreatedSpec := createTestSpecWithMetadata("id1234", defaults.MicroVMNamespace, testUID, createInstanceMetadatadata(t, testUID)) expectedCreatedSpec.Spec.CreatedAt = frozenTime().Unix() expectedCreatedSpec.Status.State = models.PendingState @@ -96,7 +99,7 @@ func TestApp_CreateMicroVM(t *testing.T) { nil, ) - expectedCreatedSpec := createTestSpec("id1234", "default", testUID) + expectedCreatedSpec := createTestSpecWithMetadata("id1234", "default", testUID, createInstanceMetadatadata(t, testUID)) expectedCreatedSpec.Spec.CreatedAt = frozenTime().Unix() expectedCreatedSpec.Status.State = models.PendingState @@ -138,6 +141,47 @@ func TestApp_CreateMicroVM(t *testing.T) { ) }, }, + { + name: "spec with id, namespace and existing instance data create", + specToCreate: createTestSpecWithMetadata("id1234", "default", testUID, createInstanceMetadatadata(t, "abcdef")), + expectError: false, + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { + im.GenerateRandom().Return(testUID, nil).Times(1) + rm.Get( + gomock.AssignableToTypeOf(context.Background()), + gomock.Eq(ports.RepositoryGetOptions{ + Name: "id1234", + Namespace: "default", + UID: testUID, + }), + ).Return( + nil, + nil, + ) + + expectedCreatedSpec := createTestSpecWithMetadata("id1234", "default", testUID, createInstanceMetadatadata(t, "abcdef")) + expectedCreatedSpec.Spec.CreatedAt = frozenTime().Unix() + expectedCreatedSpec.Status.State = models.PendingState + + rm.Save( + gomock.AssignableToTypeOf(context.Background()), + gomock.Eq(expectedCreatedSpec), + ).Return( + createTestSpecWithMetadata("id1234", "default", testUID, createInstanceMetadatadata(t, "abcdef")), + nil, + ) + + em.Publish( + gomock.AssignableToTypeOf(context.Background()), + gomock.Eq(defaults.TopicMicroVMEvents), + gomock.Eq(&events.MicroVMSpecCreated{ + ID: "id1234", + Namespace: "default", + UID: testUID, + }), + ) + }, + }, } for _, tc := range testCases { @@ -556,6 +600,10 @@ func TestApp_GetAllMicroVM(t *testing.T) { } func createTestSpec(name, ns, uid string) *models.MicroVM { + return createTestSpecWithMetadata(name, ns, uid, map[string]string{}) +} + +func createTestSpecWithMetadata(name, ns, uid string, metadata map[string]string) *models.MicroVM { var vmid *models.VMID if uid == "" { @@ -600,9 +648,24 @@ func createTestSpec(name, ns, uid string) *models.MicroVM { }, Size: 20000, }, + Metadata: metadata, CreatedAt: 0, UpdatedAt: 0, DeletedAt: 0, }, } } + +func createInstanceMetadatadata(t *testing.T, instanceID string) map[string]string { + RegisterTestingT(t) + + instanceData := instance.New(instance.WithInstanceID(instanceID)) + data, err := yaml.Marshal(instanceData) + Expect(err).NotTo(HaveOccurred()) + + instanceDataStr := base64.StdEncoding.EncodeToString(data) + + return map[string]string{ + "meta-data": instanceDataStr, + } +} diff --git a/core/application/commands.go b/core/application/commands.go index 95c7c8a2..5061590a 100644 --- a/core/application/commands.go +++ b/core/application/commands.go @@ -2,9 +2,15 @@ package application import ( "context" + "encoding/base64" "fmt" + "github.com/sirupsen/logrus" + "sigs.k8s.io/yaml" + "github.com/weaveworks/flintlock/api/events" + "github.com/weaveworks/flintlock/client/cloudinit" + "github.com/weaveworks/flintlock/client/cloudinit/instance" coreerrs "github.com/weaveworks/flintlock/core/errors" "github.com/weaveworks/flintlock/core/models" "github.com/weaveworks/flintlock/core/ports" @@ -40,6 +46,7 @@ func (a *app) CreateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M } mvm.ID.SetUID(uid) + logger = logger.WithField("vmid", mvm.ID) foundMvm, err := a.ports.Repo.Get(ctx, ports.RepositoryGetOptions{ Name: mvm.ID.Name(), @@ -60,6 +67,11 @@ func (a *app) CreateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M } } + err = a.addInstanceData(mvm, logger) + if err != nil { + return nil, fmt.Errorf("adding instance data: %w", err) + } + // Set the timestamp when the VMspec was created. mvm.Spec.CreatedAt = a.ports.Clock().Unix() mvm.Status.State = models.PendingState @@ -122,3 +134,41 @@ func (a *app) DeleteMicroVM(ctx context.Context, uid string) error { return nil } + +func (a *app) addInstanceData(vm *models.MicroVM, logger *logrus.Entry) error { + instanceData := instance.New() + + meta := vm.Spec.Metadata[cloudinit.InstanceDataKey] + if meta != "" { + logger.Info("Instance metadata exists") + + data, err := base64.StdEncoding.DecodeString(meta) + if err != nil { + return fmt.Errorf("decoding existing instance metadata: %w", err) + } + + err = yaml.Unmarshal(data, &instanceData) + if err != nil { + return fmt.Errorf("unmarshalling exists instance metadata: %w", err) + } + } + + existingInstanceID := instanceData[instance.InstanceIDKey] + if existingInstanceID != "" { + logger.Infof("Instance id already set in meta-data: %s", existingInstanceID) + + return nil + } + + logger.Infof("Setting instance_id in meta-data: %s", vm.ID.UID()) + instanceData[instance.InstanceIDKey] = vm.ID.UID() + + updatedData, err := yaml.Marshal(&instanceData) + if err != nil { + return fmt.Errorf("marshalling updated instance data: %w", err) + } + + vm.Spec.Metadata[cloudinit.InstanceDataKey] = base64.StdEncoding.EncodeToString(updatedData) + + return nil +} diff --git a/go.mod b/go.mod index 60ab0a23..6b005ad4 100644 --- a/go.mod +++ b/go.mod @@ -117,4 +117,5 @@ require ( gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 0f7b9fa2..5c86b0b4 100644 --- a/go.sum +++ b/go.sum @@ -1620,4 +1620,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/infrastructure/firecracker/config.go b/infrastructure/firecracker/config.go index 3f40db54..f29ed340 100644 --- a/infrastructure/firecracker/config.go +++ b/infrastructure/firecracker/config.go @@ -7,7 +7,7 @@ import ( "github.com/firecracker-microvm/firecracker-go-sdk" "gopkg.in/yaml.v3" - "github.com/weaveworks/flintlock/client/cloudinit" + cinetwork "github.com/weaveworks/flintlock/client/cloudinit/network" "github.com/weaveworks/flintlock/core/errors" "github.com/weaveworks/flintlock/core/models" "github.com/weaveworks/flintlock/internal/config" @@ -192,9 +192,9 @@ func createNetworkIface(iface *models.NetworkInterface, status *models.NetworkIn } func generateNetworkConfig(vm *models.MicroVM) (string, error) { - network := &cloudinit.Network{ + network := &cinetwork.Network{ Version: cloudInitNetVersion, - Ethernet: map[string]cloudinit.Ethernet{}, + Ethernet: map[string]cinetwork.Ethernet{}, } for i := range vm.Spec.NetworkInterfaces { @@ -207,8 +207,8 @@ func generateNetworkConfig(vm *models.MicroVM) (string, error) { macAddress := getMacAddress(&iface, status) - eth := &cloudinit.Ethernet{ - Match: cloudinit.Match{}, + eth := &cinetwork.Ethernet{ + Match: cinetwork.Match{}, DHCP4: firecracker.Bool(true), DHCP6: firecracker.Bool(true), } @@ -236,7 +236,7 @@ func generateNetworkConfig(vm *models.MicroVM) (string, error) { return base64.StdEncoding.EncodeToString(nd), nil } -func configureStaticEthernet(iface *models.NetworkInterface, eth *cloudinit.Ethernet) error { +func configureStaticEthernet(iface *models.NetworkInterface, eth *cinetwork.Ethernet) error { eth.Addresses = []string{string(iface.StaticAddress.Address)} if iface.StaticAddress.Gateway != nil { @@ -258,7 +258,7 @@ func configureStaticEthernet(iface *models.NetworkInterface, eth *cloudinit.Ethe } if len(iface.StaticAddress.Nameservers) > 0 { - eth.Nameservers = cloudinit.Nameservers{ + eth.Nameservers = cinetwork.Nameservers{ Addresses: []string{}, } diff --git a/infrastructure/grpc/convert.go b/infrastructure/grpc/convert.go index 15d55695..2fc21b66 100644 --- a/infrastructure/grpc/convert.go +++ b/infrastructure/grpc/convert.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/weaveworks/flintlock/api/types" + "github.com/weaveworks/flintlock/client/cloudinit/instance" "github.com/weaveworks/flintlock/core/models" "github.com/weaveworks/flintlock/pkg/defaults" "github.com/weaveworks/flintlock/pkg/ptr" @@ -32,6 +33,7 @@ func convertMicroVMToModel(spec *types.MicroVMSpec) (*models.MicroVM, error) { }, VCPU: int64(spec.Vcpu), MemoryInMb: int64(spec.MemoryInMb), + Metadata: instance.New(), }, }