diff --git a/.golangci.yml b/.golangci.yml index 124fa43d..c4356cda 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,6 +21,9 @@ linters-settings: go: "1.16" stylecheck: go: "1.16" + cyclop: + max-complexity: 12 + skip-tests: true linters: enable-all: true diff --git a/Makefile b/Makefile index 97c8c625..5ee92d68 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ PROTOC_GEN_GO := $(TOOLS_BIN_DIR)/protoc-gen-go PROTOC_GEN_GO_GRPC := $(TOOLS_BIN_DIR)/protoc-gen-go-grpc PROTO_GEN_GRPC_GW := $(TOOLS_BIN_DIR)/protoc-gen-grpc-gateway PROTO_GEN_GRPC_OAPI := $(TOOLS_BIN_DIR)/protoc-gen-openapiv2 +WIRE := $(TOOLS_BIN_DIR)/wire .DEFAULT_GOAL := help @@ -57,6 +58,7 @@ generate: $(BUF) $(MOCKGEN) ## Generate code generate: ## Generate code $(MAKE) generate-go $(MAKE) generate-proto + $(MAKE) generate-di .PHONY: generate-go generate-go: $(MOCKGEN) ## Generate Go Code @@ -65,7 +67,11 @@ generate-go: $(MOCKGEN) ## Generate Go Code .PHONY: generate-proto ## Generate protobuf/grpc code generate-proto: $(BUF) $(PROTOC_GEN_GO) $(PROTOC_GEN_GO_GRPC) $(PROTO_GEN_GRPC_GW) $(PROTO_GEN_GRPC_OAPI) $(BUF) generate - + +.PHONY: generate-di ## Generate the dependency injection code +generate-di: $(WIRE) + $(WIRE) gen github.com/weaveworks/reignite/internal/inject + ##@ Linting .PHONY: lint @@ -114,6 +120,9 @@ $(PROTO_GEN_GRPC_GW): $(TOOLS_DIR)/go.mod $(PROTO_GEN_GRPC_OAPI): $(TOOLS_DIR)/go.mod cd $(TOOLS_DIR); go build -tags=tools -o $(subst hack/tools/,,$@) github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 +$(WIRE): $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR); go build -tags=tools -o $(subst hack/tools/,,$@) github.com/google/wire/cmd/wire + BUF_TARGET := buf-Linux-x86_64.tar.gz ifeq ($(OS), darwin) diff --git a/api/services/microvm/v1alpha1/microvms.pb.go b/api/services/microvm/v1alpha1/microvms.pb.go index 0cac5967..7122f8d1 100644 --- a/api/services/microvm/v1alpha1/microvms.pb.go +++ b/api/services/microvm/v1alpha1/microvms.pb.go @@ -7,6 +7,9 @@ package v1alpha1 import ( + reflect "reflect" + sync "sync" + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" types "github.com/weaveworks/reignite/api/types" _ "google.golang.org/genproto/googleapis/api/annotations" @@ -15,8 +18,6 @@ import ( anypb "google.golang.org/protobuf/types/known/anypb" emptypb "google.golang.org/protobuf/types/known/emptypb" fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" - reflect "reflect" - sync "sync" ) const ( @@ -695,24 +696,27 @@ func file_services_microvm_v1alpha1_microvms_proto_rawDescGZIP() []byte { return file_services_microvm_v1alpha1_microvms_proto_rawDescData } -var file_services_microvm_v1alpha1_microvms_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_services_microvm_v1alpha1_microvms_proto_goTypes = []interface{}{ - (*CreateMicroVMRequest)(nil), // 0: microvm.services.api.v1alpha1.CreateMicroVMRequest - (*CreateMicroVMResponse)(nil), // 1: microvm.services.api.v1alpha1.CreateMicroVMResponse - (*DeleteMicroVMRequest)(nil), // 2: microvm.services.api.v1alpha1.DeleteMicroVMRequest - (*UpdateMicroVMRequest)(nil), // 3: microvm.services.api.v1alpha1.UpdateMicroVMRequest - (*UpdateMicroVMResponse)(nil), // 4: microvm.services.api.v1alpha1.UpdateMicroVMResponse - (*GetMicroVMRequest)(nil), // 5: microvm.services.api.v1alpha1.GetMicroVMRequest - (*GetMicroVMResponse)(nil), // 6: microvm.services.api.v1alpha1.GetMicroVMResponse - (*ListMicroVMsRequest)(nil), // 7: microvm.services.api.v1alpha1.ListMicroVMsRequest - (*ListMicroVMsResponse)(nil), // 8: microvm.services.api.v1alpha1.ListMicroVMsResponse - (*ListMessage)(nil), // 9: microvm.services.api.v1alpha1.ListMessage - nil, // 10: microvm.services.api.v1alpha1.CreateMicroVMRequest.MetadataEntry - (*types.MicroVMSpec)(nil), // 11: reignite.types.MicroVMSpec - (*fieldmaskpb.FieldMask)(nil), // 12: google.protobuf.FieldMask - (*anypb.Any)(nil), // 13: google.protobuf.Any - (*emptypb.Empty)(nil), // 14: google.protobuf.Empty -} +var ( + file_services_microvm_v1alpha1_microvms_proto_msgTypes = make([]protoimpl.MessageInfo, 11) + file_services_microvm_v1alpha1_microvms_proto_goTypes = []interface{}{ + (*CreateMicroVMRequest)(nil), // 0: microvm.services.api.v1alpha1.CreateMicroVMRequest + (*CreateMicroVMResponse)(nil), // 1: microvm.services.api.v1alpha1.CreateMicroVMResponse + (*DeleteMicroVMRequest)(nil), // 2: microvm.services.api.v1alpha1.DeleteMicroVMRequest + (*UpdateMicroVMRequest)(nil), // 3: microvm.services.api.v1alpha1.UpdateMicroVMRequest + (*UpdateMicroVMResponse)(nil), // 4: microvm.services.api.v1alpha1.UpdateMicroVMResponse + (*GetMicroVMRequest)(nil), // 5: microvm.services.api.v1alpha1.GetMicroVMRequest + (*GetMicroVMResponse)(nil), // 6: microvm.services.api.v1alpha1.GetMicroVMResponse + (*ListMicroVMsRequest)(nil), // 7: microvm.services.api.v1alpha1.ListMicroVMsRequest + (*ListMicroVMsResponse)(nil), // 8: microvm.services.api.v1alpha1.ListMicroVMsResponse + (*ListMessage)(nil), // 9: microvm.services.api.v1alpha1.ListMessage + nil, // 10: microvm.services.api.v1alpha1.CreateMicroVMRequest.MetadataEntry + (*types.MicroVMSpec)(nil), // 11: reignite.types.MicroVMSpec + (*fieldmaskpb.FieldMask)(nil), // 12: google.protobuf.FieldMask + (*anypb.Any)(nil), // 13: google.protobuf.Any + (*emptypb.Empty)(nil), // 14: google.protobuf.Empty + } +) + var file_services_microvm_v1alpha1_microvms_proto_depIdxs = []int32{ 11, // 0: microvm.services.api.v1alpha1.CreateMicroVMRequest.microvm:type_name -> reignite.types.MicroVMSpec 10, // 1: microvm.services.api.v1alpha1.CreateMicroVMRequest.metadata:type_name -> microvm.services.api.v1alpha1.CreateMicroVMRequest.MetadataEntry diff --git a/api/services/microvm/v1alpha1/microvms.pb.gw.go b/api/services/microvm/v1alpha1/microvms.pb.gw.go index 264b3ff9..3ff1b00e 100644 --- a/api/services/microvm/v1alpha1/microvms.pb.gw.go +++ b/api/services/microvm/v1alpha1/microvms.pb.gw.go @@ -24,17 +24,17 @@ import ( ) // Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - var ( - filter_MicroVM_CreateMicroVM_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + _ codes.Code + _ io.Reader + _ status.Status + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join ) +var filter_MicroVM_CreateMicroVM_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + func request_MicroVM_CreateMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, client MicroVMClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateMicroVMRequest var metadata runtime.ServerMetadata @@ -48,7 +48,6 @@ func request_MicroVM_CreateMicroVM_0(ctx context.Context, marshaler runtime.Mars msg, err := client.CreateMicroVM(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err - } func local_request_MicroVM_CreateMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, server MicroVMServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -64,12 +63,9 @@ func local_request_MicroVM_CreateMicroVM_0(ctx context.Context, marshaler runtim msg, err := server.CreateMicroVM(ctx, &protoReq) return msg, metadata, err - } -var ( - filter_MicroVM_UpdateMicroVM_0 = &utilities.DoubleArray{Encoding: map[string]int{"microvm": 0, "namespace": 1, "id": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 2, 3, 4}} -) +var filter_MicroVM_UpdateMicroVM_0 = &utilities.DoubleArray{Encoding: map[string]int{"microvm": 0, "namespace": 1, "id": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 2, 3, 4}} func request_MicroVM_UpdateMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, client MicroVMClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UpdateMicroVMRequest @@ -111,7 +107,6 @@ func request_MicroVM_UpdateMicroVM_0(ctx context.Context, marshaler runtime.Mars msg, err := client.UpdateMicroVM(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err - } func local_request_MicroVM_UpdateMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, server MicroVMServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -154,7 +149,6 @@ func local_request_MicroVM_UpdateMicroVM_0(ctx context.Context, marshaler runtim msg, err := server.UpdateMicroVM(ctx, &protoReq) return msg, metadata, err - } func request_MicroVM_DeleteMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, client MicroVMClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -190,7 +184,6 @@ func request_MicroVM_DeleteMicroVM_0(ctx context.Context, marshaler runtime.Mars msg, err := client.DeleteMicroVM(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err - } func local_request_MicroVM_DeleteMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, server MicroVMServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -226,7 +219,6 @@ func local_request_MicroVM_DeleteMicroVM_0(ctx context.Context, marshaler runtim msg, err := server.DeleteMicroVM(ctx, &protoReq) return msg, metadata, err - } func request_MicroVM_GetMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, client MicroVMClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -262,7 +254,6 @@ func request_MicroVM_GetMicroVM_0(ctx context.Context, marshaler runtime.Marshal msg, err := client.GetMicroVM(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err - } func local_request_MicroVM_GetMicroVM_0(ctx context.Context, marshaler runtime.Marshaler, server MicroVMServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -298,7 +289,6 @@ func local_request_MicroVM_GetMicroVM_0(ctx context.Context, marshaler runtime.M msg, err := server.GetMicroVM(ctx, &protoReq) return msg, metadata, err - } func request_MicroVM_ListMicroVMs_0(ctx context.Context, marshaler runtime.Marshaler, client MicroVMClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -324,7 +314,6 @@ func request_MicroVM_ListMicroVMs_0(ctx context.Context, marshaler runtime.Marsh msg, err := client.ListMicroVMs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err - } func local_request_MicroVM_ListMicroVMs_0(ctx context.Context, marshaler runtime.Marshaler, server MicroVMServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -350,7 +339,6 @@ func local_request_MicroVM_ListMicroVMs_0(ctx context.Context, marshaler runtime msg, err := server.ListMicroVMs(ctx, &protoReq) return msg, metadata, err - } func request_MicroVM_ListMicroVMsStream_0(ctx context.Context, marshaler runtime.Marshaler, client MicroVMClient, req *http.Request, pathParams map[string]string) (MicroVM_ListMicroVMsStreamClient, runtime.ServerMetadata, error) { @@ -375,7 +363,6 @@ func request_MicroVM_ListMicroVMsStream_0(ctx context.Context, marshaler runtime } metadata.HeaderMD = header return stream, metadata, nil - } // RegisterMicroVMHandlerServer registers the http handlers for service MicroVM to "mux". @@ -383,7 +370,6 @@ func request_MicroVM_ListMicroVMsStream_0(ctx context.Context, marshaler runtime // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterMicroVMHandlerFromEndpoint instead. func RegisterMicroVMHandlerServer(ctx context.Context, mux *runtime.ServeMux, server MicroVMServer) error { - mux.Handle("POST", pattern_MicroVM_CreateMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -404,7 +390,6 @@ func RegisterMicroVMHandlerServer(ctx context.Context, mux *runtime.ServeMux, se } forward_MicroVM_CreateMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("PUT", pattern_MicroVM_UpdateMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -427,7 +412,6 @@ func RegisterMicroVMHandlerServer(ctx context.Context, mux *runtime.ServeMux, se } forward_MicroVM_UpdateMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("DELETE", pattern_MicroVM_DeleteMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -450,7 +434,6 @@ func RegisterMicroVMHandlerServer(ctx context.Context, mux *runtime.ServeMux, se } forward_MicroVM_DeleteMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("GET", pattern_MicroVM_GetMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -473,7 +456,6 @@ func RegisterMicroVMHandlerServer(ctx context.Context, mux *runtime.ServeMux, se } forward_MicroVM_GetMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("GET", pattern_MicroVM_ListMicroVMs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -496,7 +478,6 @@ func RegisterMicroVMHandlerServer(ctx context.Context, mux *runtime.ServeMux, se } forward_MicroVM_ListMicroVMs_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("POST", pattern_MicroVM_ListMicroVMsStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -546,7 +527,6 @@ func RegisterMicroVMHandler(ctx context.Context, mux *runtime.ServeMux, conn *gr // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // "MicroVMClient" to call the correct interceptors. func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, client MicroVMClient) error { - mux.Handle("POST", pattern_MicroVM_CreateMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -564,7 +544,6 @@ func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl } forward_MicroVM_CreateMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("PUT", pattern_MicroVM_UpdateMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -584,7 +563,6 @@ func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl } forward_MicroVM_UpdateMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("DELETE", pattern_MicroVM_DeleteMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -604,7 +582,6 @@ func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl } forward_MicroVM_DeleteMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("GET", pattern_MicroVM_GetMicroVM_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -624,7 +601,6 @@ func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl } forward_MicroVM_GetMicroVM_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("GET", pattern_MicroVM_ListMicroVMs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -644,7 +620,6 @@ func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl } forward_MicroVM_ListMicroVMs_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) mux.Handle("POST", pattern_MicroVM_ListMicroVMsStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -664,7 +639,6 @@ func RegisterMicroVMHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl } forward_MicroVM_ListMicroVMsStream_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) - }) return nil diff --git a/api/services/microvm/v1alpha1/microvms.proto b/api/services/microvm/v1alpha1/microvms.proto index 560d502d..90b99c3f 100644 --- a/api/services/microvm/v1alpha1/microvms.proto +++ b/api/services/microvm/v1alpha1/microvms.proto @@ -13,12 +13,12 @@ option go_package = "github.com/weaveworks/reignite/api/services/microvm/v1alpha option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "Reignite MicroVM API", - version: "0.1"; - description: "The Reignite MicroVM API handles operations for provisioning and managing microvms"; - }; - consumes: "application/json"; - produces: "application/json"; + title: "Reignite MicroVM API" + version: "0.1" + description: "The Reignite MicroVM API handles operations for provisioning and managing microvms" + } + consumes: "application/json" + produces: "application/json" }; // MicroVM providers a service to create and manage the lifecycle of microvms. diff --git a/api/services/microvm/v1alpha1/microvms.swagger.json b/api/services/microvm/v1alpha1/microvms.swagger.json index 23d8186e..9c6ce5c9 100644 --- a/api/services/microvm/v1alpha1/microvms.swagger.json +++ b/api/services/microvm/v1alpha1/microvms.swagger.json @@ -179,14 +179,14 @@ } }, "definitions": { - "HostPathVolumeSourceHostPathType": { + "NetworkInterfaceIfaceType": { "type": "string", "enum": [ - "UNKNOWN", - "RAW_FILE" + "MACVTAP", + "TAP" ], - "default": "UNKNOWN", - "description": " - RAW_FILE: RAW_FILE represents a file on the host to use as a source for a volume. It should be a raw fs." + "default": "MACVTAP", + "description": " - MACVTAP: MACVTAP represents a network interface that is macvtap.\n - TAP: TAP represents a network interface that is a tap." }, "protobufAny": { "type": "object", @@ -221,19 +221,19 @@ } } }, - "typesHostPathVolumeSource": { + "typesInitrd": { "type": "object", "properties": { - "path": { + "image": { "type": "string", - "description": "Path on the host of the file/device to use as a source for a volume." + "description": "Image is the container image to use." }, - "type": { - "$ref": "#/definitions/HostPathVolumeSourceHostPathType", - "description": "Type is the type of file/device on the host." + "filename": { + "type": "string", + "title": "Filename is used to specify the name of the kernel file\nin the Image. Defaults to initrd" } }, - "description": "HostPathVolumeSource represents the details of a volume coming from a file/device on the host." + "description": "Initrd represents the configuration for the initial ramdisk." }, "typesKernel": { "type": "object", @@ -249,6 +249,10 @@ "filename": { "type": "string", "description": "Filename is used to specify the name of the kernel file\nin the Image." + }, + "addNetworkConfig": { + "type": "boolean", + "description": "AddNetworkConfig if set to true indicates that the network-config kernel argument should be generated." } }, "description": "Kernel represents the configuration for a kernel." @@ -285,9 +289,9 @@ "$ref": "#/definitions/typesKernel", "description": "Kernel is the details of the kernel to use ." }, - "initrdImage": { - "type": "string", - "description": "Initrd_image is the container image to use for the initial ramdisk." + "initrd": { + "$ref": "#/definitions/typesInitrd", + "description": "Initrd is the optional details of the initial ramdisk." }, "volumes": { "type": "array", @@ -303,6 +307,13 @@ }, "description": "Interfaces specifies the network interfaces to be attached to the microvm." }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Metadata allows you to specify data to be added to the metadata service. The key is the name\nof the metadata item and the value is the base64 encoded contents of the metadata." + }, "createdAt": { "type": "string", "format": "date-time", @@ -319,17 +330,25 @@ "typesNetworkInterface": { "type": "object", "properties": { + "guestDeviceName": { + "type": "string", + "description": "GuestDeviceName is the name of the network interface to create in the microvm." + }, + "type": { + "$ref": "#/definitions/NetworkInterfaceIfaceType", + "description": "IfaceType specifies the type of network interface to create for use by the guest." + }, "allowMetadataReq": { "type": "boolean", "description": "AllowMetadataReq indicates if the network interface allows metadata requests." }, "guestMac": { "type": "string", - "title": "GuestMAC allows the specifying of a specifi MAC address to use for the interface. If\nnot supplied a autogenerated MAC address will be used.y" + "description": "GuestMAC allows the specifying of a specifi MAC address to use for the interface. If\nnot supplied a autogenerated MAC address will be used." }, - "guestDeviceName": { + "address": { "type": "string", - "title": "GuestDeviceName is the name of the network interface to create in the microvm. If this\nis not supplied than a device name will be assigned automatically" + "description": "Address is an optional IP address to assign to this interface. If not supplied then DHCP will be used." } } }, @@ -374,10 +393,6 @@ "containerSource": { "type": "string", "description": "Container is used to specify a source of a volume as a OCI container." - }, - "hostpathSource": { - "$ref": "#/definitions/typesHostPathVolumeSource", - "description": "HostPath is used to specify a source of a volume as a file/device on the host." } }, "description": "VolumeSource is the source of a volume. Based loosely on the volumes in Kubernetes Pod specs." diff --git a/api/services/microvm/v1alpha1/microvms_grpc.pb.go b/api/services/microvm/v1alpha1/microvms_grpc.pb.go index 0219d718..421b8aef 100644 --- a/api/services/microvm/v1alpha1/microvms_grpc.pb.go +++ b/api/services/microvm/v1alpha1/microvms_grpc.pb.go @@ -4,6 +4,7 @@ package v1alpha1 import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -125,24 +126,28 @@ type MicroVMServer interface { } // UnimplementedMicroVMServer should be embedded to have forward compatible implementations. -type UnimplementedMicroVMServer struct { -} +type UnimplementedMicroVMServer struct{} func (UnimplementedMicroVMServer) CreateMicroVM(context.Context, *CreateMicroVMRequest) (*CreateMicroVMResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateMicroVM not implemented") } + func (UnimplementedMicroVMServer) UpdateMicroVM(context.Context, *UpdateMicroVMRequest) (*UpdateMicroVMResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateMicroVM not implemented") } + func (UnimplementedMicroVMServer) DeleteMicroVM(context.Context, *DeleteMicroVMRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteMicroVM not implemented") } + func (UnimplementedMicroVMServer) GetMicroVM(context.Context, *GetMicroVMRequest) (*GetMicroVMResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetMicroVM not implemented") } + func (UnimplementedMicroVMServer) ListMicroVMs(context.Context, *ListMicroVMsRequest) (*ListMicroVMsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListMicroVMs not implemented") } + func (UnimplementedMicroVMServer) ListMicroVMsStream(*ListMicroVMsRequest, MicroVM_ListMicroVMsStreamServer) error { return status.Errorf(codes.Unimplemented, "method ListMicroVMsStream not implemented") } diff --git a/api/types/microvm.pb.go b/api/types/microvm.pb.go index b7fef614..853c86c3 100644 --- a/api/types/microvm.pb.go +++ b/api/types/microvm.pb.go @@ -7,11 +7,12 @@ package types import ( + reflect "reflect" + sync "sync" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" ) const ( @@ -21,51 +22,52 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type HostPathVolumeSource_HostPathType int32 +type NetworkInterface_IfaceType int32 const ( - HostPathVolumeSource_UNKNOWN HostPathVolumeSource_HostPathType = 0 - // RAW_FILE represents a file on the host to use as a source for a volume. It should be a raw fs. - HostPathVolumeSource_RAW_FILE HostPathVolumeSource_HostPathType = 1 + // MACVTAP represents a network interface that is macvtap. + NetworkInterface_MACVTAP NetworkInterface_IfaceType = 0 + // TAP represents a network interface that is a tap. + NetworkInterface_TAP NetworkInterface_IfaceType = 1 ) -// Enum value maps for HostPathVolumeSource_HostPathType. +// Enum value maps for NetworkInterface_IfaceType. var ( - HostPathVolumeSource_HostPathType_name = map[int32]string{ - 0: "UNKNOWN", - 1: "RAW_FILE", + NetworkInterface_IfaceType_name = map[int32]string{ + 0: "MACVTAP", + 1: "TAP", } - HostPathVolumeSource_HostPathType_value = map[string]int32{ - "UNKNOWN": 0, - "RAW_FILE": 1, + NetworkInterface_IfaceType_value = map[string]int32{ + "MACVTAP": 0, + "TAP": 1, } ) -func (x HostPathVolumeSource_HostPathType) Enum() *HostPathVolumeSource_HostPathType { - p := new(HostPathVolumeSource_HostPathType) +func (x NetworkInterface_IfaceType) Enum() *NetworkInterface_IfaceType { + p := new(NetworkInterface_IfaceType) *p = x return p } -func (x HostPathVolumeSource_HostPathType) String() string { +func (x NetworkInterface_IfaceType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } -func (HostPathVolumeSource_HostPathType) Descriptor() protoreflect.EnumDescriptor { +func (NetworkInterface_IfaceType) Descriptor() protoreflect.EnumDescriptor { return file_types_microvm_proto_enumTypes[0].Descriptor() } -func (HostPathVolumeSource_HostPathType) Type() protoreflect.EnumType { +func (NetworkInterface_IfaceType) Type() protoreflect.EnumType { return &file_types_microvm_proto_enumTypes[0] } -func (x HostPathVolumeSource_HostPathType) Number() protoreflect.EnumNumber { +func (x NetworkInterface_IfaceType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use HostPathVolumeSource_HostPathType.Descriptor instead. -func (HostPathVolumeSource_HostPathType) EnumDescriptor() ([]byte, []int) { - return file_types_microvm_proto_rawDescGZIP(), []int{6, 0} +// Deprecated: Use NetworkInterface_IfaceType.Descriptor instead. +func (NetworkInterface_IfaceType) EnumDescriptor() ([]byte, []int) { + return file_types_microvm_proto_rawDescGZIP(), []int{3, 0} } // MicroVMSpec represents the specification for a microvm. @@ -87,17 +89,20 @@ type MicroVMSpec struct { MemoryInMb int32 `protobuf:"varint,5,opt,name=memory_in_mb,json=memoryInMb,proto3" json:"memory_in_mb,omitempty"` // Kernel is the details of the kernel to use . Kernel *Kernel `protobuf:"bytes,6,opt,name=kernel,proto3" json:"kernel,omitempty"` - // Initrd_image is the container image to use for the initial ramdisk. - InitrdImage *string `protobuf:"bytes,7,opt,name=initrd_image,json=initrdImage,proto3,oneof" json:"initrd_image,omitempty"` + // Initrd is the optional details of the initial ramdisk. + Initrd *Initrd `protobuf:"bytes,7,opt,name=initrd,proto3,oneof" json:"initrd,omitempty"` // Volumes specifies the volumes to be attached to the microvm. There must be // at lease one volume that will act as the root volume. Volumes []*Volume `protobuf:"bytes,8,rep,name=volumes,proto3" json:"volumes,omitempty"` // Interfaces specifies the network interfaces to be attached to the microvm. Interfaces []*NetworkInterface `protobuf:"bytes,9,rep,name=interfaces,proto3" json:"interfaces,omitempty"` + // Metadata allows you to specify data to be added to the metadata service. The key is the name + // of the metadata item and the value is the base64 encoded contents of the metadata. + Metadata map[string]string `protobuf:"bytes,10,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // CreatedAt indicates the time the microvm was created at. - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // UpdatedAt indicates the time the microvm was last updated. - UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` } func (x *MicroVMSpec) Reset() { @@ -174,11 +179,11 @@ func (x *MicroVMSpec) GetKernel() *Kernel { return nil } -func (x *MicroVMSpec) GetInitrdImage() string { - if x != nil && x.InitrdImage != nil { - return *x.InitrdImage +func (x *MicroVMSpec) GetInitrd() *Initrd { + if x != nil { + return x.Initrd } - return "" + return nil } func (x *MicroVMSpec) GetVolumes() []*Volume { @@ -195,6 +200,13 @@ func (x *MicroVMSpec) GetInterfaces() []*NetworkInterface { return nil } +func (x *MicroVMSpec) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + func (x *MicroVMSpec) GetCreatedAt() *timestamppb.Timestamp { if x != nil { return x.CreatedAt @@ -222,6 +234,8 @@ type Kernel struct { // Filename is used to specify the name of the kernel file // in the Image. Filename *string `protobuf:"bytes,3,opt,name=filename,proto3,oneof" json:"filename,omitempty"` + // AddNetworkConfig if set to true indicates that the network-config kernel argument should be generated. + AddNetworkConfig bool `protobuf:"varint,4,opt,name=add_network_config,json=addNetworkConfig,proto3" json:"add_network_config,omitempty"` } func (x *Kernel) Reset() { @@ -277,25 +291,94 @@ func (x *Kernel) GetFilename() string { return "" } +func (x *Kernel) GetAddNetworkConfig() bool { + if x != nil { + return x.AddNetworkConfig + } + return false +} + +// Initrd represents the configuration for the initial ramdisk. +type Initrd struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Image is the container image to use. + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + // Filename is used to specify the name of the kernel file + // in the Image. Defaults to initrd + Filename *string `protobuf:"bytes,2,opt,name=filename,proto3,oneof" json:"filename,omitempty"` +} + +func (x *Initrd) Reset() { + *x = Initrd{} + if protoimpl.UnsafeEnabled { + mi := &file_types_microvm_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Initrd) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Initrd) ProtoMessage() {} + +func (x *Initrd) ProtoReflect() protoreflect.Message { + mi := &file_types_microvm_proto_msgTypes[2] + 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 Initrd.ProtoReflect.Descriptor instead. +func (*Initrd) Descriptor() ([]byte, []int) { + return file_types_microvm_proto_rawDescGZIP(), []int{2} +} + +func (x *Initrd) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *Initrd) GetFilename() string { + if x != nil && x.Filename != nil { + return *x.Filename + } + return "" +} + type NetworkInterface struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + // GuestDeviceName is the name of the network interface to create in the microvm. + GuestDeviceName string `protobuf:"bytes,1,opt,name=guest_device_name,json=guestDeviceName,proto3" json:"guest_device_name,omitempty"` + // IfaceType specifies the type of network interface to create for use by the guest. + Type NetworkInterface_IfaceType `protobuf:"varint,2,opt,name=type,proto3,enum=reignite.types.NetworkInterface_IfaceType" json:"type,omitempty"` // AllowMetadataReq indicates if the network interface allows metadata requests. - AllowMetadataReq bool `protobuf:"varint,1,opt,name=allow_metadata_req,json=allowMetadataReq,proto3" json:"allow_metadata_req,omitempty"` + AllowMetadataReq bool `protobuf:"varint,3,opt,name=allow_metadata_req,json=allowMetadataReq,proto3" json:"allow_metadata_req,omitempty"` // GuestMAC allows the specifying of a specifi MAC address to use for the interface. If - // not supplied a autogenerated MAC address will be used.y - GuestMac *string `protobuf:"bytes,2,opt,name=guest_mac,json=guestMac,proto3,oneof" json:"guest_mac,omitempty"` - // GuestDeviceName is the name of the network interface to create in the microvm. If this - // is not supplied than a device name will be assigned automatically - GuestDeviceName *string `protobuf:"bytes,3,opt,name=guest_device_name,json=guestDeviceName,proto3,oneof" json:"guest_device_name,omitempty"` + // not supplied a autogenerated MAC address will be used. + GuestMac *string `protobuf:"bytes,4,opt,name=guest_mac,json=guestMac,proto3,oneof" json:"guest_mac,omitempty"` + // Address is an optional IP address to assign to this interface. If not supplied then DHCP will be used. + Address *string `protobuf:"bytes,5,opt,name=address,proto3,oneof" json:"address,omitempty"` } func (x *NetworkInterface) Reset() { *x = NetworkInterface{} if protoimpl.UnsafeEnabled { - mi := &file_types_microvm_proto_msgTypes[2] + mi := &file_types_microvm_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -308,7 +391,7 @@ func (x *NetworkInterface) String() string { func (*NetworkInterface) ProtoMessage() {} func (x *NetworkInterface) ProtoReflect() protoreflect.Message { - mi := &file_types_microvm_proto_msgTypes[2] + mi := &file_types_microvm_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -321,7 +404,21 @@ func (x *NetworkInterface) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkInterface.ProtoReflect.Descriptor instead. func (*NetworkInterface) Descriptor() ([]byte, []int) { - return file_types_microvm_proto_rawDescGZIP(), []int{2} + return file_types_microvm_proto_rawDescGZIP(), []int{3} +} + +func (x *NetworkInterface) GetGuestDeviceName() string { + if x != nil { + return x.GuestDeviceName + } + return "" +} + +func (x *NetworkInterface) GetType() NetworkInterface_IfaceType { + if x != nil { + return x.Type + } + return NetworkInterface_MACVTAP } func (x *NetworkInterface) GetAllowMetadataReq() bool { @@ -338,9 +435,9 @@ func (x *NetworkInterface) GetGuestMac() string { return "" } -func (x *NetworkInterface) GetGuestDeviceName() string { - if x != nil && x.GuestDeviceName != nil { - return *x.GuestDeviceName +func (x *NetworkInterface) GetAddress() string { + if x != nil && x.Address != nil { + return *x.Address } return "" } @@ -371,7 +468,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} if protoimpl.UnsafeEnabled { - mi := &file_types_microvm_proto_msgTypes[3] + mi := &file_types_microvm_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -384,7 +481,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_types_microvm_proto_msgTypes[3] + mi := &file_types_microvm_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -397,7 +494,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_types_microvm_proto_rawDescGZIP(), []int{3} + return file_types_microvm_proto_rawDescGZIP(), []int{4} } func (x *Volume) GetId() string { @@ -456,15 +553,13 @@ type VolumeSource struct { unknownFields protoimpl.UnknownFields // Container is used to specify a source of a volume as a OCI container. - ContainerSource *string `protobuf:"bytes,1,opt,name=container_source,json=containerSource,proto3,oneof" json:"container_source,omitempty"` - // HostPath is used to specify a source of a volume as a file/device on the host. - HostpathSource *HostPathVolumeSource `protobuf:"bytes,2,opt,name=hostpath_source,json=hostpathSource,proto3,oneof" json:"hostpath_source,omitempty"` //TODO: add CSI + ContainerSource *string `protobuf:"bytes,1,opt,name=container_source,json=containerSource,proto3,oneof" json:"container_source,omitempty"` // TODO: add CSI } func (x *VolumeSource) Reset() { *x = VolumeSource{} if protoimpl.UnsafeEnabled { - mi := &file_types_microvm_proto_msgTypes[4] + mi := &file_types_microvm_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -477,7 +572,7 @@ func (x *VolumeSource) String() string { func (*VolumeSource) ProtoMessage() {} func (x *VolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_types_microvm_proto_msgTypes[4] + mi := &file_types_microvm_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -490,7 +585,7 @@ func (x *VolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeSource.ProtoReflect.Descriptor instead. func (*VolumeSource) Descriptor() ([]byte, []int) { - return file_types_microvm_proto_rawDescGZIP(), []int{4} + return file_types_microvm_proto_rawDescGZIP(), []int{5} } func (x *VolumeSource) GetContainerSource() string { @@ -500,13 +595,6 @@ func (x *VolumeSource) GetContainerSource() string { return "" } -func (x *VolumeSource) GetHostpathSource() *HostPathVolumeSource { - if x != nil { - return x.HostpathSource - } - return nil -} - // ContainerVolumeSource represents the details of a volume coming from a OCI image. type ContainerVolumeSource struct { state protoimpl.MessageState @@ -520,7 +608,7 @@ type ContainerVolumeSource struct { func (x *ContainerVolumeSource) Reset() { *x = ContainerVolumeSource{} if protoimpl.UnsafeEnabled { - mi := &file_types_microvm_proto_msgTypes[5] + mi := &file_types_microvm_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -533,7 +621,7 @@ func (x *ContainerVolumeSource) String() string { func (*ContainerVolumeSource) ProtoMessage() {} func (x *ContainerVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_types_microvm_proto_msgTypes[5] + mi := &file_types_microvm_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -546,7 +634,7 @@ func (x *ContainerVolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerVolumeSource.ProtoReflect.Descriptor instead. func (*ContainerVolumeSource) Descriptor() ([]byte, []int) { - return file_types_microvm_proto_rawDescGZIP(), []int{5} + return file_types_microvm_proto_rawDescGZIP(), []int{6} } func (x *ContainerVolumeSource) GetImage() string { @@ -556,64 +644,6 @@ func (x *ContainerVolumeSource) GetImage() string { return "" } -// HostPathVolumeSource represents the details of a volume coming from a file/device on the host. -type HostPathVolumeSource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Path on the host of the file/device to use as a source for a volume. - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - // Type is the type of file/device on the host. - Type HostPathVolumeSource_HostPathType `protobuf:"varint,2,opt,name=type,proto3,enum=reignite.types.HostPathVolumeSource_HostPathType" json:"type,omitempty"` -} - -func (x *HostPathVolumeSource) Reset() { - *x = HostPathVolumeSource{} - if protoimpl.UnsafeEnabled { - mi := &file_types_microvm_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *HostPathVolumeSource) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HostPathVolumeSource) ProtoMessage() {} - -func (x *HostPathVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_types_microvm_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 HostPathVolumeSource.ProtoReflect.Descriptor instead. -func (*HostPathVolumeSource) Descriptor() ([]byte, []int) { - return file_types_microvm_proto_rawDescGZIP(), []int{6} -} - -func (x *HostPathVolumeSource) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *HostPathVolumeSource) GetType() HostPathVolumeSource_HostPathType { - if x != nil { - return x.Type - } - return HostPathVolumeSource_UNKNOWN -} - var File_types_microvm_proto protoreflect.FileDescriptor var file_types_microvm_proto_rawDesc = []byte{ @@ -621,7 +651,7 @@ var file_types_microvm_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc0, 0x04, 0x0a, 0x0b, 0x4d, 0x69, 0x63, 0x72, 0x6f, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcb, 0x05, 0x0a, 0x0b, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x56, 0x4d, 0x53, 0x70, 0x65, 0x63, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, @@ -635,93 +665,100 @@ var file_types_microvm_proto_rawDesc = []byte{ 0x0a, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x4d, 0x62, 0x12, 0x2e, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4b, 0x65, 0x72, - 0x6e, 0x65, 0x6c, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x26, 0x0a, 0x0c, 0x69, - 0x6e, 0x69, 0x74, 0x72, 0x64, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x72, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x08, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x40, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x69, 0x67, - 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x52, 0x0a, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x39, 0x0a, - 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x69, 0x6e, 0x69, - 0x74, 0x72, 0x64, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x66, 0x0a, 0x06, 0x4b, 0x65, 0x72, - 0x6e, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6d, 0x64, - 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6d, 0x64, 0x6c, - 0x69, 0x6e, 0x65, 0x12, 0x1f, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, - 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, - 0x65, 0x22, 0xb7, 0x01, 0x0a, 0x10, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x09, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6d, 0x61, - 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x67, 0x75, 0x65, 0x73, 0x74, - 0x4d, 0x61, 0x63, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x11, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x01, 0x52, 0x0f, 0x67, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x67, 0x75, 0x65, 0x73, - 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x95, 0x02, 0x0a, 0x06, - 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x6f, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x52, 0x6f, 0x6f, 0x74, 0x12, - 0x20, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, - 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, - 0x70, 0x65, 0x73, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x61, 0x72, 0x74, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, - 0x12, 0x21, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x6e, 0x5f, 0x6d, 0x62, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x08, 0x73, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x4d, 0x62, - 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x6e, - 0x5f, 0x6d, 0x62, 0x22, 0xbb, 0x01, 0x0a, 0x0c, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x52, 0x0a, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x70, 0x61, 0x74, 0x68, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, - 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x48, - 0x6f, 0x73, 0x74, 0x50, 0x61, 0x74, 0x68, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x48, 0x01, 0x52, 0x0e, 0x68, 0x6f, 0x73, 0x74, 0x70, 0x61, 0x74, 0x68, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x12, 0x0a, - 0x10, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x70, 0x61, 0x74, 0x68, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x22, 0x2d, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x56, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, - 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, - 0x22, 0x9c, 0x01, 0x0a, 0x14, 0x48, 0x6f, 0x73, 0x74, 0x50, 0x61, 0x74, 0x68, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x45, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x72, 0x65, - 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x48, 0x6f, 0x73, - 0x74, 0x50, 0x61, 0x74, 0x68, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x50, 0x61, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x29, 0x0a, 0x0c, 0x48, 0x6f, 0x73, 0x74, 0x50, 0x61, 0x74, 0x68, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, - 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x41, 0x57, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x01, 0x42, - 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x77, 0x65, - 0x61, 0x76, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x2f, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, - 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x3b, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x65, 0x6c, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x33, 0x0a, 0x06, 0x69, + 0x6e, 0x69, 0x74, 0x72, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, + 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x69, + 0x74, 0x72, 0x64, 0x48, 0x00, 0x52, 0x06, 0x69, 0x6e, 0x69, 0x74, 0x72, 0x64, 0x88, 0x01, 0x01, + 0x12, 0x30, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x73, 0x12, 0x40, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, + 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, + 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, + 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x56, 0x4d, 0x53, + 0x70, 0x65, 0x63, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3b, 0x0a, 0x0d, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x69, 0x6e, + 0x69, 0x74, 0x72, 0x64, 0x22, 0x94, 0x01, 0x0a, 0x06, 0x4b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, + 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x12, + 0x1f, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x64, 0x64, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x64, + 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0b, + 0x0a, 0x09, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x4c, 0x0a, 0x06, 0x49, + 0x6e, 0x69, 0x74, 0x72, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x0a, 0x08, 0x66, + 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, + 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xaa, 0x02, 0x0a, 0x10, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x2a, + 0x0a, 0x11, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x75, 0x65, 0x73, 0x74, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x69, 0x67, 0x6e, + 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x2e, 0x49, 0x66, 0x61, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, 0x65, 0x71, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x09, 0x67, 0x75, 0x65, 0x73, + 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x67, + 0x75, 0x65, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x22, 0x21, 0x0a, 0x09, 0x49, 0x66, 0x61, + 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x41, 0x43, 0x56, 0x54, 0x41, + 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x50, 0x10, 0x01, 0x42, 0x0c, 0x0a, 0x0a, + 0x5f, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x95, 0x02, 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x69, 0x73, + 0x5f, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x69, 0x73, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x1f, 0x0a, 0x0b, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x34, 0x0a, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x72, 0x65, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x0a, 0x73, + 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x6e, 0x5f, 0x6d, 0x62, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, + 0x01, 0x52, 0x08, 0x73, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x4d, 0x62, 0x88, 0x01, 0x01, 0x42, 0x0f, + 0x0a, 0x0d, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x42, + 0x0d, 0x0a, 0x0b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x6e, 0x5f, 0x6d, 0x62, 0x22, 0x53, + 0x0a, 0x0c, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2e, + 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x13, + 0x0a, 0x11, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x22, 0x2d, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, + 0x67, 0x65, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x2f, 0x72, 0x65, 0x69, 0x67, + 0x6e, 0x69, 0x74, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x3b, 0x74, + 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -736,35 +773,40 @@ func file_types_microvm_proto_rawDescGZIP() []byte { return file_types_microvm_proto_rawDescData } -var file_types_microvm_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_types_microvm_proto_msgTypes = make([]protoimpl.MessageInfo, 8) -var file_types_microvm_proto_goTypes = []interface{}{ - (HostPathVolumeSource_HostPathType)(0), // 0: reignite.types.HostPathVolumeSource.HostPathType - (*MicroVMSpec)(nil), // 1: reignite.types.MicroVMSpec - (*Kernel)(nil), // 2: reignite.types.Kernel - (*NetworkInterface)(nil), // 3: reignite.types.NetworkInterface - (*Volume)(nil), // 4: reignite.types.Volume - (*VolumeSource)(nil), // 5: reignite.types.VolumeSource - (*ContainerVolumeSource)(nil), // 6: reignite.types.ContainerVolumeSource - (*HostPathVolumeSource)(nil), // 7: reignite.types.HostPathVolumeSource - nil, // 8: reignite.types.MicroVMSpec.LabelsEntry - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp -} +var ( + file_types_microvm_proto_enumTypes = make([]protoimpl.EnumInfo, 1) + file_types_microvm_proto_msgTypes = make([]protoimpl.MessageInfo, 9) + file_types_microvm_proto_goTypes = []interface{}{ + (NetworkInterface_IfaceType)(0), // 0: reignite.types.NetworkInterface.IfaceType + (*MicroVMSpec)(nil), // 1: reignite.types.MicroVMSpec + (*Kernel)(nil), // 2: reignite.types.Kernel + (*Initrd)(nil), // 3: reignite.types.Initrd + (*NetworkInterface)(nil), // 4: reignite.types.NetworkInterface + (*Volume)(nil), // 5: reignite.types.Volume + (*VolumeSource)(nil), // 6: reignite.types.VolumeSource + (*ContainerVolumeSource)(nil), // 7: reignite.types.ContainerVolumeSource + nil, // 8: reignite.types.MicroVMSpec.LabelsEntry + nil, // 9: reignite.types.MicroVMSpec.MetadataEntry + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + } +) + var file_types_microvm_proto_depIdxs = []int32{ - 8, // 0: reignite.types.MicroVMSpec.labels:type_name -> reignite.types.MicroVMSpec.LabelsEntry - 2, // 1: reignite.types.MicroVMSpec.kernel:type_name -> reignite.types.Kernel - 4, // 2: reignite.types.MicroVMSpec.volumes:type_name -> reignite.types.Volume - 3, // 3: reignite.types.MicroVMSpec.interfaces:type_name -> reignite.types.NetworkInterface - 9, // 4: reignite.types.MicroVMSpec.created_at:type_name -> google.protobuf.Timestamp - 9, // 5: reignite.types.MicroVMSpec.updated_at:type_name -> google.protobuf.Timestamp - 5, // 6: reignite.types.Volume.source:type_name -> reignite.types.VolumeSource - 7, // 7: reignite.types.VolumeSource.hostpath_source:type_name -> reignite.types.HostPathVolumeSource - 0, // 8: reignite.types.HostPathVolumeSource.type:type_name -> reignite.types.HostPathVolumeSource.HostPathType - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 8, // 0: reignite.types.MicroVMSpec.labels:type_name -> reignite.types.MicroVMSpec.LabelsEntry + 2, // 1: reignite.types.MicroVMSpec.kernel:type_name -> reignite.types.Kernel + 3, // 2: reignite.types.MicroVMSpec.initrd:type_name -> reignite.types.Initrd + 5, // 3: reignite.types.MicroVMSpec.volumes:type_name -> reignite.types.Volume + 4, // 4: reignite.types.MicroVMSpec.interfaces:type_name -> reignite.types.NetworkInterface + 9, // 5: reignite.types.MicroVMSpec.metadata:type_name -> reignite.types.MicroVMSpec.MetadataEntry + 10, // 6: reignite.types.MicroVMSpec.created_at:type_name -> google.protobuf.Timestamp + 10, // 7: reignite.types.MicroVMSpec.updated_at:type_name -> google.protobuf.Timestamp + 0, // 8: reignite.types.NetworkInterface.type:type_name -> reignite.types.NetworkInterface.IfaceType + 6, // 9: reignite.types.Volume.source:type_name -> reignite.types.VolumeSource + 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 } func init() { file_types_microvm_proto_init() } @@ -798,7 +840,7 @@ func file_types_microvm_proto_init() { } } file_types_microvm_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkInterface); i { + switch v := v.(*Initrd); i { case 0: return &v.state case 1: @@ -810,7 +852,7 @@ func file_types_microvm_proto_init() { } } file_types_microvm_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Volume); i { + switch v := v.(*NetworkInterface); i { case 0: return &v.state case 1: @@ -822,7 +864,7 @@ func file_types_microvm_proto_init() { } } file_types_microvm_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VolumeSource); i { + switch v := v.(*Volume); i { case 0: return &v.state case 1: @@ -834,7 +876,7 @@ func file_types_microvm_proto_init() { } } file_types_microvm_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ContainerVolumeSource); i { + switch v := v.(*VolumeSource); i { case 0: return &v.state case 1: @@ -846,7 +888,7 @@ func file_types_microvm_proto_init() { } } file_types_microvm_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostPathVolumeSource); i { + switch v := v.(*ContainerVolumeSource); i { case 0: return &v.state case 1: @@ -863,13 +905,14 @@ func file_types_microvm_proto_init() { file_types_microvm_proto_msgTypes[2].OneofWrappers = []interface{}{} file_types_microvm_proto_msgTypes[3].OneofWrappers = []interface{}{} file_types_microvm_proto_msgTypes[4].OneofWrappers = []interface{}{} + file_types_microvm_proto_msgTypes[5].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_types_microvm_proto_rawDesc, NumEnums: 1, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/api/types/microvm.proto b/api/types/microvm.proto index 2443f564..28beda48 100644 --- a/api/types/microvm.proto +++ b/api/types/microvm.proto @@ -27,8 +27,8 @@ message MicroVMSpec { // Kernel is the details of the kernel to use . Kernel kernel = 6; - // Initrd_image is the container image to use for the initial ramdisk. - optional string initrd_image = 7; + // Initrd is the optional details of the initial ramdisk. + optional Initrd initrd = 7; // Volumes specifies the volumes to be attached to the microvm. There must be // at lease one volume that will act as the root volume. @@ -37,11 +37,15 @@ message MicroVMSpec { // Interfaces specifies the network interfaces to be attached to the microvm. repeated NetworkInterface interfaces = 9; + // Metadata allows you to specify data to be added to the metadata service. The key is the name + // of the metadata item and the value is the base64 encoded contents of the metadata. + map metadata = 10; + // CreatedAt indicates the time the microvm was created at. - google.protobuf.Timestamp created_at = 10; + google.protobuf.Timestamp created_at = 11; // UpdatedAt indicates the time the microvm was last updated. - google.protobuf.Timestamp updated_at = 11; + google.protobuf.Timestamp updated_at = 12; } // Kernel represents the configuration for a kernel. @@ -53,17 +57,37 @@ message Kernel { // Filename is used to specify the name of the kernel file // in the Image. optional string filename = 3; + // AddNetworkConfig if set to true indicates that the network-config kernel argument should be generated. + bool add_network_config = 4; +} + +// Initrd represents the configuration for the initial ramdisk. +message Initrd { + // Image is the container image to use. + string image = 1; + // Filename is used to specify the name of the kernel file + // in the Image. Defaults to initrd + optional string filename = 2; } message NetworkInterface { + enum IfaceType { + // MACVTAP represents a network interface that is macvtap. + MACVTAP = 0; + // TAP represents a network interface that is a tap. + TAP = 1; + } + // GuestDeviceName is the name of the network interface to create in the microvm. + string guest_device_name = 1; + // IfaceType specifies the type of network interface to create for use by the guest. + IfaceType type = 2; // AllowMetadataReq indicates if the network interface allows metadata requests. - bool allow_metadata_req = 1; + bool allow_metadata_req = 3; // GuestMAC allows the specifying of a specifi MAC address to use for the interface. If - // not supplied a autogenerated MAC address will be used.y - optional string guest_mac = 2; - // GuestDeviceName is the name of the network interface to create in the microvm. If this - // is not supplied than a device name will be assigned automatically - optional string guest_device_name = 3; + // not supplied a autogenerated MAC address will be used. + optional string guest_mac = 4; + // Address is an optional IP address to assign to this interface. If not supplied then DHCP will be used. + optional string address = 5; } // Volume represents the configuration for a volume to be attached to a microvm. @@ -90,8 +114,6 @@ message Volume { message VolumeSource { // Container is used to specify a source of a volume as a OCI container. optional string container_source = 1; - // HostPath is used to specify a source of a volume as a file/device on the host. - optional HostPathVolumeSource hostpath_source = 2; //TODO: add CSI } @@ -99,19 +121,4 @@ message VolumeSource { message ContainerVolumeSource { // Image specifies teh conatiner image to use for the volume. string image = 1; -} - -// HostPathVolumeSource represents the details of a volume coming from a file/device on the host. -message HostPathVolumeSource { - // Path on the host of the file/device to use as a source for a volume. - string path = 1; - - enum HostPathType { - UNKNOWN = 0; - // RAW_FILE represents a file on the host to use as a source for a volume. It should be a raw fs. - RAW_FILE = 1; - } - - // Type is the type of file/device on the host. - HostPathType type = 2; } \ No newline at end of file diff --git a/cmd/dev-helper/main.go b/cmd/dev-helper/main.go index d135e942..8342d339 100644 --- a/cmd/dev-helper/main.go +++ b/cmd/dev-helper/main.go @@ -3,38 +3,45 @@ package main import ( "context" + "encoding/base64" "fmt" + "io/ioutil" "log" "time" - "github.com/containerd/containerd/namespaces" + "github.com/weaveworks/reignite/pkg/cloudinit" + "github.com/weaveworks/reignite/pkg/ptr" + "gopkg.in/yaml.v3" + + "github.com/weaveworks/reignite/internal/config" + "github.com/weaveworks/reignite/internal/inject" + + "github.com/weaveworks/reignite/core/plans" + "github.com/weaveworks/reignite/pkg/planner" + "github.com/sirupsen/logrus" _ "github.com/containerd/containerd/api/events" - ctr "github.com/containerd/containerd" - - "github.com/weaveworks/reignite/api/events" "github.com/weaveworks/reignite/core/models" - "github.com/weaveworks/reignite/core/ports" - "github.com/weaveworks/reignite/infrastructure/containerd" - "github.com/weaveworks/reignite/infrastructure/controllers" + portsctx "github.com/weaveworks/reignite/core/ports/context" "github.com/weaveworks/reignite/pkg/defaults" rlog "github.com/weaveworks/reignite/pkg/log" ) -//NOTE: this is a temporary app to help with development +// NOTE: this is a temporary app to help with development const ( - vmName = "vm1" - vmNamespace = "teamabc" - imageName = "docker.io/library/ubuntu:groovy" + namespace = "ns1" + numNodes = 2 + socketPath = "/home/richard/code/scratch/containerdlocal/run/containerd.sock" + sshKeyPath = "/home/richard/.ssh/id_ed25519.pub" + rootfsImage = "docker.io/richardcase/ubuntu-bionic-test:cloudimage_v0.0.1" + kernelImage = "docker.io/richardcase/ubuntu-bionic-kernel:0.0.11" + fqdnFormat = "%s.fruitcase" ) func main() { - //socketPath := defaults.ContainerdSocket - socketPath := "/home/richard/code/scratch/containerdlocal/run/containerd.sock" - ctx, cancel := context.WithCancel(context.Background()) rlog.Configure(&rlog.Config{ @@ -45,235 +52,179 @@ func main() { logger := rlog.GetLogger(ctx) logger.Infof("reignite dev-helper, using containerd socket: %s", socketPath) - //eventPublishTest(ctx, socketPath, logger) - - logger.Info("starting containerd event listener") - go eventListener(ctx, socketPath, logger) - - logger.Infof("Press [enter] to run controller test") - //fmt.Scanln() - go controllerTest(ctx, socketPath, logger) - go publishPeriodicEvents(ctx, socketPath, logger) - - //logger.Infof("Press [enter] to write vmspec to using containerd repo") - //fmt.Scanln() - //repoTest(ctx, socketPath, logger) + keyData, err := ioutil.ReadFile(sshKeyPath) + if err != nil { + panic(err) + } - //logger.Infof("Press [enter] to get image %s", imageName) - //fmt.Scanln() - //imageServiceTest(ctx, socketPath, logger) + logger.Infof("Running createvm plan to create %d microvms", numNodes) + for i := 0; i < numNodes; i++ { + runCreateVMPlanTest(ctx, i, string(keyData), logger) + } - logger.Info("Press [enter] to exit") - fmt.Scanln() + logger.Info("finished creating microvms") cancel() } -func eventPublishTest(ctx context.Context, socketPath string, logger *logrus.Entry) { - cfg := &containerd.Config{ - SocketPath: socketPath, - } - logger.Info("creating event service") +func runCreateVMPlanTest(ctx context.Context, nodeNum int, sshKey string, logger *logrus.Entry) { + macMeta := "AA:FF:00:00:00:01" + hostname := fmt.Sprintf("mvm%d", nodeNum) - es, err := containerd.NewEventService(cfg) + cfg := &config.Config{ + ConfigFilePath: "", + Logging: rlog.Config{ + Verbosity: 9, + Format: "text", + }, + GRPCAPIEndpoint: "", + HTTPAPIEndpoint: "", + CtrSnapshotterKernel: defaults.ContainerdKernelSnapshotter, + CtrSnapshotterVolume: defaults.ContainerdVolumeSnapshotter, + CtrSocketPath: socketPath, + CtrNamespace: defaults.ContainerdNamespace, + FirecrackerBin: "/home/richard/bin/firecracker", + FirecrackerDetatch: false, + FirecrackerUseAPI: true, + StateRootDir: defaults.StateRootDir, + ParentIface: "enp1s0", + DisableReconcile: false, + DisableAPI: false, + ResyncPeriod: 0, + } + + vmid, _ := models.NewVMID(hostname, namespace) + + userdata, err := getUserMetadata(hostname, sshKey) if err != nil { - log.Fatal(err) + panic(err) } - - evt := &events.MicroVMSpecCreated{ - ID: "abcdf", - Namespace: "ns1", - } - - ctx, cancel := context.WithCancel(ctx) - - evts, errs := es.Subscribe(ctx) - - err = es.Publish(ctx, "/test", evt) + hostmetadata, err := getHostMetadata(vmid, hostname) if err != nil { - log.Fatal(err) + panic(err) } - select { - case evt := <-evts: - fmt.Printf("in dev-helper, got evtenr: %#v\n", evt.Event) - case evtErr := <-errs: - fmt.Println(evtErr) + spec := &models.MicroVM{ + ID: *vmid, + Version: 0, + Spec: models.MicroVMSpec{ + VCPU: 2, + MemoryInMb: 2048, + Kernel: models.Kernel{ + Image: models.ContainerImage(kernelImage), + CmdLine: "console=ttyS0 reboot=k panic=1 pci=off i8042.noaux i8042.nomux i8042.nopnp i8042.dumbkbd ds=nocloud-net;s=http://169.254.169.254/latest/", + Filename: "vmlinux", + AddNetworkConfig: true, + }, + Initrd: &models.Initrd{ + Image: models.ContainerImage(kernelImage), + Filename: "initrd-generic", + }, + Volumes: models.Volumes{ + { + ID: "root", + IsRoot: true, + IsReadOnly: false, + MountPoint: "/", + Source: models.VolumeSource{ + Container: &models.ContainerVolumeSource{ + Image: models.ContainerImage(rootfsImage), + }, + }, + }, + }, + NetworkInterfaces: []models.NetworkInterface{ + { + GuestDeviceName: "eth0", + GuestMAC: macMeta, + Type: models.IfaceTypeTap, + AllowMetadataRequests: true, + Address: "169.254.0.1/16", + }, + { + GuestDeviceName: "eth1", + Type: models.IfaceTypeMacvtap, + AllowMetadataRequests: false, + }, + }, + Metadata: map[string]string{ + "meta-data": hostmetadata, + "user-data": userdata, + }, + }, } - cancel() -} - -func repoTest(ctx context.Context, socketPath string, logger *logrus.Entry) { - client, err := ctr.New(socketPath) + ports, err := inject.InitializePorts(cfg) if err != nil { - log.Fatal(err) + panic(err) } + execCtx := portsctx.WithPorts(ctx, ports) - repo := containerd.NewMicroVMRepoWithClient(client) - - vmSpec := getTestSpec() - logger.Infof("saving microvm spec %s", vmSpec.ID) - - _, err = repo.Save(ctx, vmSpec) - if err != nil { - log.Fatal(err) + input := &plans.CreatePlanInput{ + StateDirectory: cfg.StateRootDir, + VM: spec, } -} -func imageServiceTest(ctx context.Context, socketPath string, logger *logrus.Entry) { - cfg := &containerd.Config{ - //Snapshotter: defaults.ContainerdSnapshotter, - //Snapshotter: "overlayfs", - SnapshotterKernel: "native", - SnapshotterVolume: "native", - SocketPath: socketPath, - } - logger.Infof("using snapshotters %s & %s", cfg.SnapshotterKernel, cfg.SnapshotterVolume) + start := time.Now() - imageService, err := containerd.NewImageService(cfg) - if err != nil { - log.Fatal(err) - } + plan := plans.MicroVMCreatePlan(input) - input := ports.GetImageInput{ - ImageName: imageName, - OwnerName: vmName, - OwnerNamespace: vmNamespace, - Use: models.ImageUseVolume, + actuator := planner.NewActuator() + if err := actuator.Execute(execCtx, plan, "1234567890"); err != nil { + panic(err) } - mountPoint, err := imageService.GetAndMount(ctx, input) - if err != nil { - log.Fatal(err) + + if err := ports.Provider.Start(ctx, spec.ID.String()); err != nil { + panic(err) } - logger.Infof("mounted image %s to %s (type %s)", imageName, mountPoint[0].Source, mountPoint[0].Type) + elapsed := time.Since(start) + log.Printf("create & start microvm took %s", elapsed) } -func publishPeriodicEvents(ctx context.Context, socketPath string, logger *logrus.Entry) { - client, err := ctr.New(socketPath) - if err != nil { - log.Fatal(err) +func getUserMetadata(hostname, sshkey string) (string, error) { + runCommands := []string{ + "dhclient -r", + "dhclient", } - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) - - tickCh := time.Tick(10 * time.Second) - - for { - select { - case <-ctx.Done(): - logger.Info("Cancelled, exiting publisher") - return - case <-tickCh: - logger.Info("publishing event") - createEvt := &events.MicroVMSpecCreated{ - ID: "vm1", - Namespace: "testns", - } - - err = client.EventService().Publish(namespaceCtx, defaults.TopicMicroVMEvents, createEvt) - if err != nil { - logger.Error(err) - } - - } + userData := &cloudinit.UserData{ + Users: &[]cloudinit.User{ + { + Name: "root", + SSHAuthorizedKeys: &[]string{sshkey}, + }, + }, + HostName: ptr.String(hostname), + Fqdn: ptr.String(fmt.Sprintf(fqdnFormat, hostname)), + DisableRoot: ptr.Bool(false), + PackageUpdate: ptr.Bool(false), + FinalMessage: ptr.String("The reignited booted system is good to go after $UPTIME seconds"), + // WriteFiles: nil, + RunCommands: &runCommands, } -} -func controllerTest(ctx context.Context, socketPath string, logger *logrus.Entry) { - em, err := containerd.NewEventService(&containerd.Config{ - SocketPath: socketPath, - }) + md, err := yaml.Marshal(userData) if err != nil { - log.Fatal(err) - } - - app := &testApp{ - logger: logger, + return "", fmt.Errorf("marshalling cloud-init userdata: %w", err) } - controller := controllers.New(em, app) - - logger.Info("running controller") - controller.Run(ctx, 1) - logger.Info("controller not running") + userDataStr := fmt.Sprintf("#cloud-config\n%s", string(md)) + return base64.StdEncoding.EncodeToString([]byte(userDataStr)), nil } -func eventListener(ctx context.Context, socketPath string, logger *logrus.Entry) { - cfg := &containerd.Config{ - SocketPath: socketPath, +func getHostMetadata(vmid *models.VMID, hostname string) (string, error) { + meta := &cloudinit.Metadata{ + InstanceID: vmid.String(), + LocalHostname: hostname, + Platform: "liquid_metal", } - logger.Info("creating event service") - es, err := containerd.NewEventService(cfg) + md, err := yaml.Marshal(meta) if err != nil { - log.Fatal(err) - } - - ch, errsCh := es.Subscribe(ctx) - for { - select { - case <-ctx.Done(): - logger.Info("Existing event listener") - return - case evt := <-ch: - logger.Infof("event received, ns %s, topic %s, body: %#v", evt.Namespace, evt.Topic, evt.Event) - case errEvt := <-errsCh: - logger.Errorf("event error received: %s", errEvt) - } - } -} - -func getTestSpec() *models.MicroVM { - vmid, _ := models.NewVMID(vmName, vmNamespace) - return &models.MicroVM{ - ID: *vmid, - Spec: models.MicroVMSpec{ - MemoryInMb: 2048, - VCPU: 4, - Kernel: models.Kernel{ - Image: "docker.io/linuxkit/kernel:5.4.129", - CmdLine: "console=ttyS0 reboot=k panic=1 pci=off i8042.noaux i8042.nomux i8042.nopnp i8042.dumbkbd ds=nocloud-net;s=http://169.254.169.254/latest/ network-config=ASDFGFDFG", - }, - NetworkInterfaces: []models.NetworkInterface{ - { - AllowMetadataRequests: false, - GuestMAC: "AA:FF:00:00:00:01", - HostDeviceName: "tap1", - GuestDeviceName: "eth0", - }, - { - AllowMetadataRequests: false, - HostDeviceName: "/dev/tap55", - GuestDeviceName: "eth1", - }, - }, - Volumes: []models.Volume{ - { - ID: "root", - IsRoot: true, - IsReadOnly: false, - MountPoint: "/", - Source: models.VolumeSource{ - Container: &models.ContainerVolumeSource{ - Image: imageName, - }, - }, - Size: 20000, - }, - }, - }, + return "", fmt.Errorf("marshalling cloud-init metadata: %w", err) } -} - -type testApp struct { - logger *logrus.Entry -} - -func (t *testApp) ReconcileMicroVM(ctx context.Context, id, namespace string) error { - t.logger.Infof("received request to reconcile %s/%s", id, namespace) - return nil + return base64.StdEncoding.EncodeToString(md), nil } diff --git a/core/application/app.go b/core/application/app.go index 74a41f42..cc6fe707 100644 --- a/core/application/app.go +++ b/core/application/app.go @@ -1,8 +1,10 @@ package application -import "github.com/weaveworks/reignite/core/ports" +import ( + "github.com/weaveworks/reignite/core/ports" +) -// AppS is the interface for the core application. In the future this could be split +// App is the interface for the core application. In the future this could be split // into separate command, query and reconcile services. type App interface { ports.MicroVMCommandUseCases @@ -10,18 +12,18 @@ type App interface { ports.ReconcileMicroVMsUseCase } -func New(repo ports.MicroVMRepository, eventSvc ports.EventService, idSvc ports.IDService, mvmProvider ports.MicroVMProvider) App { +func New(cfg *Config, ports *ports.Collection) App { return &app{ - repo: repo, - eventSvc: eventSvc, - idSvc: idSvc, - provider: mvmProvider, + cfg: cfg, + ports: ports, } } type app struct { - repo ports.MicroVMRepository - eventSvc ports.EventService - idSvc ports.IDService - provider ports.MicroVMProvider + cfg *Config + ports *ports.Collection +} + +type Config struct { + RootStateDir string } diff --git a/core/application/app_test.go b/core/application/app_test.go index 4742a540..bdc782ca 100644 --- a/core/application/app_test.go +++ b/core/application/app_test.go @@ -6,10 +6,12 @@ import ( "github.com/golang/mock/gomock" . "github.com/onsi/gomega" + "github.com/spf13/afero" "github.com/weaveworks/reignite/api/events" "github.com/weaveworks/reignite/core/application" "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" "github.com/weaveworks/reignite/infrastructure/mock" "github.com/weaveworks/reignite/pkg/defaults" ) @@ -19,19 +21,19 @@ func TestApp_CreateMicroVM(t *testing.T) { name string specToCreate *models.MicroVM expectError bool - expect func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) + expect func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) }{ { name: "nil spec, should fail", expectError: true, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { }, }, { name: "spec with no id or namespace, create id/ns and create", specToCreate: createTestSpec("", ""), expectError: false, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { im.GenerateRandom().Return("id1234", nil) rm.Get( @@ -65,7 +67,7 @@ func TestApp_CreateMicroVM(t *testing.T) { name: "spec with id or namespace, create", specToCreate: createTestSpec("id1234", "default"), expectError: false, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { rm.Get( gomock.AssignableToTypeOf(context.Background()), gomock.Eq("id1234"), @@ -97,7 +99,7 @@ func TestApp_CreateMicroVM(t *testing.T) { name: "spec already exists, should fail", specToCreate: createTestSpec("id1234", "default"), expectError: true, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { rm.Get( gomock.AssignableToTypeOf(context.Background()), gomock.Eq("id1234"), @@ -120,12 +122,24 @@ func TestApp_CreateMicroVM(t *testing.T) { rm := mock.NewMockMicroVMRepository(mockCtrl) em := mock.NewMockEventService(mockCtrl) im := mock.NewMockIDService(mockCtrl) - pm := mock.NewMockMicroVMProvider(mockCtrl) + pm := mock.NewMockMicroVMService(mockCtrl) + ns := mock.NewMockNetworkService(mockCtrl) + is := mock.NewMockImageService(mockCtrl) + fs := afero.NewMemMapFs() + ports := &ports.Collection{ + Repo: rm, + Provider: pm, + EventService: em, + IdentifierService: im, + NetworkService: ns, + ImageService: is, + FileSystem: fs, + } tc.expect(rm.EXPECT(), em.EXPECT(), im.EXPECT(), pm.EXPECT()) ctx := context.Background() - app := application.New(rm, em, im, pm) + app := application.New(&application.Config{}, ports) _, err := app.CreateMicroVM(ctx, tc.specToCreate) if tc.expectError { @@ -142,26 +156,26 @@ func TestApp_UpdateMicroVM(t *testing.T) { name string specToUpdate *models.MicroVM expectError bool - expect func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) + expect func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) }{ { name: "nil spec, should fail", expectError: true, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { }, }, { name: "spec with no id or namespace, should fail", specToUpdate: createTestSpec("", ""), expectError: true, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { }, }, { name: "spec is valid and update is valid, update", specToUpdate: createTestSpec("id1234", "default"), expectError: false, - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { rm.Get( gomock.AssignableToTypeOf(context.Background()), gomock.Eq("id1234"), @@ -201,12 +215,24 @@ func TestApp_UpdateMicroVM(t *testing.T) { rm := mock.NewMockMicroVMRepository(mockCtrl) em := mock.NewMockEventService(mockCtrl) im := mock.NewMockIDService(mockCtrl) - pm := mock.NewMockMicroVMProvider(mockCtrl) + pm := mock.NewMockMicroVMService(mockCtrl) + ns := mock.NewMockNetworkService(mockCtrl) + is := mock.NewMockImageService(mockCtrl) + fs := afero.NewMemMapFs() + ports := &ports.Collection{ + Repo: rm, + Provider: pm, + EventService: em, + IdentifierService: im, + NetworkService: ns, + ImageService: is, + FileSystem: fs, + } tc.expect(rm.EXPECT(), em.EXPECT(), im.EXPECT(), pm.EXPECT()) ctx := context.Background() - app := application.New(rm, em, im, pm) + app := application.New(&application.Config{}, ports) _, err := app.UpdateMicroVM(ctx, tc.specToUpdate) if tc.expectError { @@ -224,14 +250,14 @@ func TestApp_DeleteMicroVM(t *testing.T) { toDeleteID string toDeleteNS string expectError bool - expect func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) + expect func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) }{ { name: "empty id, should fail", expectError: true, toDeleteID: "", toDeleteNS: "default", - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { }, }, { @@ -239,7 +265,7 @@ func TestApp_DeleteMicroVM(t *testing.T) { expectError: false, toDeleteID: "id1234", toDeleteNS: "default", - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { rm.Get( gomock.AssignableToTypeOf(context.Background()), gomock.Eq("id1234"), @@ -269,7 +295,7 @@ func TestApp_DeleteMicroVM(t *testing.T) { expectError: false, toDeleteID: "id1234", toDeleteNS: "default", - expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMProviderMockRecorder) { + expect: func(rm *mock.MockMicroVMRepositoryMockRecorder, em *mock.MockEventServiceMockRecorder, im *mock.MockIDServiceMockRecorder, pm *mock.MockMicroVMServiceMockRecorder) { rm.Get( gomock.AssignableToTypeOf(context.Background()), gomock.Eq("id1234"), @@ -292,12 +318,24 @@ func TestApp_DeleteMicroVM(t *testing.T) { rm := mock.NewMockMicroVMRepository(mockCtrl) em := mock.NewMockEventService(mockCtrl) im := mock.NewMockIDService(mockCtrl) - pm := mock.NewMockMicroVMProvider(mockCtrl) + pm := mock.NewMockMicroVMService(mockCtrl) + ns := mock.NewMockNetworkService(mockCtrl) + is := mock.NewMockImageService(mockCtrl) + fs := afero.NewMemMapFs() + ports := &ports.Collection{ + Repo: rm, + Provider: pm, + EventService: em, + IdentifierService: im, + NetworkService: ns, + ImageService: is, + FileSystem: fs, + } tc.expect(rm.EXPECT(), em.EXPECT(), im.EXPECT(), pm.EXPECT()) ctx := context.Background() - app := application.New(rm, em, im, pm) + app := application.New(&application.Config{}, ports) err := app.DeleteMicroVM(ctx, tc.toDeleteID, tc.toDeleteNS) if tc.expectError { diff --git a/core/application/commands.go b/core/application/commands.go index 1959fdd5..ece5fb81 100644 --- a/core/application/commands.go +++ b/core/application/commands.go @@ -20,7 +20,7 @@ func (a *app) CreateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M } if mvm.ID.IsEmpty() { - name, err := a.idSvc.GenerateRandom() + name, err := a.ports.IdentifierService.GenerateRandom() if err != nil { return nil, fmt.Errorf("generating random name for microvm: %w", err) } @@ -31,9 +31,11 @@ func (a *app) CreateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M mvm.ID = *vmid } - foundMvm, err := a.repo.Get(ctx, mvm.ID.Name(), mvm.ID.Namespace()) + foundMvm, err := a.ports.Repo.Get(ctx, mvm.ID.Name(), mvm.ID.Namespace()) if err != nil { - return nil, fmt.Errorf("checking to see if spec exists: %w", err) + if !coreerrs.IsSpecNotFound(err) { + return nil, fmt.Errorf("checking to see if spec exists: %w", err) + } } if foundMvm != nil { return nil, errSpecAlreadyExists{ @@ -44,12 +46,12 @@ func (a *app) CreateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M // TODO: validate the spec - createdMVM, err := a.repo.Save(ctx, mvm) + createdMVM, err := a.ports.Repo.Save(ctx, mvm) if err != nil { return nil, fmt.Errorf("saving microvm spec: %w", err) } - if err := a.eventSvc.Publish(ctx, defaults.TopicMicroVMEvents, &events.MicroVMSpecCreated{ + if err := a.ports.EventService.Publish(ctx, defaults.TopicMicroVMEvents, &events.MicroVMSpecCreated{ ID: mvm.ID.Name(), Namespace: mvm.ID.Namespace(), }); err != nil { @@ -70,7 +72,7 @@ func (a *app) UpdateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M return nil, coreerrs.ErrVMIDRequired } - foundMvm, err := a.repo.Get(ctx, mvm.ID.Name(), mvm.ID.Namespace()) + foundMvm, err := a.ports.Repo.Get(ctx, mvm.ID.Name(), mvm.ID.Namespace()) if err != nil { return nil, fmt.Errorf("checking to see if spec exists: %w", err) } @@ -84,12 +86,12 @@ func (a *app) UpdateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.M // TODO: validate incoming spec // TODO: check if update is valid (i.e. compare existing to requested update) - updatedMVM, err := a.repo.Save(ctx, mvm) + updatedMVM, err := a.ports.Repo.Save(ctx, mvm) if err != nil { return nil, fmt.Errorf("updating microvm spec: %w", err) } - if err := a.eventSvc.Publish(ctx, defaults.TopicMicroVMEvents, &events.MicroVMSpecUpdated{ + if err := a.ports.EventService.Publish(ctx, defaults.TopicMicroVMEvents, &events.MicroVMSpecUpdated{ ID: mvm.ID.Name(), Namespace: mvm.ID.Namespace(), }); err != nil { @@ -107,7 +109,7 @@ func (a *app) DeleteMicroVM(ctx context.Context, id, namespace string) error { return errIDRequired } - foundMvm, err := a.repo.Get(ctx, id, namespace) + foundMvm, err := a.ports.Repo.Get(ctx, id, namespace) if err != nil { return fmt.Errorf("checking to see if spec exists: %w", err) } @@ -117,12 +119,12 @@ func (a *app) DeleteMicroVM(ctx context.Context, id, namespace string) error { return nil } - err = a.repo.Delete(ctx, foundMvm) + err = a.ports.Repo.Delete(ctx, foundMvm) if err != nil { return fmt.Errorf("deleting microvm from repository: %w", err) } - if err := a.eventSvc.Publish(ctx, defaults.TopicMicroVMEvents, &events.MicroVMSpecDeleted{ + if err := a.ports.EventService.Publish(ctx, defaults.TopicMicroVMEvents, &events.MicroVMSpecDeleted{ ID: id, Namespace: namespace, }); err != nil { diff --git a/core/application/reconcile.go b/core/application/reconcile.go index 21c4f76c..6f4f4144 100644 --- a/core/application/reconcile.go +++ b/core/application/reconcile.go @@ -1,7 +1,82 @@ package application -import "context" +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/plans" + portsctx "github.com/weaveworks/reignite/core/ports/context" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) func (a *app) ReconcileMicroVM(ctx context.Context, id, namespace string) error { - return errNotImplemeted + logger := log.GetLogger(ctx).WithField("action", "reconcile") + + logger.Debugf("Getting spec for %s/%s", namespace, id) + spec, err := a.ports.Repo.Get(ctx, id, namespace) + if err != nil { + return fmt.Errorf("getting microvm spec for reconcile: %w", err) + } + + return a.reconcile(ctx, spec, logger) +} + +func (a *app) ResyncMicroVMs(ctx context.Context, namespace string) error { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "action": "resync", + "namespace": "ns", + }) + logger.Info("Resyncing specs") + + logger.Debug("Getting all specs") + specs, err := a.ports.Repo.GetAll(ctx, namespace) + if err != nil { + return fmt.Errorf("getting all microvm specs for resync: %w", err) + } + + for _, spec := range specs { + if err := a.reconcile(ctx, spec, logger); err != nil { + return fmt.Errorf("resync reconcile for spec %s: %w", spec.ID, err) + } + } + + return nil +} + +func (a *app) reconcile(ctx context.Context, spec *models.MicroVM, logger *logrus.Entry) error { + l := logger.WithField("vmid", spec.ID.String()) + l.Info("Starting reconciliation") + + input := &plans.CreatePlanInput{ + StateDirectory: a.cfg.RootStateDir, + VM: spec, + } + + plan := plans.MicroVMCreatePlan(input) + + execCtx := portsctx.WithPorts(ctx, a.ports) + + executionID, err := a.ports.IdentifierService.GenerateRandom() + if err != nil { + return fmt.Errorf("generating plan execution id: %w", err) + } + + actuator := planner.NewActuator() + if err := actuator.Execute(execCtx, plan, executionID); err != nil { + return fmt.Errorf("executing plan: %w", err) + } + + if _, err := a.ports.Repo.Save(ctx, spec); err != nil { + return fmt.Errorf("saving spec after plan execution: %w", err) + } + + if err := a.ports.Provider.Start(ctx, spec.ID.String()); err != nil { + return fmt.Errorf("starting micro vm %s: %w", spec.ID, err) + } + + return nil } diff --git a/core/errors/errors.go b/core/errors/errors.go index 408d9deb..a53f761b 100644 --- a/core/errors/errors.go +++ b/core/errors/errors.go @@ -1,4 +1,4 @@ -package core +package errors import ( "errors" @@ -6,10 +6,19 @@ import ( ) var ( - ErrSpecRequired = errors.New("microvm spec is required") - ErrVMIDRequired = errors.New("id for microvm is required") - ErrNameRequired = errors.New("name is required") - ErrNamespaceRequired = errors.New("namespace is required") + ErrSpecRequired = errors.New("microvm spec is required") + ErrVMIDRequired = errors.New("id for microvm is required") + ErrNameRequired = errors.New("name is required") + ErrNamespaceRequired = errors.New("namespace is required") + ErrKernelImageRequired = errors.New("kernel image is required") + ErrVolumeRequired = errors.New("no volumes specified, at least 1 volume is required") + ErrRootVolumeRequired = errors.New("a root volume is required") + ErrNoMount = errors.New("no image mount point") + ErrNoVolumeMount = errors.New("no volume mount point") + ErrParentIfaceRequired = errors.New("a parent network device name is required") + ErrGuestDeviceNameRequired = errors.New("a guest device name is required") + ErrUnsupportedIfaceType = errors.New("unsupported network interface type") + ErrIfaceNotFound = errors.New("network interface not found") ) // ErrTopicNotFound is an error created when a topic with a specific name isn't found. @@ -30,3 +39,75 @@ type ErrIncorrectVMIDFormat struct { func (e ErrIncorrectVMIDFormat) Error() string { return fmt.Sprintf("unexpected vmid format: %s", e.ActualID) } + +func NewErrUnsupportedInterface(ifaceType string) ErrUnsupportedInterface { + return ErrUnsupportedInterface{ + ifaceType: ifaceType, + } +} + +type ErrUnsupportedInterface struct { + ifaceType string +} + +// Error returns the error message. +func (e ErrUnsupportedInterface) Error() string { + return fmt.Sprintf("network interface type %s is unsupported", e.ifaceType) +} + +func NewVolumeNotMounted(volumeID string) ErrVolumeNotMounted { + return ErrVolumeNotMounted{ + id: volumeID, + } +} + +// ErrVolumeNotMounted is an error used when a volume hasn't been mounted. +type ErrVolumeNotMounted struct { + id string +} + +// Error returns the error message. +func (e ErrVolumeNotMounted) Error() string { + return fmt.Sprintf("volume %s is not mounted", e.id) +} + +func NewNetworkInterfaceStatusMissing(guestIface string) ErrNetworkInterfaceStatusMissing { + return ErrNetworkInterfaceStatusMissing{ + guestIface: guestIface, + } +} + +// NetworkInterfaceStatusMissing is an error used when a network interfaces +// status cannot be found. +type ErrNetworkInterfaceStatusMissing struct { + guestIface string +} + +// Error returns the error message. +func (e ErrNetworkInterfaceStatusMissing) Error() string { + return fmt.Sprintf("status for network interface %s is not found", e.guestIface) +} + +func NewSpecNotFound(name, namespace string) error { + return errSpecNotFound{ + name: name, + namespace: namespace, + } +} + +type errSpecNotFound struct { + name string + namespace string +} + +// Error returns the error message. +func (e errSpecNotFound) Error() string { + return fmt.Sprintf("microvm spec %s/%s not found", e.namespace, e.name) +} + +// IsSpecNotFound tests an error to see if its a spec not found error. +func IsSpecNotFound(err error) bool { + e := &errSpecNotFound{} + + return errors.As(err, e) +} diff --git a/core/models/microvm.go b/core/models/microvm.go index c3ab3b64..e28edc9c 100644 --- a/core/models/microvm.go +++ b/core/models/microvm.go @@ -8,14 +8,16 @@ type MicroVM struct { Version int `json:"version"` // Spec is the specification of the microvm. Spec MicroVMSpec `json:"spec"` + // Status is the runtime status of the microvm. + Status MicroVMStatus `json:"status"` } // MicroVMSpec represents the specification of a microvm machine. type MicroVMSpec struct { // Kernel specifies the kernel and its argments to use. Kernel Kernel `json:"kernel"` - // InitrdImage is an optional initial ramdisk to use. - InitrdImage ContainerImage `json:"initrd_image,omitempty"` + // Initrd is an optional initial ramdisk to use. + Initrd *Initrd `json:"initrd,omitempty"` // VCPU specifies how many vcpu the machine will be allocated. VCPU int64 `json:"vcpu"` // MemoryInMb is the amount of memory in megabytes that the machine will be allocated. @@ -23,7 +25,22 @@ type MicroVMSpec struct { // NetworkInterfaces specifies the network interfaces attached to the machine. NetworkInterfaces []NetworkInterface `json:"network_interfaces"` // Volumes specifies the volumes to be attached to the the machine. - Volumes []Volume `json:"volumes"` + Volumes Volumes `json:"volumes"` + // Metadata allows you to specify data to be added to the metadata service. The key is the name + // of the metadata item and the value is the base64 encoded contents of the metadata. + Metadata map[string]string `json:"metadata"` +} + +// MicroVMStatus contains the runtime status of the microvm. +type MicroVMStatus struct { + // Volumes holds the status of the volumes. + Volumes VolumeStatuses + // KernelMount holds the status of the kernel mount point. + KernelMount *Mount + // InitrdMount holds the status of the initrd mount point. + InitrdMount *Mount + // NetworkInterfaces holds the status of the network interfaces. + NetworkInterfaces NetworkInterfaceStatuses } // Kernel is the specification of the kernel and its arguments. @@ -34,76 +51,16 @@ type Kernel struct { Filename string // CmdLine are the args to use for the kernel cmdline. CmdLine string `json:"cmdline,omitempty"` + // AddNetworkConfig if set to true indicates that the network-config kernel argument should be generated. + AddNetworkConfig bool `json:"add_network_config"` } -// ContainerImage represents the address of a OCI image. -type ContainerImage string - -// NetworkInterface represents a network interface for the microvm. -type NetworkInterface struct { - // AllowMetadataRequests indicates that this interface can be used for metadata requests. - // TODO: we may hide this within the firecracker plugin. - AllowMetadataRequests bool `json:"allow_mmds,omitempty"` - // GuestMAC allows the specifying of a specifi MAC address to use for the interface. If - // not supplied a autogenerated MAC address will be used. - GuestMAC string `json:"guest_mac,omitempty"` - // HostDeviceName is the name of the network interface to use from the host. This will be - // a tuntap or macvtap interface. - HostDeviceName string `json:"host_device_name"` - // GuestDeviceName is the name of the network interface to create in the microvm. If this - // is not supplied than a device name will be assigned automatically. - GuestDeviceName string `json:"guest_device_name,omitempty"` - // TODO: add rate limiting. - // TODO: add CNI. -} - -// Volume represents a volume to be attached to a microvm machine. -type Volume struct { - // ID is the uinique identifier of the volume. - ID string `json:"id"` - // IsRoot specifies that the volume is to be used as the root volume. A machine - // must have a root volume. - IsRoot bool `json:"is_root"` - // IsReadOnly specifies that the volume is to be mounted readonly. - IsReadOnly bool `json:"is_read_only,omitempty"` - // MountPoint is the mount point for the volume in the microvm. - MountPoint string `json:"mount_point"` - // Source is where the volume will be sourced from. - Source VolumeSource `json:"source"` - // PartitionID is the uuid of the boot partition. - PartitionID string `json:"partition_id,omitempty"` - // Size is the size to resize this volume to. - Size int32 `json:"size,omitempty"` - // TODO: add rate limiting. -} - -// VolumeSource is the source of a volume. Based loosely on the volumes in Kubernetes Pod specs. -type VolumeSource struct { - // Container is used to specify a source of a volume as a OCI container. - Container *ContainerVolumeSource `json:"container,omitempty"` - // HostPath is used to specify a source of a volume as a file/device on the host. - HostPath *HostPathVolumeSource `json:"host_path,omitempty"` - // TODO: add CSI. -} - -// ContainerDriveSource represents the details of a volume coming from a OCI image. -type ContainerVolumeSource struct { - // Image is the OCI image to use. +type Initrd struct { + // Image is the container image to use for the initrd. Image ContainerImage `json:"image"` + // Filename is the name of the initrd filename in the container. + Filename string } -// HostPathVolumeSource represents the details of a volume coming from a file/device on the host. -type HostPathVolumeSource struct { - // Path on the host of the file/device to use as a source for a volume. - Path string - // Type is the type of file/device on the host. - Type HostPathType -} - -// HostPathType is a type representing the different type of files/devices. -type HostPathType string - -const ( - // HostPathRawFile represents a file on the host to use as a source for a volume. It should be a raw fs. - HostPathRawFile HostPathType = "RawFile" -) +// ContainerImage represents the address of a OCI image. +type ContainerImage string diff --git a/core/models/network.go b/core/models/network.go new file mode 100644 index 00000000..42501d2b --- /dev/null +++ b/core/models/network.go @@ -0,0 +1,44 @@ +package models + +// NetworkInterface represents a network interface for the microvm. +type NetworkInterface struct { + // GuestDeviceName is the name of the network interface to create in the microvm. + GuestDeviceName string `json:"guest_device_name"` + // AllowMetadataRequests indicates that this interface can be used for metadata requests. + // TODO: we may hide this within the firecracker plugin. + AllowMetadataRequests bool `json:"allow_mmds,omitempty"` + // GuestMAC allows the specifying of a specifi MAC address to use for the interface. If + // not supplied a autogenerated MAC address will be used. + GuestMAC string `json:"guest_mac,omitempty"` + // Type is the type of host network interface type to create to use by the guest. + Type IfaceType `json:"type"` + // Address is an optional IP address to assign to this interface. If not supplied then DHCP will be used. + Address string `json:"address,omitempty"` + // TODO: add rate limiting. + // TODO: add CNI. +} + +type NetworkInterfaceStatus struct { + // HostDeviceName is the name of the network interface used from the host. This will be + // a tuntap or macvtap interface. + HostDeviceName string `json:"host_device_name"` + // Index is the index of the network interface on the host. + Index int + // MACAddress is the MAC address of the host interface. + MACAddress string +} + +// NetworkInterfaceStatuses is a collection of network interfaces. +type NetworkInterfaceStatuses map[string]*NetworkInterfaceStatus + +// IfaceType is a type representing the supported network interface types. +type IfaceType string + +const ( + // IfaceTypeTap is a TAP network interface. + IfaceTypeTap = "tap" + // IfaceTypeMacvtap is a MACVTAP network interface. + IfaceTypeMacvtap = "macvtap" + // IfaceTypeUnsupported is a type that represents an unsupported network interface type. + IfaceTypeUnsupported = "unsupported" +) diff --git a/core/models/vmid.go b/core/models/vmid.go index 77e3caca..7fe08dc0 100644 --- a/core/models/vmid.go +++ b/core/models/vmid.go @@ -60,7 +60,7 @@ func (v *VMID) Namespace() string { } // String returns a string representation of the vmid. -func (v *VMID) String() string { +func (v VMID) String() string { return fmt.Sprintf("%s/%s", v.namespace, v.name) } diff --git a/core/models/volumes.go b/core/models/volumes.go index a4eb7665..622bff89 100644 --- a/core/models/volumes.go +++ b/core/models/volumes.go @@ -1,5 +1,52 @@ package models +// Volume represents a volume to be attached to a microvm machine. +type Volume struct { + // ID is the uinique identifier of the volume. + ID string `json:"id"` + // IsRoot specifies that the volume is to be used as the root volume. A machine + // must have a root volume. + IsRoot bool `json:"is_root"` + // IsReadOnly specifies that the volume is to be mounted readonly. + IsReadOnly bool `json:"is_read_only,omitempty"` + // MountPoint is the mount point for the volume in the microvm. + MountPoint string `json:"mount_point"` + // Source is where the volume will be sourced from. + Source VolumeSource `json:"source"` + // PartitionID is the uuid of the boot partition. + PartitionID string `json:"partition_id,omitempty"` + // Size is the size to resize this volume to. + Size int32 `json:"size,omitempty"` + // TODO: add rate limiting. +} + +// Volumes represents a collection of volumes. +type Volumes []Volume + +// GetByID will get a volume status by id. +func (v Volumes) GetByID(id string) *Volume { + for _, vol := range v { + if vol.ID == id { + return &vol + } + } + + return nil +} + +// VolumeSource is the source of a volume. Based loosely on the volumes in Kubernetes Pod specs. +type VolumeSource struct { + // Container is used to specify a source of a volume as a OCI container. + Container *ContainerVolumeSource `json:"container,omitempty"` + // TODO: add CSI. +} + +// ContainerDriveSource represents the details of a volume coming from a OCI image. +type ContainerVolumeSource struct { + // Image is the OCI image to use. + Image ContainerImage `json:"image"` +} + // Mount represents a volume mount point. type Mount struct { // Type specifies the type of the mount (e.g. device or directory). @@ -29,3 +76,12 @@ const ( // ImageUseKernel represents the usage of af an image for a initial ramdisk. ImageUseInitrd ImageUse = "initrd" ) + +// VolumeStatus holds status information about the volumes. +type VolumeStatus struct { + // Mount is the mount point information for the volume. + Mount Mount `json:"mount"` +} + +// VolumeStatuses represents a collection of volume status. +type VolumeStatuses map[string]*VolumeStatus diff --git a/core/plans/microvm_create.go b/core/plans/microvm_create.go new file mode 100644 index 00000000..71151042 --- /dev/null +++ b/core/plans/microvm_create.go @@ -0,0 +1,147 @@ +package plans + +import ( + "context" + "fmt" + + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + portsctx "github.com/weaveworks/reignite/core/ports/context" + "github.com/weaveworks/reignite/core/steps/microvm" + "github.com/weaveworks/reignite/core/steps/network" + "github.com/weaveworks/reignite/core/steps/runtime" + "github.com/weaveworks/reignite/pkg/defaults" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +type CreatePlanInput struct { + StateDirectory string + VM *models.MicroVM +} + +func MicroVMCreatePlan(input *CreatePlanInput) planner.Plan { + return µvmCreatePlan{ + vm: input.VM, + stateDir: input.StateDirectory, + steps: []planner.Procedure{}, + } +} + +type microvmCreatePlan struct { + vm *models.MicroVM + stateDir string + + steps []planner.Procedure +} + +func (p *microvmCreatePlan) Name() string { + return "microvm_create" +} + +// Create will create the plan to reconcile a microvm. +func (p *microvmCreatePlan) Create(ctx context.Context) ([]planner.Procedure, error) { + logger := log.GetLogger(ctx) + logger.Debugf("creating plan for microvm %s", p.vm.ID) + + ports, ok := portsctx.GetPorts(ctx) + if !ok { + return nil, portsctx.ErrPortsMissing + } + + // TODO: test for deletion and if deleted don't proceed + + p.ensureStatus() + p.steps = []planner.Procedure{} + if err := p.addStep(ctx, runtime.NewCreateDirectory(p.stateDir, defaults.DataDirPerm, ports.FileSystem)); err != nil { + return nil, fmt.Errorf("adding root dir step: %w", err) + } + + // Images + if err := p.addImageSteps(ctx, p.vm, ports.ImageService); err != nil { + return nil, fmt.Errorf("adding image steps: %w", err) + } + + // Network interfaces + if err := p.addNetworkSteps(ctx, p.vm, ports.NetworkService); err != nil { + return nil, fmt.Errorf("adding network steps: %w", err) + } + + // MicroVM provider create + if err := p.addStep(ctx, microvm.NewCreateStep(p.vm, ports.Provider)); err != nil { + return nil, fmt.Errorf("adding microvm create step: %w", err) + } + + return p.steps, nil +} + +// Result is the result of the plan. +func (p *microvmCreatePlan) Result() interface{} { + return nil +} + +func (p *microvmCreatePlan) addStep(ctx context.Context, step planner.Procedure) error { + shouldDo, err := step.ShouldDo(ctx) + if err != nil { + return fmt.Errorf("checking if step %s should be included in plan: %w", step.Name(), err) + } + + if shouldDo { + p.steps = append(p.steps, step) + } + + return nil +} + +func (p *microvmCreatePlan) addImageSteps(ctx context.Context, vm *models.MicroVM, imageSvc ports.ImageService) error { + for i := range vm.Spec.Volumes { + vol := vm.Spec.Volumes[i] + status, ok := vm.Status.Volumes[vol.ID] + if !ok { + status = &models.VolumeStatus{} + vm.Status.Volumes[vol.ID] = status + } + if vol.Source.Container != nil { + if err := p.addStep(ctx, runtime.NewVolumeMount(&vm.ID, &vol, status, imageSvc)); err != nil { + return fmt.Errorf("adding volume mount step: %w", err) + } + } + } + if string(vm.Spec.Kernel.Image) != "" { + if err := p.addStep(ctx, runtime.NewKernelMount(vm, imageSvc)); err != nil { + return fmt.Errorf("adding kernel mount step: %w", err) + } + } + if vm.Spec.Initrd != nil { + if err := p.addStep(ctx, runtime.NewInitrdMount(vm, imageSvc)); err != nil { + return fmt.Errorf("adding initrd mount step: %w", err) + } + } + + return nil +} + +func (p *microvmCreatePlan) addNetworkSteps(ctx context.Context, vm *models.MicroVM, networkSvc ports.NetworkService) error { + for i := range vm.Spec.NetworkInterfaces { + iface := vm.Spec.NetworkInterfaces[i] + status, ok := vm.Status.NetworkInterfaces[iface.GuestDeviceName] + if !ok { + status = &models.NetworkInterfaceStatus{} + vm.Status.NetworkInterfaces[iface.GuestDeviceName] = status + } + if err := p.addStep(ctx, network.NewNetworkInterface(&vm.ID, &iface, status, networkSvc)); err != nil { + return fmt.Errorf("adding create network interface step: %w", err) + } + } + + return nil +} + +func (p *microvmCreatePlan) ensureStatus() { + if p.vm.Status.Volumes == nil { + p.vm.Status.Volumes = models.VolumeStatuses{} + } + if p.vm.Status.NetworkInterfaces == nil { + p.vm.Status.NetworkInterfaces = models.NetworkInterfaceStatuses{} + } +} diff --git a/core/plans/types.go b/core/plans/types.go new file mode 100644 index 00000000..c9603650 --- /dev/null +++ b/core/plans/types.go @@ -0,0 +1,17 @@ +package plans + +import ( + "github.com/spf13/afero" + "github.com/weaveworks/reignite/core/ports" +) + +// Providers input is a type to be used as input to plans. +type ProvidersInput struct { + MicroVMService ports.MicroVMService + MicroVMRepo ports.MicroVMRepository + EventService ports.EventService + ImageService ports.ImageService + NetworkService ports.NetworkService + + FS afero.Fs +} diff --git a/core/ports/collection.go b/core/ports/collection.go new file mode 100644 index 00000000..6d7e4b65 --- /dev/null +++ b/core/ports/collection.go @@ -0,0 +1,13 @@ +package ports + +import "github.com/spf13/afero" + +type Collection struct { + Repo MicroVMRepository + Provider MicroVMService + EventService EventService + IdentifierService IDService + NetworkService NetworkService + ImageService ImageService + FileSystem afero.Fs +} diff --git a/core/ports/context/context.go b/core/ports/context/context.go new file mode 100644 index 00000000..146badad --- /dev/null +++ b/core/ports/context/context.go @@ -0,0 +1,25 @@ +package ports + +import ( + "context" + "errors" + + "github.com/weaveworks/reignite/core/ports" +) + +type portsCtxKeyType string + +const portsKey portsCtxKeyType = "reignited.ports" + +var ErrPortsMissing = errors.New("ports collection not in the context") + +func WithPorts(ctx context.Context, ports *ports.Collection) context.Context { + return context.WithValue(ctx, portsKey, ports) +} + +// GetPorts will get the ports from the context. +func GetPorts(ctx context.Context) (*ports.Collection, bool) { + ports, ok := ctx.Value(portsKey).(*ports.Collection) + + return ports, ok +} diff --git a/core/ports/providers.go b/core/ports/providers.go deleted file mode 100644 index 3f46c1f1..00000000 --- a/core/ports/providers.go +++ /dev/null @@ -1,29 +0,0 @@ -package ports - -import ( - "context" - - "github.com/weaveworks/reignite/core/models" -) - -// MicroVMProvider is the port definition for a microvm provider. -type MicroVMProvider interface { - // Capabilities returns a list of the capabilities the provider supports. - Capabilities() models.Capabilities - - // CreateVM will create a new microvm. - CreateVM(ctx context.Context, vm *models.MicroVM) (*models.MicroVM, error) - // StartVM will start a created microvm. - StartVM(ctx context.Context, id string) error - // PauseVM will pause a started microvm. - PauseVM(ctx context.Context, id string) error - // ResumeVM will resume a paused microvm. - ResumeVM(ctx context.Context, id string) error - // StopVM will stop a paused or running microvm. - StopVM(ctx context.Context, id string) error - // DeleteVM will delete a VM and its runtime state. - DeleteVM(ctx context.Context, id string) error - - // ListVMs will return a list of the microvms. - ListVMs(ctx context.Context, count int) ([]*models.MicroVM, error) -} diff --git a/core/ports/repositories.go b/core/ports/repositories.go index df221a09..fc773193 100644 --- a/core/ports/repositories.go +++ b/core/ports/repositories.go @@ -14,7 +14,8 @@ type MicroVMRepository interface { Delete(ctx context.Context, microvm *models.MicroVM) error // Get will get the microvm spec with the given name/namespace. Get(ctx context.Context, name, namespace string) (*models.MicroVM, error) - // GetAll will get a list of microvm details. + // GetAll will get a list of microvm details. If namespace is an empty string all + // details of microvms will be returned. GetAll(ctx context.Context, namespace string) ([]*models.MicroVM, error) // Exists checks to see if the microvm spec exists in the repo. Exists(ctx context.Context, name, namespace string) (bool, error) diff --git a/core/ports/services.go b/core/ports/services.go index 6d91f36e..30fc4069 100644 --- a/core/ports/services.go +++ b/core/ports/services.go @@ -8,6 +8,27 @@ import ( "github.com/weaveworks/reignite/core/models" ) +// MicroVMService is the port definition for a microvm service. +type MicroVMService interface { + // Capabilities returns a list of the capabilities the provider supports. + Capabilities() models.Capabilities + + // CreateVM will create a new microvm. + Create(ctx context.Context, vm *models.MicroVM) error + // DeleteVM will delete a VM and its runtime state. + Delete(ctx context.Context, id string) error + // StartVM will start a created microvm. + Start(ctx context.Context, id string) error + // PauseVM will pause a started microvm. + Pause(ctx context.Context, id string) error + // ResumeVM will resume a paused microvm. + Resume(ctx context.Context, id string) error + // StopVM will stop a paused or running microvm. + Stop(ctx context.Context, id string) error + // IsRunning returns true if the microvm is running. + IsRunning(ctx context.Context, id string) (bool, error) +} + // MicroVMGRPCService is a port for a microvm grpc service. type MicroVMGRPCService interface { mvmv1.MicroVMServer @@ -40,21 +61,71 @@ type EventEnvelope struct { // ImageService is a port for a service that interacts with OCI images. type ImageService interface { - // Get will get (i.e. pull) the image for a specific owner. - Get(ctx context.Context, input GetImageInput) error - // GetAndMount will get (i.e. pull) the image for a specific owner and then + // Pull will get (i.e. pull) the image for a specific owner. + Pull(ctx context.Context, input *ImageSpec) error + // PullAndMount will get (i.e. pull) the image for a specific owner and then // make it available via a mount point. - GetAndMount(ctx context.Context, input GetImageInput) ([]models.Mount, error) + PullAndMount(ctx context.Context, input *ImageMountSpec) ([]models.Mount, error) + // Exists checks if the image already exists on the machine. + Exists(ctx context.Context, input *ImageSpec) (bool, error) + // IsMounted checks if the image is pulled and mounted. + IsMounted(ctx context.Context, input *ImageMountSpec) (bool, error) +} + +type ImageSpec struct { + // ImageName is the name of the image to get. + ImageName string + // Owner is the name of the owner of the image. + Owner string } -// GetImageInput is the input to getting a image. -type GetImageInput struct { +// ImageMountSpec is the declaration of an image that needs to be pulled and mounted. +type ImageMountSpec struct { // ImageName is the name of the image to get. ImageName string - // OwnerName is the name of the owner of the image. - OwnerName string - // OwnerNamespace is the namespace of the owner of the image. - OwnerNamespace string - // Use is an indoicator of what the image will be used for. + // Owner is the name of the owner of the image. + Owner string + // Use is an indicator of what the image will be used for. Use models.ImageUse + // OwnerUsageID is an identifier from the owner. + OwnerUsageID string +} + +// NetworkService is a port for a service that interacts with the network +// stack on the host machine. +type NetworkService interface { + // IfaceCreate will create the network interface. + IfaceCreate(ctx context.Context, input IfaceCreateInput) (*IfaceDetails, error) + // IfaceDelete is used to delete a network interface + IfaceDelete(ctx context.Context, input DeleteIfaceInput) error + // IfaceExists will check if an interface with the given name exists + IfaceExists(ctx context.Context, name string) (bool, error) + // IfaceDetails will get the details of the supplied network interface. + IfaceDetails(ctx context.Context, name string) (*IfaceDetails, error) +} + +type IfaceCreateInput struct { + // DeviceName is the name of the network interface to create on the host. + DeviceName string + // Type is the type of network interface to create. + Type models.IfaceType + // MAC allows the specifying of a specific MAC address to use for the interface. If + // not supplied a autogenerated MAC address will be used. + MAC string +} + +type IfaceDetails struct { + // DeviceName is the name of the network interface created on the host. + DeviceName string + // Type is the type of network interface created. + Type models.IfaceType + // MAC is the MAC address of the created interface. + MAC string + // Index is the network interface index on the host. + Index int +} + +type DeleteIfaceInput struct { + // DeviceName is the name of the network interface to delete from the host. + DeviceName string } diff --git a/core/ports/usecases.go b/core/ports/usecases.go index f976c33f..4fde9f85 100644 --- a/core/ports/usecases.go +++ b/core/ports/usecases.go @@ -28,4 +28,7 @@ type MicroVMQueryUseCases interface { type ReconcileMicroVMsUseCase interface { // ReconcileMicroVM is a use case for reconciling a specific microvm. ReconcileMicroVM(ctx context.Context, id, namespace string) error + // ResyncMicroVMs is used to resync the microvms. If a namespace is supplied then it will + // resync only the microvms in that namespaces. + ResyncMicroVMs(ctx context.Context, namespace string) error } diff --git a/core/steps/microvm/create.go b/core/steps/microvm/create.go new file mode 100644 index 00000000..6f27e44f --- /dev/null +++ b/core/steps/microvm/create.go @@ -0,0 +1,58 @@ +package microvm + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/weaveworks/reignite/core/errors" + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +func NewCreateStep(vm *models.MicroVM, vmSvc ports.MicroVMService) planner.Procedure { + return &createStep{ + vm: vm, + vmSvc: vmSvc, + } +} + +type createStep struct { + vm *models.MicroVM + vmSvc ports.MicroVMService +} + +// Name is the name of the procedure/operation. +func (s *createStep) Name() string { + return "microvm_create" +} + +func (s *createStep) ShouldDo(ctx context.Context) (bool, error) { + isRunning, err := s.vmSvc.IsRunning(ctx, s.vm.ID.String()) + if err != nil { + return false, fmt.Errorf("checking if microvm is running: %w", err) + } + + return !isRunning, nil +} + +// Do will perform the operation/procedure. +func (s *createStep) Do(ctx context.Context) ([]planner.Procedure, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "vmid": s.vm.ID, + }) + logger.Debug("creating microvm") + + if s.vm == nil { + return nil, errors.ErrSpecRequired + } + + if err := s.vmSvc.Create(ctx, s.vm); err != nil { + return nil, fmt.Errorf("creating microvm: %w", err) + } + + return nil, nil +} diff --git a/core/steps/network/interface_create.go b/core/steps/network/interface_create.go new file mode 100644 index 00000000..e5c48122 --- /dev/null +++ b/core/steps/network/interface_create.go @@ -0,0 +1,122 @@ +package network + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/weaveworks/reignite/core/errors" + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +const ( + tapFormat = "%s_%s_tap" + macvtapFormat = "%s_%s_vtap" +) + +func NewNetworkInterface(vmid *models.VMID, iface *models.NetworkInterface, status *models.NetworkInterfaceStatus, svc ports.NetworkService) planner.Procedure { + return &createInterface{ + vmid: vmid, + iface: iface, + svc: svc, + status: status, + } +} + +type createInterface struct { + vmid *models.VMID + iface *models.NetworkInterface + status *models.NetworkInterfaceStatus + + svc ports.NetworkService +} + +// Name is the name of the procedure/operation. +func (s *createInterface) Name() string { + return "network_iface_create" +} + +func (s *createInterface) ShouldDo(ctx context.Context) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "iface": s.iface.GuestDeviceName, + }) + logger.Debug("checking if procedure should be run") + + if s.status == nil || s.status.HostDeviceName == "" { + return true, nil + } + + deviceName := s.deviceName() + + exists, err := s.svc.IfaceExists(ctx, deviceName) + if err != nil { + return false, fmt.Errorf("checking if network interface %s exists: %w", deviceName, err) + } + + return !exists, nil +} + +// Do will perform the operation/procedure. +func (s *createInterface) Do(ctx context.Context) ([]planner.Procedure, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "iface": s.iface.GuestDeviceName, + }) + logger.Debug("running step to create network interface") + + if s.iface.GuestDeviceName == "" { + return nil, errors.ErrGuestDeviceNameRequired + } + + if s.status == nil { + s.status = &models.NetworkInterfaceStatus{} + } + + deviceName := s.deviceName() + + exists, err := s.svc.IfaceExists(ctx, deviceName) + if err != nil { + return nil, fmt.Errorf("checking if networking interface exists: %w", err) + } + if exists { + details, err := s.svc.IfaceDetails(ctx, deviceName) + if err != nil { + return nil, fmt.Errorf("getting interface details: %w", err) + } + + s.status.HostDeviceName = deviceName + s.status.Index = details.Index + s.status.MACAddress = details.MAC + + return nil, nil + } + + input := &ports.IfaceCreateInput{ + DeviceName: deviceName, + Type: s.iface.Type, + MAC: s.iface.GuestMAC, + } + + output, err := s.svc.IfaceCreate(ctx, *input) + if err != nil { + return nil, fmt.Errorf("creating network interface: %w", err) + } + + s.status.HostDeviceName = deviceName + s.status.Index = output.Index + s.status.MACAddress = output.MAC + + return nil, nil +} + +func (s *createInterface) deviceName() string { + if s.iface.Type == models.IfaceTypeMacvtap { + return fmt.Sprintf(macvtapFormat, s.vmid.Namespace(), s.vmid.Name()) + } + + return fmt.Sprintf(tapFormat, s.vmid.Namespace(), s.vmid.Name()) +} diff --git a/core/steps/runtime/dir_create.go b/core/steps/runtime/dir_create.go new file mode 100644 index 00000000..407b02a0 --- /dev/null +++ b/core/steps/runtime/dir_create.go @@ -0,0 +1,103 @@ +package runtime + +import ( + "context" + "fmt" + "os" + + "github.com/sirupsen/logrus" + "github.com/spf13/afero" + + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +func NewCreateDirectory(dir string, mode os.FileMode, fs afero.Fs) planner.Procedure { + return &createDirectory{ + dir: dir, + mode: mode, + fs: fs, + } +} + +type createDirectory struct { + dir string + mode os.FileMode + fs afero.Fs +} + +// Name is the name of the procedure/operation. +func (s *createDirectory) Name() string { + return "io_create_dir" +} + +func (s *createDirectory) ShouldDo(ctx context.Context) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "dir": s.dir, + "mode": s.mode.String(), + }) + logger.Debug("checking if procedure should be run") + + logger.Trace("checking if directory exists") + exists, err := s.directoryExists() + if err != nil { + return false, err + } + + if !exists { + return true, nil + } + + logger.Trace("checking directory permissions") + info, err := s.fs.Stat(s.dir) + if err != nil { + return false, fmt.Errorf("doing stat on %s: %w", s.dir, err) + } + + expectedDirMode := s.mode | os.ModeDir + if expectedDirMode.String() != info.Mode().String() { + logger.Trace("permissions for directory match don't match") + + return true, nil + } + + return false, nil +} + +// Do will perform the operation/procedure. +func (s *createDirectory) Do(ctx context.Context) ([]planner.Procedure, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "dir": s.dir, + "mode": s.mode.String(), + }) + logger.Debug("running step to create directory") + + exists, err := s.directoryExists() + if err != nil { + return nil, err + } + if !exists { + logger.Trace("creating directory") + if err := s.fs.Mkdir(s.dir, s.mode); err != nil { + return nil, fmt.Errorf("creating directory %s: %w", s.dir, err) + } + } + + logger.Trace("setting permissions for directory") + if err := s.fs.Chmod(s.dir, s.mode); err != nil { + return nil, fmt.Errorf("changing directory permissions for %s: %w", s.dir, err) + } + + return nil, nil +} + +func (s *createDirectory) directoryExists() (bool, error) { + exists, err := afero.DirExists(s.fs, s.dir) + if err != nil { + return false, fmt.Errorf("checking if dir %s exists: %w", s.dir, err) + } + + return exists, nil +} diff --git a/core/steps/runtime/dir_create_test.go b/core/steps/runtime/dir_create_test.go new file mode 100644 index 00000000..2a1e7fba --- /dev/null +++ b/core/steps/runtime/dir_create_test.go @@ -0,0 +1,83 @@ +package runtime_test + +import ( + "context" + "os" + "testing" + + . "github.com/onsi/gomega" + "github.com/spf13/afero" + + "github.com/weaveworks/reignite/core/steps/runtime" +) + +func TestCreateDirectory_NotExists(t *testing.T) { + RegisterTestingT(t) + + testDir := "/test/dir" + testMode := os.ModePerm + dirMode := testMode | os.ModeDir + + fs := afero.NewMemMapFs() + ctx := context.Background() + + step := runtime.NewCreateDirectory(testDir, testMode, fs) + childSteps, err := step.Do(ctx) + + Expect(err).NotTo(HaveOccurred()) + Expect(len(childSteps)).To(Equal(0)) + + testDirExists(t, testDir, dirMode, fs) +} + +func TestCreateDirectory_Exists(t *testing.T) { + RegisterTestingT(t) + + testDir := "/test/dir" + testMode := os.ModePerm + dirMode := testMode | os.ModeDir + + fs := afero.NewMemMapFs() + + err := fs.Mkdir(testDir, testMode) + Expect(err).NotTo(HaveOccurred()) + + ctx := context.Background() + step := runtime.NewCreateDirectory(testDir, testMode, fs) + childSteps, err := step.Do(ctx) + + Expect(err).NotTo(HaveOccurred()) + Expect(len(childSteps)).To(Equal(0)) + + testDirExists(t, testDir, dirMode, fs) +} + +func TestCreateDirectory_ExistsButChangeMode(t *testing.T) { + RegisterTestingT(t) + + testDir := "/test/dir" + createMode := os.FileMode(0o644) + changeMode := os.FileMode(0o755) + dirMode := changeMode | os.ModeDir + + fs := afero.NewMemMapFs() + + err := fs.Mkdir(testDir, createMode) + Expect(err).NotTo(HaveOccurred()) + + ctx := context.Background() + step := runtime.NewCreateDirectory(testDir, changeMode, fs) + childSteps, err := step.Do(ctx) + + Expect(err).NotTo(HaveOccurred()) + Expect(len(childSteps)).To(Equal(0)) + + testDirExists(t, testDir, dirMode, fs) +} + +func testDirExists(t *testing.T, dir string, mode os.FileMode, fs afero.Fs) { + info, err := fs.Stat(dir) + Expect(err).NotTo(HaveOccurred()) + Expect(info.IsDir()).To(BeTrue()) + Expect(info.Mode().String()).To(Equal(mode.String())) +} diff --git a/core/steps/runtime/initrd_mount.go b/core/steps/runtime/initrd_mount.go new file mode 100644 index 00000000..91f44b59 --- /dev/null +++ b/core/steps/runtime/initrd_mount.go @@ -0,0 +1,87 @@ +package runtime + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + cerrs "github.com/weaveworks/reignite/core/errors" + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +func NewInitrdMount(vm *models.MicroVM, imageService ports.ImageService) planner.Procedure { + return &initrdMount{ + vm: vm, + imageSvc: imageService, + } +} + +type initrdMount struct { + vm *models.MicroVM + imageSvc ports.ImageService +} + +// Name is the name of the procedure/operation. +func (s *initrdMount) Name() string { + return "runtime_initrd_mount" +} + +func (s *initrdMount) ShouldDo(ctx context.Context) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "image": s.vm.Spec.Kernel.Image, + }) + logger.Debug("checking if procedure should be run") + + if s.vm.Spec.Initrd == nil { + return false, nil + } + + if s.vm.Status.InitrdMount == nil || s.vm.Status.InitrdMount.Source == "" { + return true, nil + } + + input := s.getMountSpec() + mounted, err := s.imageSvc.IsMounted(ctx, input) + if err != nil { + return false, fmt.Errorf("checking if image %s is mounted: %w", input.ImageName, err) + } + + return !mounted, nil +} + +// Do will perform the operation/procedure. +func (s *initrdMount) Do(ctx context.Context) ([]planner.Procedure, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "image": s.vm.Spec.Kernel.Image, + }) + logger.Debug("running step to mount initrd image") + + input := s.getMountSpec() + + mounts, err := s.imageSvc.PullAndMount(ctx, input) + if err != nil { + return nil, fmt.Errorf("mount images %s for initrd use: %w", input.ImageName, err) + } + if len(mounts) == 0 { + return nil, cerrs.ErrNoMount + } + + s.vm.Status.InitrdMount = &mounts[0] + + return nil, nil +} + +func (s *initrdMount) getMountSpec() *ports.ImageMountSpec { + return &ports.ImageMountSpec{ + ImageName: string(s.vm.Spec.Initrd.Image), + Owner: s.vm.ID.String(), + OwnerUsageID: "initrd", + Use: models.ImageUseInitrd, + } +} diff --git a/core/steps/runtime/kernel_mount.go b/core/steps/runtime/kernel_mount.go new file mode 100644 index 00000000..f63b8d50 --- /dev/null +++ b/core/steps/runtime/kernel_mount.go @@ -0,0 +1,87 @@ +package runtime + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + cerrs "github.com/weaveworks/reignite/core/errors" + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +func NewKernelMount(vm *models.MicroVM, imageService ports.ImageService) planner.Procedure { + return &kernelMount{ + vm: vm, + imageSvc: imageService, + } +} + +type kernelMount struct { + vm *models.MicroVM + imageSvc ports.ImageService +} + +// Name is the name of the procedure/operation. +func (s *kernelMount) Name() string { + return "runtime_kernel_mount" +} + +func (s *kernelMount) ShouldDo(ctx context.Context) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "image": s.vm.Spec.Kernel.Image, + }) + logger.Debug("checking if procedure should be run") + + if s.vm.Status.KernelMount == nil || s.vm.Status.KernelMount.Source == "" { + return true, nil + } + + input := s.getMountSpec() + mounted, err := s.imageSvc.IsMounted(ctx, input) + if err != nil { + return false, fmt.Errorf("checking if image %s is mounted: %w", input.ImageName, err) + } + + return !mounted, nil +} + +// Do will perform the operation/procedure. +func (s *kernelMount) Do(ctx context.Context) ([]planner.Procedure, error) { + if s.vm.Spec.Kernel.Image == "" { + return nil, cerrs.ErrKernelImageRequired + } + + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "image": s.vm.Spec.Kernel.Image, + }) + logger.Debug("running step to mount kernel image") + + input := s.getMountSpec() + + mounts, err := s.imageSvc.PullAndMount(ctx, input) + if err != nil { + return nil, fmt.Errorf("mount images %s for kernel use: %w", input.ImageName, err) + } + if len(mounts) == 0 { + return nil, cerrs.ErrNoMount + } + + s.vm.Status.KernelMount = &mounts[0] + + return nil, nil +} + +func (s *kernelMount) getMountSpec() *ports.ImageMountSpec { + return &ports.ImageMountSpec{ + ImageName: string(s.vm.Spec.Kernel.Image), + Owner: s.vm.ID.String(), + OwnerUsageID: "kernel", + Use: models.ImageUseKernel, + } +} diff --git a/core/steps/runtime/volume_mount.go b/core/steps/runtime/volume_mount.go new file mode 100644 index 00000000..072b09f6 --- /dev/null +++ b/core/steps/runtime/volume_mount.go @@ -0,0 +1,90 @@ +package runtime + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + cerrs "github.com/weaveworks/reignite/core/errors" + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/planner" +) + +func NewVolumeMount(vmid *models.VMID, volume *models.Volume, status *models.VolumeStatus, imageService ports.ImageService) planner.Procedure { + return &volumeMount{ + vmid: vmid, + volume: volume, + status: status, + imageSvc: imageService, + } +} + +type volumeMount struct { + vmid *models.VMID + volume *models.Volume + status *models.VolumeStatus + imageSvc ports.ImageService +} + +// Name is the name of the procedure/operation. +func (s *volumeMount) Name() string { + return "runtime_volume_mount" +} + +func (s *volumeMount) ShouldDo(ctx context.Context) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "id": s.volume.ID, + }) + logger.Debug("checking if procedure should be run") + + if s.status == nil || s.status.Mount.Source == "" { + return true, nil + } + + input := s.getMountSpec() + mounted, err := s.imageSvc.IsMounted(ctx, input) + if err != nil { + return false, fmt.Errorf("checking if image %s is mounted: %w", input.ImageName, err) + } + + return !mounted, nil +} + +// Do will perform the operation/procedure. +func (s *volumeMount) Do(ctx context.Context) ([]planner.Procedure, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "step": s.Name(), + "id": s.volume.ID, + }) + logger.Debug("running step to mount volume") + + input := s.getMountSpec() + + mounts, err := s.imageSvc.PullAndMount(ctx, input) + if err != nil { + return nil, fmt.Errorf("mount images %s for volume use: %w", input.ImageName, err) + } + if len(mounts) == 0 { + return nil, cerrs.ErrNoVolumeMount + } + + if s.status == nil { + s.status = &models.VolumeStatus{} + } + s.status.Mount = mounts[0] + + return nil, nil +} + +func (s *volumeMount) getMountSpec() *ports.ImageMountSpec { + return &ports.ImageMountSpec{ + ImageName: string(s.volume.Source.Container.Image), + Owner: s.vmid.String(), + OwnerUsageID: s.volume.ID, + Use: models.ImageUseVolume, + } +} diff --git a/go.mod b/go.mod index b795db71..974adc75 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.3.0 // indirect + github.com/google/wire v0.5.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 github.com/gruntwork-io/terratest v0.37.8 @@ -18,10 +19,14 @@ require ( github.com/opencontainers/image-spec v1.0.1 github.com/prometheus/client_golang v1.11.0 github.com/sirupsen/logrus v1.8.1 + github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.9.0 google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 google.golang.org/grpc v1.41.0 google.golang.org/protobuf v1.27.1 + github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 6148da90..8c7ffa97 100644 --- a/go.sum +++ b/go.sum @@ -595,12 +595,16 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -1151,6 +1155,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1378,6 +1383,7 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -1434,6 +1440,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 36da38b7..bc8245f0 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/golang/mock v1.6.0 github.com/golangci/golangci-lint v1.41.1 + github.com/google/wire v0.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 github.com/onsi/ginkgo v1.16.4 github.com/stretchr/objx v0.2.0 // indirect diff --git a/hack/tools/go.sum b/hack/tools/go.sum index e85cfe37..2e9e2d74 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -281,11 +281,15 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= @@ -884,6 +888,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= diff --git a/hack/tools/tools.go b/hack/tools/tools.go index 087c843c..8c4abf21 100644 --- a/hack/tools/tools.go +++ b/hack/tools/tools.go @@ -1,3 +1,4 @@ +//go:build tools // +build tools package tools @@ -5,6 +6,7 @@ package tools import ( _ "github.com/golang/mock/mockgen" _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "github.com/google/wire/cmd/wire" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" _ "github.com/onsi/ginkgo/ginkgo" diff --git a/infrastructure/containerd/config.go b/infrastructure/containerd/config.go index 628221ab..45dbb5d7 100644 --- a/infrastructure/containerd/config.go +++ b/infrastructure/containerd/config.go @@ -1,17 +1,6 @@ package containerd -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/weaveworks/reignite/pkg/defaults" -) - const ( - volSnapshotterFlagName = "containerd-volume-ss" - kernelSnapshotterFlagName = "containerd-kernel-ss" - socketPathFlagName = "containerd-socket" - supportedSnapshotters = "overlayfs,native,devmapper" ) @@ -23,24 +12,6 @@ type Config struct { SnapshotterVolume string // SocketPath is the path to the containerd socket. SocketPath string -} - -// AddFlagsToCommand will add the containerd image service specific flags to the supplied cobra command. -func AddFlagsToCommand(cmd *cobra.Command, config *Config) error { - cmd.Flags().StringVar(&config.SocketPath, - socketPathFlagName, - defaults.ContainerdSocket, - "The path to the containerd socket.") - - cmd.Flags().StringVar(&config.SnapshotterKernel, - kernelSnapshotterFlagName, - defaults.ContainerdSnapshotter, - fmt.Sprintf("The name of the snapshotter to use with containerd for kernel images. Options: %s", supportedSnapshotters)) - - cmd.Flags().StringVar(&config.SnapshotterVolume, - volSnapshotterFlagName, - defaults.ContainerdSnapshotter, - fmt.Sprintf("The name of the snapshotter to use with containerd for volume/initrd images. Options: %s", supportedSnapshotters)) - - return nil + // Namespace is the default containerd namespace to use + Namespace string } diff --git a/infrastructure/containerd/content.go b/infrastructure/containerd/content.go index c32b5aeb..c5da577c 100644 --- a/infrastructure/containerd/content.go +++ b/infrastructure/containerd/content.go @@ -16,6 +16,8 @@ var ( TypeLabel = fmt.Sprintf("%s/type", defaults.Domain) // VersionLabel is the name of the containerd content store label to hold version of the content. VersionLabel = fmt.Sprintf("%s/version", defaults.Domain) + // MicroVMSpecType is the type name for a microvm spec. + MicroVMSpecType = "microvm" ) func contentRefName(microvm *models.MicroVM) string { diff --git a/infrastructure/containerd/errors.go b/infrastructure/containerd/errors.go index d09409d0..138c692c 100644 --- a/infrastructure/containerd/errors.go +++ b/infrastructure/containerd/errors.go @@ -8,23 +8,6 @@ import ( // ErrFailedReadingContent is used when there is an error reading from the content store. var ErrFailedReadingContent = errors.New("failed reading from content store") -type errSpecNotFound struct { - name string - namespace string -} - -// Error returns the error message. -func (e errSpecNotFound) Error() string { - return fmt.Sprintf("microvm spec %s/%s not found", e.namespace, e.name) -} - -// IsSpecNotFound tests an error to see if its a spec not found error. -func IsSpecNotFound(err error) bool { - var e errSpecNotFound - - return errors.Is(err, e) -} - type errUnsupportedSnapshotter struct { name string } diff --git a/infrastructure/containerd/event_service.go b/infrastructure/containerd/event_service.go index dace3b52..e0b980ff 100644 --- a/infrastructure/containerd/event_service.go +++ b/infrastructure/containerd/event_service.go @@ -10,7 +10,6 @@ import ( "github.com/containerd/containerd/namespaces" "github.com/weaveworks/reignite/core/ports" - "github.com/weaveworks/reignite/pkg/defaults" ) func NewEventService(cfg *Config) (ports.EventService, error) { @@ -19,22 +18,24 @@ func NewEventService(cfg *Config) (ports.EventService, error) { return nil, fmt.Errorf("creating containerd client: %w", err) } - return NewEventServiceWithClient(client), nil + return NewEventServiceWithClient(cfg, client), nil } -func NewEventServiceWithClient(client *containerd.Client) ports.EventService { +func NewEventServiceWithClient(cfg *Config, client *containerd.Client) ports.EventService { return &eventService{ client: client, + cfg: cfg, } } type eventService struct { client *containerd.Client + cfg *Config } // Publish will publish an event to a specific topic. func (es *eventService) Publish(ctx context.Context, topic string, eventToPublish interface{}) error { - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + namespaceCtx := namespaces.WithNamespace(ctx, es.cfg.Namespace) ctrEventSrv := es.client.EventService() if err := ctrEventSrv.Publish(namespaceCtx, topic, eventToPublish); err != nil { return fmt.Errorf("publishing event: %w", err) @@ -74,7 +75,7 @@ func (es *eventService) subscribe(ctx context.Context, filters ...string) (ch <- errs = evtErrCh ch = evtCh - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + namespaceCtx := namespaces.WithNamespace(ctx, es.cfg.Namespace) var ctrEvents <-chan *events.Envelope var ctrErrs <-chan error diff --git a/infrastructure/containerd/event_service_test.go b/infrastructure/containerd/event_service_test.go index c3886d4c..d2da3b1f 100644 --- a/infrastructure/containerd/event_service_test.go +++ b/infrastructure/containerd/event_service_test.go @@ -20,7 +20,11 @@ func TestEventService_Integration(t *testing.T) { client, ctx := testCreateClient(t) - es := containerd.NewEventServiceWithClient(client) + es := containerd.NewEventServiceWithClient(&containerd.Config{ + SnapshotterKernel: testSnapshotter, + SnapshotterVolume: testSnapshotter, + Namespace: testContainerdNs, + }, client) t.Log("creating subscribers") diff --git a/infrastructure/containerd/image_service.go b/infrastructure/containerd/image_service.go index f2483668..2023afb4 100644 --- a/infrastructure/containerd/image_service.go +++ b/infrastructure/containerd/image_service.go @@ -4,14 +4,16 @@ import ( "context" "fmt" + "github.com/containerd/containerd/snapshots" + "github.com/containerd/containerd" + "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/namespaces" "github.com/sirupsen/logrus" "github.com/weaveworks/reignite/core/models" "github.com/weaveworks/reignite/core/ports" - "github.com/weaveworks/reignite/pkg/defaults" "github.com/weaveworks/reignite/pkg/log" ) @@ -38,15 +40,15 @@ type imageService struct { config *Config } -// Get will get (i.e. pull) the image for a specific owner. -func (im *imageService) Get(ctx context.Context, input ports.GetImageInput) error { +// Pull will get (i.e. pull) the image for a specific owner. +func (im *imageService) Pull(ctx context.Context, input *ports.ImageSpec) error { logger := log.GetLogger(ctx).WithField("service", "containerd_image") - actionMessage := fmt.Sprintf("getting image %s for owner %s/%s", input.ImageName, input.OwnerNamespace, input.OwnerName) + actionMessage := fmt.Sprintf("getting image %s for owner %s", input.ImageName, input.Owner) logger.Debugf(actionMessage) - nsCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + nsCtx := namespaces.WithNamespace(ctx, im.config.Namespace) - _, err := im.getImage(nsCtx, input.ImageName, input.OwnerName, input.OwnerNamespace) + _, err := im.pullImage(nsCtx, input.ImageName, input.Owner) if err != nil { return fmt.Errorf("%s: %w", actionMessage, err) } @@ -54,34 +56,101 @@ func (im *imageService) Get(ctx context.Context, input ports.GetImageInput) erro return nil } -// Get will get (i.e. pull) the image for a specific owner and then -// make it available via a mount point. -func (im *imageService) GetAndMount(ctx context.Context, input ports.GetImageInput) ([]models.Mount, error) { +// PullAndMount will get (i.e. pull) the image for a specific owner and then make it available via a mount point. +func (im *imageService) PullAndMount(ctx context.Context, input *ports.ImageMountSpec) ([]models.Mount, error) { + logger := log.GetLogger(ctx).WithField("service", "containerd_image") + logger.Debugf("getting and mounting image %s for owner %s", input.ImageName, input.Owner) + + nsCtx := namespaces.WithNamespace(ctx, im.config.Namespace) + leaseCtx, err := withOwnerLease(nsCtx, input.Owner, im.client) + if err != nil { + return nil, fmt.Errorf("getting lease for image pulling and mounting: %w", err) + } + + var image containerd.Image + exists, image, err := im.imageExists(leaseCtx, input.ImageName, input.Owner) + if err != nil { + return nil, fmt.Errorf("checking if image %s exists for owner %s: %w", input.ImageName, input.Owner, err) + } + + if !exists { + image, err = im.pullImage(leaseCtx, input.ImageName, input.Owner) + if err != nil { + return nil, fmt.Errorf("getting image %s for owner %s: %w", input.ImageName, input.Owner, err) + } + } + ss := im.getSnapshotter(input.Use) + + return im.snapshotAndMount(leaseCtx, image, input.Owner, input.OwnerUsageID, ss, logger) +} + +// Exists checks if the image already exists on the machine. +func (im *imageService) Exists(ctx context.Context, input *ports.ImageSpec) (bool, error) { + logger := log.GetLogger(ctx).WithField("service", "containerd_image") + logger.Debugf("checking if image %s exists for owner %s", input.ImageName, input.Owner) + + nsCtx := namespaces.WithNamespace(ctx, im.config.Namespace) + + exists, _, err := im.imageExists(nsCtx, input.ImageName, input.Owner) + if err != nil { + return false, fmt.Errorf("checking image exists: %w", err) + } + + return exists, nil +} + +// IsMounted checks if the image is pulled and mounted. +func (im *imageService) IsMounted(ctx context.Context, input *ports.ImageMountSpec) (bool, error) { logger := log.GetLogger(ctx).WithField("service", "containerd_image") - logger.Debugf("getting and mounting image %s for owner %s/%s", input.ImageName, input.OwnerNamespace, input.OwnerName) + logger.Debugf("checking if image %s exists and is mounted for owner %s", input.ImageName, input.Owner) - nsCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + nsCtx := namespaces.WithNamespace(ctx, im.config.Namespace) - image, err := im.getImage(nsCtx, input.ImageName, input.OwnerName, input.OwnerNamespace) + exists, _, err := im.imageExists(nsCtx, input.ImageName, input.Owner) if err != nil { - return nil, fmt.Errorf("getting image %s for owner %s/%s: %w", input.ImageName, input.OwnerNamespace, input.OwnerName, err) + return false, fmt.Errorf("checking image exists: %w", err) + } + if !exists { + return false, nil } - ss := im.config.SnapshotterVolume - if input.Use == models.ImageUseKernel { - ss = im.config.SnapshotterKernel + snapshotter := im.getSnapshotter(input.Use) + snapshotKey := snapshotKey(input.Owner, input.OwnerUsageID) + ss := im.client.SnapshotService(snapshotter) + + snapshotExists, err := snapshotExists(nsCtx, snapshotKey, ss) + if err != nil { + return false, fmt.Errorf("checking for existence of snapshot %s: %w", snapshotKey, err) } - return im.snapshotAndMount(nsCtx, image, input.OwnerName, ss, logger) + return snapshotExists, nil } -func (im *imageService) getImage(ctx context.Context, imageName string, ownerName, ownerNamespace string) (containerd.Image, error) { - leaseCtx, err := withOwnerLease(ctx, ownerName, ownerNamespace, im.client) +func (im *imageService) imageExists(ctx context.Context, imageName string, owner string) (bool, containerd.Image, error) { + leaseCtx, err := withOwnerLease(ctx, owner, im.client) + if err != nil { + return false, nil, fmt.Errorf("getting lease for owner: %w", err) + } + + image, err := im.client.GetImage(leaseCtx, imageName) + if err != nil { + if errdefs.IsNotFound(err) { + return false, nil, nil + } + + return false, nil, fmt.Errorf("getting image from containerd %s: %w", imageName, err) + } + + return true, image, nil +} + +func (im *imageService) pullImage(ctx context.Context, imageName string, owner string) (containerd.Image, error) { + leaseCtx, err := withOwnerLease(ctx, owner, im.client) if err != nil { return nil, fmt.Errorf("getting lease for owner: %w", err) } - image, err := im.client.Pull(leaseCtx, imageName, containerd.WithPullUnpack) + image, err := im.client.Pull(leaseCtx, imageName) if err != nil { return nil, fmt.Errorf("pulling image using containerd: %w", err) } @@ -89,7 +158,7 @@ func (im *imageService) getImage(ctx context.Context, imageName string, ownerNam return image, nil } -func (im *imageService) snapshotAndMount(ctx context.Context, image containerd.Image, ownerName, snapshotter string, logger *logrus.Entry) ([]models.Mount, error) { +func (im *imageService) snapshotAndMount(ctx context.Context, image containerd.Image, owner, ownerUsageID, snapshotter string, logger *logrus.Entry) ([]models.Mount, error) { unpacked, err := image.IsUnpacked(ctx, snapshotter) if err != nil { return nil, fmt.Errorf("checking if image %s has been unpacked with snapshotter %s: %w", image.Name(), snapshotter, err) @@ -107,7 +176,7 @@ func (im *imageService) snapshotAndMount(ctx context.Context, image containerd.I } parent := imageContent[0].String() - snapshotKey := snapshotKey(ownerName) + snapshotKey := snapshotKey(owner, ownerUsageID) logger.Debugf("creating snapshot %s for image %s with snapshotter %s", snapshotKey, image.Name(), snapshotter) ss := im.client.SnapshotService(snapshotter) @@ -118,7 +187,11 @@ func (im *imageService) snapshotAndMount(ctx context.Context, image containerd.I var mounts []mount.Mount if !snapshotExists { - mounts, err = ss.Prepare(ctx, snapshotKey, parent) + labels := map[string]string{ + "reignite/owner": owner, + "reignite/owner-usage": ownerUsageID, + } + mounts, err = ss.Prepare(ctx, snapshotKey, parent, snapshots.WithLabels(labels)) if err != nil { return nil, fmt.Errorf("preparing snapshot of %s: %w", image.Name(), err) } @@ -136,3 +209,11 @@ func (im *imageService) snapshotAndMount(ctx context.Context, image containerd.I return convertedMounts, nil } + +func (im *imageService) getSnapshotter(use models.ImageUse) string { + if use == models.ImageUseVolume { + return im.config.SnapshotterVolume + } + + return im.config.SnapshotterKernel +} diff --git a/infrastructure/containerd/image_service_test.go b/infrastructure/containerd/image_service_test.go index e9fc53d6..47ce4bdd 100644 --- a/infrastructure/containerd/image_service_test.go +++ b/infrastructure/containerd/image_service_test.go @@ -6,15 +6,14 @@ import ( "os" "testing" + ctr "github.com/containerd/containerd" + "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/snapshots" . "github.com/onsi/gomega" + "github.com/weaveworks/reignite/core/models" "github.com/weaveworks/reignite/core/ports" "github.com/weaveworks/reignite/infrastructure/containerd" - "github.com/weaveworks/reignite/pkg/defaults" - - ctr "github.com/containerd/containerd" - "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/snapshots" ) const ( @@ -22,7 +21,9 @@ const ( testImageKernel = "docker.io/linuxkit/kernel:5.4.129" testSnapshotter = "native" testOwnerNamespace = "int_ns" + testOwnerUsageID = "vol1" testOwnerName = "imageservice-get-test" + testContainerdNs = "reignite_test_ctr" ) func TestImageService_Integration(t *testing.T) { @@ -33,23 +34,29 @@ func TestImageService_Integration(t *testing.T) { RegisterTestingT(t) client, ctx := testCreateClient(t) - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + namespaceCtx := namespaces.WithNamespace(ctx, testContainerdNs) imageSvc := containerd.NewImageServiceWithClient(&containerd.Config{ SnapshotterKernel: testSnapshotter, SnapshotterVolume: testSnapshotter, + Namespace: testContainerdNs, }, client) - input := ports.GetImageInput{ - ImageName: getTestVolumeImage(), - OwnerName: testOwnerName, - OwnerNamespace: testOwnerNamespace, - Use: models.ImageUseVolume, + inputGetAndMount := &ports.ImageMountSpec{ + ImageName: getTestVolumeImage(), + Owner: fmt.Sprintf("%s/%s", testOwnerNamespace, testOwnerName), + OwnerUsageID: testOwnerUsageID, + Use: models.ImageUseVolume, } - err := imageSvc.Get(ctx, input) + inputGet := &ports.ImageSpec{ + ImageName: inputGetAndMount.ImageName, + Owner: inputGetAndMount.Owner, + } + + err := imageSvc.Pull(ctx, inputGet) Expect(err).NotTo(HaveOccurred()) - mounts, err := imageSvc.GetAndMount(ctx, input) + mounts, err := imageSvc.PullAndMount(ctx, inputGetAndMount) Expect(err).NotTo(HaveOccurred()) Expect(mounts).NotTo(BeNil()) Expect(len(mounts)).To(Equal(1)) @@ -59,7 +66,7 @@ func TestImageService_Integration(t *testing.T) { Expect(len(img)).To(Equal(1)) Expect(img[0].Name).To(Equal(getTestVolumeImage())) - expectedSnapshotName := fmt.Sprintf("reignite/%s", testOwnerName) + expectedSnapshotName := fmt.Sprintf("reignite/%s/%s/%s", testOwnerNamespace, testOwnerName, testOwnerUsageID) snapshotExists := false err = client.SnapshotService(testSnapshotter).Walk(namespaceCtx, func(walkCtx context.Context, info snapshots.Info) error { if info.Name == expectedSnapshotName { @@ -77,10 +84,9 @@ func TestImageService_Integration(t *testing.T) { Expect(len(leases)).To(Equal(1)) Expect(leases[0].ID).To(Equal(expectedLeaseName), "expect lease with name %s to exists", expectedLeaseName) - input.Use = models.ImageUseKernel - input.ImageName = getTestKernelImage() + inputGet.ImageName = "docker.io/linuxkit/kernel:5.4.129" - err = imageSvc.Get(ctx, input) + err = imageSvc.Pull(ctx, inputGet) Expect(err).NotTo(HaveOccurred()) } diff --git a/infrastructure/containerd/lease.go b/infrastructure/containerd/lease.go index 3fe32d8c..b3aa0e08 100644 --- a/infrastructure/containerd/lease.go +++ b/infrastructure/containerd/lease.go @@ -9,8 +9,8 @@ import ( "github.com/containerd/containerd/leases" ) -func withOwnerLease(ctx context.Context, ownerName, ownerNamespace string, client *containerd.Client) (context.Context, error) { - leaseName := getLeaseNameForOwner(ownerName, ownerNamespace) +func withOwnerLease(ctx context.Context, owner string, client *containerd.Client) (context.Context, error) { + leaseName := getLeaseNameForOwner(owner) l, err := getExistingOrCreateLease(ctx, leaseName, client.LeasesService()) if err != nil { @@ -41,6 +41,6 @@ func getExistingOrCreateLease(ctx context.Context, name string, manager leases.M return &lease, nil } -func getLeaseNameForOwner(ownerName, ownerNamespace string) string { - return fmt.Sprintf("reignite/%s/%s", ownerNamespace, ownerName) +func getLeaseNameForOwner(owner string) string { + return fmt.Sprintf("reignite/%s", owner) } diff --git a/infrastructure/containerd/repo.go b/infrastructure/containerd/repo.go index 481f93de..236ac554 100644 --- a/infrastructure/containerd/repo.go +++ b/infrastructure/containerd/repo.go @@ -10,12 +10,13 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/content" "github.com/containerd/containerd/namespaces" + "github.com/google/go-cmp/cmp" "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/weaveworks/reignite/core/errors" "github.com/weaveworks/reignite/core/models" "github.com/weaveworks/reignite/core/ports" - "github.com/weaveworks/reignite/pkg/defaults" "github.com/weaveworks/reignite/pkg/log" ) @@ -26,19 +27,21 @@ func NewMicroVMRepo(cfg *Config) (ports.MicroVMRepository, error) { return nil, fmt.Errorf("creating containerd client: %w", err) } - return NewMicroVMRepoWithClient(client), nil + return NewMicroVMRepoWithClient(cfg, client), nil } // NewMicroVMRepoWithClient will create a new containerd backed microvm repository with the supplied containerd client. -func NewMicroVMRepoWithClient(client *containerd.Client) ports.MicroVMRepository { +func NewMicroVMRepoWithClient(cfg *Config, client *containerd.Client) ports.MicroVMRepository { return &containerdRepo{ client: client, + config: cfg, locks: map[string]*sync.RWMutex{}, } } type containerdRepo struct { client *containerd.Client + config *Config locks map[string]*sync.RWMutex locksMu sync.Mutex @@ -53,13 +56,31 @@ func (r *containerdRepo) Save(ctx context.Context, microvm *models.MicroVM) (*mo mu.Lock() defer mu.Unlock() - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + existingSpec, err := r.get(ctx, microvm.ID.Name(), microvm.ID.Namespace()) + if err != nil { + return nil, fmt.Errorf("getting vm spec from store: %w", err) + } + if existingSpec != nil { + specDiff := cmp.Diff(existingSpec.Spec, microvm.Spec) + statusDiff := cmp.Diff(existingSpec.Status, microvm.Status) + if specDiff == "" && statusDiff == "" { + logger.Debug("microvm specs have no diff, skipping save") + + return existingSpec, nil + } + } + + namespaceCtx := namespaces.WithNamespace(ctx, r.config.Namespace) + leaseCtx, err := withOwnerLease(namespaceCtx, microvm.ID.String(), r.client) + if err != nil { + return nil, fmt.Errorf("getting lease for owner: %w", err) + } store := r.client.ContentStore() microvm.Version++ refName := contentRefName(microvm) - writer, err := store.Writer(namespaceCtx, content.WithRef(refName)) + writer, err := store.Writer(leaseCtx, content.WithRef(refName)) if err != nil { return nil, fmt.Errorf("getting containerd writer: %w", err) } @@ -89,26 +110,27 @@ func (r *containerdRepo) Get(ctx context.Context, name, namespace string) (*mode mu.RLock() defer mu.RUnlock() - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) - - digest, err := r.findLatestDigestForSpec(namespaceCtx, name, namespace) + spec, err := r.get(ctx, name, namespace) if err != nil { - return nil, fmt.Errorf("finding content in store: %w", err) + return nil, fmt.Errorf("getting vm spec from store: %w", err) } - if digest == nil { - return nil, errSpecNotFound{name: name, namespace: namespace} + if spec == nil { + return nil, errors.NewSpecNotFound(name, namespace) //nolint: wrapcheck } - return r.getWithDigest(namespaceCtx, digest) + return spec, nil } -// GetAll will get a list of microvm details from the containerd content store. +// GetAll will get a list of microvm details from the containerd content store. If namespace is an empty string all +// details of microvms will be returned. func (r *containerdRepo) GetAll(ctx context.Context, namespace string) ([]*models.MicroVM, error) { - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + namespaceCtx := namespaces.WithNamespace(ctx, r.config.Namespace) store := r.client.ContentStore() - // NOTE: this seems redundant as we have the namespace based context - nsLabelFilter := labelFilter(NamespaceLabel, namespace) + filters := []string{labelFilter(TypeLabel, MicroVMSpecType)} + if namespace != "" { + filters = append(filters, labelFilter(NamespaceLabel, namespace)) + } versions := map[string]int{} digests := map[string]*digest.Digest{} @@ -130,7 +152,7 @@ func (r *containerdRepo) GetAll(ctx context.Context, namespace string) ([]*model } return nil - }, nsLabelFilter) + }, filters...) if err != nil { return nil, fmt.Errorf("walking content store: %w", err) } @@ -154,7 +176,7 @@ func (r *containerdRepo) Delete(ctx context.Context, microvm *models.MicroVM) er mu.Lock() defer mu.Unlock() - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + namespaceCtx := namespaces.WithNamespace(ctx, r.config.Namespace) store := r.client.ContentStore() digests, err := r.findAllDigestForSpec(namespaceCtx, microvm.ID.Name(), microvm.ID.Namespace()) @@ -181,7 +203,7 @@ func (r *containerdRepo) Exists(ctx context.Context, name, namespace string) (bo mu.RLock() defer mu.RUnlock() - namespaceCtx := namespaces.WithNamespace(ctx, defaults.ContainerdNamespace) + namespaceCtx := namespaces.WithNamespace(ctx, r.config.Namespace) digest, err := r.findLatestDigestForSpec(namespaceCtx, name, namespace) if err != nil { @@ -194,6 +216,20 @@ func (r *containerdRepo) Exists(ctx context.Context, name, namespace string) (bo return true, nil } +func (r *containerdRepo) get(ctx context.Context, name, namespace string) (*models.MicroVM, error) { + namespaceCtx := namespaces.WithNamespace(ctx, r.config.Namespace) + + digest, err := r.findLatestDigestForSpec(namespaceCtx, name, namespace) + if err != nil { + return nil, fmt.Errorf("finding content in store: %w", err) + } + if digest == nil { + return nil, nil + } + + return r.getWithDigest(namespaceCtx, digest) +} + func (r *containerdRepo) getWithDigest(ctx context.Context, metadigest *digest.Digest) (*models.MicroVM, error) { readData, err := content.ReadBlob(ctx, r.client.ContentStore(), v1.Descriptor{ Digest: *metadigest, @@ -275,7 +311,7 @@ func getVMLabels(microvm *models.MicroVM) map[string]string { labels := map[string]string{ NameLabel: microvm.ID.Name(), NamespaceLabel: microvm.ID.Namespace(), - TypeLabel: "microvm", + TypeLabel: MicroVMSpecType, VersionLabel: strconv.Itoa(microvm.Version), } diff --git a/infrastructure/containerd/repo_test.go b/infrastructure/containerd/repo_test.go index a35a0a2d..b977e015 100644 --- a/infrastructure/containerd/repo_test.go +++ b/infrastructure/containerd/repo_test.go @@ -18,7 +18,11 @@ func TestMicroVMRepo_Integration(t *testing.T) { client, ctx := testCreateClient(t) - repo := containerd.NewMicroVMRepoWithClient(client) + repo := containerd.NewMicroVMRepoWithClient(&containerd.Config{ + SnapshotterKernel: testSnapshotter, + SnapshotterVolume: testSnapshotter, + Namespace: testContainerdNs, + }, client) exists, err := repo.Exists(ctx, testOwnerName, testOwnerNamespace) Expect(err).NotTo(HaveOccurred()) Expect(exists).To(BeFalse()) @@ -63,6 +67,33 @@ func TestMicroVMRepo_Integration(t *testing.T) { Expect(err).To(HaveOccurred()) } +func TestMicroVMRepo_Integration_MultipleSave(t *testing.T) { + if !runContainerDTests() { + t.Skip("skipping containerd microvm repo integration multipel save test") + } + + RegisterTestingT(t) + + client, ctx := testCreateClient(t) + + testVm := makeSpec(testOwnerName, testOwnerNamespace) + + repo := containerd.NewMicroVMRepoWithClient(&containerd.Config{ + SnapshotterKernel: testSnapshotter, + SnapshotterVolume: testSnapshotter, + Namespace: testContainerdNs, + }, client) + savedVM, err := repo.Save(ctx, testVm) + Expect(err).NotTo(HaveOccurred()) + Expect(savedVM).NotTo(BeNil()) + Expect(savedVM.Version).To(Equal(2)) + + savedVM, err = repo.Save(ctx, testVm) + Expect(err).NotTo(HaveOccurred()) + Expect(savedVM).NotTo(BeNil()) + Expect(savedVM.Version).To(Equal(2)) +} + func makeSpec(name, ns string) *models.MicroVM { vmid, _ := models.NewVMID(name, ns) return &models.MicroVM{ diff --git a/infrastructure/containerd/snapshot.go b/infrastructure/containerd/snapshot.go index ef94b45d..842f7f65 100644 --- a/infrastructure/containerd/snapshot.go +++ b/infrastructure/containerd/snapshot.go @@ -7,8 +7,8 @@ import ( "github.com/containerd/containerd/snapshots" ) -func snapshotKey(ownerName string) string { - return fmt.Sprintf("reignite/%s", ownerName) +func snapshotKey(owner, ownerUsageID string) string { + return fmt.Sprintf("reignite/%s/%s", owner, ownerUsageID) } func snapshotExists(ctx context.Context, key string, ss snapshots.Snapshotter) (bool, error) { diff --git a/infrastructure/controllers/microvm_controller.go b/infrastructure/controllers/microvm_controller.go index 727ecefb..4eb6b9e0 100644 --- a/infrastructure/controllers/microvm_controller.go +++ b/infrastructure/controllers/microvm_controller.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "sync" + "time" "github.com/sirupsen/logrus" @@ -31,7 +32,7 @@ type MicroVMController struct { queue queue.Queue } -func (r *MicroVMController) Run(ctx context.Context, numWorkers int) error { +func (r *MicroVMController) Run(ctx context.Context, numWorkers int, resyncPeriod time.Duration, resyncOnStart bool) error { logger := log.GetLogger(ctx).WithField("controller", "microvm") ctx = log.WithLogger(ctx, logger) logger.Infof("starting microvm controller with %d workers", numWorkers) @@ -41,12 +42,19 @@ func (r *MicroVMController) Run(ctx context.Context, numWorkers int) error { r.queue.Shutdown() }() + if resyncOnStart { + if err := r.resyncSpecs(ctx, logger); err != nil { + // TODO: should we just log here? + return fmt.Errorf("resyncing specs on start: %w", err) + } + } + wg := &sync.WaitGroup{} logger.Info("starting event listener") wg.Add(1) go func() { defer wg.Done() - r.runEventListener(ctx) + r.runEventListener(ctx, resyncPeriod) }() logger.Info("Starting workers", "num_workers", numWorkers) @@ -67,8 +75,9 @@ func (r *MicroVMController) Run(ctx context.Context, numWorkers int) error { return nil } -func (r *MicroVMController) runEventListener(ctx context.Context) { +func (r *MicroVMController) runEventListener(ctx context.Context, resyncPeriod time.Duration) { logger := log.GetLogger(ctx) + ticker := time.NewTicker(resyncPeriod) evtCh, errCh := r.eventSvc.SubscribeTopic(ctx, defaults.TopicMicroVMEvents) for { @@ -84,6 +93,11 @@ func (r *MicroVMController) runEventListener(ctx context.Context) { logger.Errorf("handling events: %s", err) // TODO: should we exit here } + case <-ticker.C: + if err := r.resyncSpecs(ctx, logger); err != nil { + logger.Errorf("resyncing specs: %s", err) + // TODO: should we exit here + } case evtErr := <-errCh: logger.Errorf("error from event service: %s", evtErr) // TODO: should we exit here? @@ -153,3 +167,16 @@ func (r *MicroVMController) handleEvent(envelope *ports.EventEnvelope, logger *l return nil } + +func (r *MicroVMController) resyncSpecs(ctx context.Context, logger *logrus.Entry) error { + logger.Info("resyncing microvm specs") + + err := r.reconcileUC.ResyncMicroVMs(ctx, "") + if err != nil { + logger.Errorf("failed to resync microvms: %s", err) + + return fmt.Errorf("resyncing microvms: %w", err) + } + + return nil +} diff --git a/infrastructure/controllers/microvm_controller_test.go b/infrastructure/controllers/microvm_controller_test.go index ae65ed9b..64c4aed7 100644 --- a/infrastructure/controllers/microvm_controller_test.go +++ b/infrastructure/controllers/microvm_controller_test.go @@ -21,8 +21,9 @@ import ( ) var ( - vmID = "vm1" - vmNS = "testns" + vmID = "vm1" + vmNS = "testns" + ctrNS = "reignite_test_controller" ) func TestMicroVMController(t *testing.T) { @@ -129,7 +130,7 @@ func TestMicroVMController(t *testing.T) { ctrlWG.Add(1) go func() { defer ctrlWG.Done() - err = controller.Run(ctx, 1) + err = controller.Run(ctx, 1, 10*time.Minute, false) }() for _, evt := range tc.eventsToSend { @@ -168,7 +169,7 @@ func hasLogError(hook *lgrtest.Hook) bool { func createdEvent(name, namespace string) *ports.EventEnvelope { return &ports.EventEnvelope{ Timestamp: time.Now(), - Namespace: defaults.ContainerdNamespace, + Namespace: ctrNS, Topic: defaults.TopicMicroVMEvents, Event: &events.MicroVMSpecCreated{ ID: name, @@ -180,7 +181,7 @@ func createdEvent(name, namespace string) *ports.EventEnvelope { func updatedEvent(name, namespace string) *ports.EventEnvelope { return &ports.EventEnvelope{ Timestamp: time.Now(), - Namespace: defaults.ContainerdNamespace, + Namespace: ctrNS, Topic: defaults.TopicMicroVMEvents, Event: &events.MicroVMSpecUpdated{ ID: name, @@ -192,7 +193,7 @@ func updatedEvent(name, namespace string) *ports.EventEnvelope { func deletedEvent(name, namespace string) *ports.EventEnvelope { return &ports.EventEnvelope{ Timestamp: time.Now(), - Namespace: defaults.ContainerdNamespace, + Namespace: ctrNS, Topic: defaults.TopicMicroVMEvents, Event: &events.MicroVMSpecDeleted{ ID: name, diff --git a/infrastructure/firecracker/config.go b/infrastructure/firecracker/config.go index 4a6986e4..65b3daf4 100644 --- a/infrastructure/firecracker/config.go +++ b/infrastructure/firecracker/config.go @@ -1,38 +1,272 @@ package firecracker import ( + "context" + "encoding/base64" + "fmt" + + "github.com/weaveworks/reignite/pkg/ptr" + "github.com/firecracker-microvm/firecracker-go-sdk" fcmodels "github.com/firecracker-microvm/firecracker-go-sdk/client/models" + "gopkg.in/yaml.v3" + + "github.com/weaveworks/reignite/core/errors" "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/pkg/cloudinit" +) + +const ( + cloudInitNetVersion = 2 ) -func (p *fcProvider) getConfig(machine *models.MicroVM) (*firecracker.Config, error) { - if p.config.SocketPath == "" { - return nil, errSocketPathRequired - } - - // TODO: get the metadata data and populate the rest of firecracker.Config - - conf := &firecracker.Config{ - SocketPath: p.config.SocketPath, - // LogFifo: "", - // LogLevel: "", - // MetricsFifo: "", - // FifoLogWriter: nil, - KernelImagePath: string(machine.Spec.Kernel.Image), - KernelArgs: machine.Spec.Kernel.CmdLine, - // Drives: , - // NetworkInterfaces: , - // VsockDevices: , - MachineCfg: fcmodels.MachineConfiguration{ - VcpuCount: firecracker.Int64(machine.Spec.VCPU), - // CPUTemplate: , - // HtEnabled: , - MemSizeMib: firecracker.Int64(machine.Spec.MemoryInMb), - }, - // JailerCfg: nil, - VMID: machine.ID.String(), - } - - return conf, nil +type ConfigOption func(cfg *VmmConfig) error + +func CreateConfig(opts ...ConfigOption) (*VmmConfig, error) { + cfg := &VmmConfig{} + + for _, opt := range opts { + if err := opt(cfg); err != nil { + return nil, fmt.Errorf("creating firecracker configuration: %w", err) + } + } + + // TODO: do we need to add validation? + + return cfg, nil +} + +func WithMicroVM(vm *models.MicroVM) ConfigOption { + return func(cfg *VmmConfig) error { + if vm == nil { + return errors.ErrSpecRequired + } + + cfg.MachineConfig = VMConfig{ + MemSizeMib: vm.Spec.MemoryInMb, + VcpuCount: vm.Spec.VCPU, + HTEnabled: false, + } + + cfg.NetDevices = []NetworkInterfaceConfig{} + for i := range vm.Spec.NetworkInterfaces { + iface := vm.Spec.NetworkInterfaces[i] + + status, ok := vm.Status.NetworkInterfaces[iface.GuestDeviceName] + if !ok { + return errors.NewNetworkInterfaceStatusMissing(iface.GuestDeviceName) + } + + fcInt := createNetworkIface(&iface, status) + cfg.NetDevices = append(cfg.NetDevices, *fcInt) + } + + cfg.BlockDevices = []BlockDeviceConfig{} + for _, vol := range vm.Spec.Volumes { + status, ok := vm.Status.Volumes[vol.ID] + if !ok { + return errors.NewVolumeNotMounted(vol.ID) + } + + cfg.BlockDevices = append(cfg.BlockDevices, BlockDeviceConfig{ + ID: vol.ID, + IsReadOnly: vol.IsReadOnly, + IsRootDevice: vol.IsRoot, + PathOnHost: status.Mount.Source, + // Partuuid: , + // RateLimiter: , + CacheType: CacheTypeUnsafe, + }) + } + + kernelArgs := vm.Spec.Kernel.CmdLine + if vm.Spec.Kernel.AddNetworkConfig { + networkConfig, err := generateNetworkConfig(vm) + if err != nil { + return fmt.Errorf("generating kernel network-config: %w", err) + } + kernelArgs = fmt.Sprintf("%s network-config=%s", kernelArgs, networkConfig) + } + + cfg.BootSource = BootSourceConfig{ + KernelImagePage: fmt.Sprintf("%s/%s", vm.Status.KernelMount.Source, vm.Spec.Kernel.Filename), + BootArgs: &kernelArgs, + } + + if vm.Spec.Initrd != nil { + initrdPath := fmt.Sprintf("%s/%s", vm.Status.InitrdMount.Source, vm.Spec.Initrd.Filename) + cfg.BootSource.InitrdPath = &initrdPath + } + + return nil + } +} + +func WithState(vmState State) ConfigOption { + return func(cfg *VmmConfig) error { + cfg.Logger = &LoggerConfig{ + LogPath: vmState.LogPath(), + Level: LogLevelDebug, + ShowLevel: true, + ShowLogOrigin: true, + } + cfg.Metrics = &MetricsConfig{ + Path: vmState.MetricsPath(), + } + + return nil + } +} + +func ApplyConfig(ctx context.Context, cfg *VmmConfig, client *firecracker.Client) error { + machineConf := &fcmodels.MachineConfiguration{ + VcpuCount: &cfg.MachineConfig.VcpuCount, + MemSizeMib: &cfg.MachineConfig.MemSizeMib, + HtEnabled: &cfg.MachineConfig.HTEnabled, + } + if cfg.MachineConfig.CPUTemplate != nil { + machineConf.CPUTemplate = fcmodels.CPUTemplate(*cfg.MachineConfig.CPUTemplate) + } + + _, err := client.PutMachineConfiguration(ctx, machineConf) + if err != nil { + return fmt.Errorf("failed to put machine configuration: %w", err) + } + for _, drive := range cfg.BlockDevices { + _, err := client.PutGuestDriveByID(ctx, drive.ID, &fcmodels.Drive{ + DriveID: &drive.ID, + IsReadOnly: &drive.IsReadOnly, + IsRootDevice: &drive.IsRootDevice, + Partuuid: drive.PartUUID, + PathOnHost: &drive.PathOnHost, + // RateLimiter: , + }) + if err != nil { + return fmt.Errorf("putting drive configuration: %w", err) + } + } + for i, netInt := range cfg.NetDevices { + guestIfaceName := fmt.Sprintf("eth%d", i) + _, err := client.PutGuestNetworkInterfaceByID(ctx, guestIfaceName, &fcmodels.NetworkInterface{ + IfaceID: &guestIfaceName, + GuestMac: netInt.GuestMAC, + HostDevName: &netInt.HostDevName, + AllowMmdsRequests: netInt.AllowMMDSRequests, + }) + if err != nil { + return fmt.Errorf("putting %s network configuration: %w", guestIfaceName, err) + } + } + _, err = client.PutGuestBootSource(ctx, &fcmodels.BootSource{ + KernelImagePath: &cfg.BootSource.KernelImagePage, + BootArgs: *cfg.BootSource.BootArgs, + InitrdPath: *cfg.BootSource.InitrdPath, + }) + if err != nil { + return fmt.Errorf("failed to put machine bootsource: %w", err) + } + if cfg.Logger != nil { + _, err = client.PutLogger(ctx, &fcmodels.Logger{ + LogPath: &cfg.Logger.LogPath, + ShowLevel: ptr.Bool(true), + ShowLogOrigin: ptr.Bool(true), + Level: ptr.String(string(cfg.Logger.Level)), + }) + if err != nil { + return fmt.Errorf("failed to put logging configuration: %w", err) + } + } + if cfg.Metrics != nil { + _, err = client.PutMetrics(ctx, &fcmodels.Metrics{ + MetricsPath: &cfg.Metrics.Path, + }) + if err != nil { + return fmt.Errorf("failed to put metrics configuration: %w", err) + } + } + // vsock + // mmds + + return nil +} + +func ApplyMetadata(ctx context.Context, metadata map[string]string, client *firecracker.Client) error { + if len(metadata) == 0 { + return nil + } + + meta := &Metadata{ + Latest: map[string]string{}, + } + for metadataKey, metadataVal := range metadata { + encodedVal, err := base64.StdEncoding.DecodeString(metadataVal) + if err != nil { + return fmt.Errorf("base64 decoding metadata %s: %w", metadataKey, err) + } + + meta.Latest[metadataKey] = string(encodedVal) + } + + if _, err := client.PutMmds(ctx, meta); err != nil { + return fmt.Errorf("putting %d metadata items into mmds: %w", len(meta.Latest), err) + } + + return nil +} + +func createNetworkIface(iface *models.NetworkInterface, status *models.NetworkInterfaceStatus) *NetworkInterfaceConfig { + macAddr := iface.GuestMAC + hostDevName := status.HostDeviceName + + if iface.Type == models.IfaceTypeMacvtap { + hostDevName = fmt.Sprintf("/dev/tap%d", status.Index) + if macAddr == "" { + macAddr = status.MACAddress + } + } + + netInt := &NetworkInterfaceConfig{ + IfaceID: iface.GuestDeviceName, + HostDevName: hostDevName, + GuestMAC: macAddr, + AllowMMDSRequests: iface.AllowMetadataRequests, + } + + return netInt +} + +func generateNetworkConfig(vm *models.MicroVM) (string, error) { + network := &cloudinit.Network{ + Version: cloudInitNetVersion, + Ethernet: map[string]*cloudinit.Ethernet{}, + } + + for _, iface := range vm.Spec.NetworkInterfaces { + eth := &cloudinit.Ethernet{ + Match: cloudinit.Match{}, + } + + if iface.GuestMAC != "" { + address := iface.GuestMAC + eth.Match.MACAddress = &address + } else { + eth.Match.Name = &iface.GuestDeviceName + } + + if iface.Address != "" { + eth.Addresses = []string{iface.Address} + eth.DHCP4 = ptr.Bool(false) + } else { + eth.DHCP4 = firecracker.Bool(true) + } + + network.Ethernet[iface.GuestDeviceName] = eth + } + + nd, err := yaml.Marshal(network) + if err != nil { + return "", fmt.Errorf("marshalling network data: %w", err) + } + + return base64.StdEncoding.EncodeToString(nd), nil } diff --git a/infrastructure/firecracker/create.go b/infrastructure/firecracker/create.go new file mode 100644 index 00000000..ebc12f92 --- /dev/null +++ b/infrastructure/firecracker/create.go @@ -0,0 +1,164 @@ +package firecracker + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/firecracker-microvm/firecracker-go-sdk" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" + + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/pkg/defaults" + "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/process" + "github.com/weaveworks/reignite/pkg/wait" +) + +const ( + socketTimeoutInSec = 10 + socketPollInMs = 500 +) + +// Create will create a new microvm. +func (p *fcProvider) Create(ctx context.Context, vm *models.MicroVM) error { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "firecracker_microvm", + "vmid": vm.ID.String(), + }) + logger.Debugf("creating microvm") + + vmState := NewState(vm.ID, p.config.StateRoot, p.fs) + if err := p.ensureState(vmState); err != nil { + return fmt.Errorf("ensuring state dir: %w", err) + } + + config, err := CreateConfig(WithMicroVM(vm), WithState(vmState)) + if err != nil { + return fmt.Errorf("creating firecracker config: %w", err) + } + if err := vmState.SetConfig(config); err != nil { + return fmt.Errorf("saving firecracker config: %w", err) + } + + id := strings.ReplaceAll(vm.ID.String(), "/", "-") + args := []string{"--id", id, "--boot-timer"} + if !p.config.APIConfig { + args = append(args, "--config-file", vmState.ConfigPath()) + } + cmd := firecracker.VMCommandBuilder{}. + WithBin(p.config.FirecrackerBin). + WithSocketPath(vmState.SockPath()). + WithArgs(args). + Build(ctx) + + var proc *os.Process + if p.config.RunDetached { + proc, err = p.startFirecrackerDetached(cmd, vmState) + } else { + proc, err = p.startFirecracker(cmd, vmState) + } + if err != nil { + return fmt.Errorf("starting firecracker process: %w", err) + } + + if err := vmState.SetPid(proc.Pid); err != nil { + return fmt.Errorf("saving pid %d to file: %w", proc.Pid, err) + } + + err = wait.ForCondition(wait.FileExistsCondition(vmState.SockPath(), p.fs), socketTimeoutInSec*time.Second, socketPollInMs*time.Millisecond) + if err != nil { + return fmt.Errorf("waiting for sock file to exist: %w", err) + } + + if p.config.APIConfig { + client := firecracker.NewClient(vmState.SockPath(), logger, true) + if err := ApplyConfig(ctx, config, client); err != nil { + return fmt.Errorf("applying firecracker configuration: %w", err) + } + if err := ApplyMetadata(ctx, vm.Spec.Metadata, client); err != nil { + return fmt.Errorf("applying metadata to mmds: %w", err) + } + } + + return nil +} + +func (p *fcProvider) startFirecrackerDetached(cmd *exec.Cmd, vmState State) (*os.Process, error) { + stdOutFile, err := os.OpenFile(vmState.StdoutPath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaults.DataFilePerm) + if err != nil { + return nil, fmt.Errorf("opening stdout file %s: %w", vmState.StdoutPath(), err) + } + + stdErrFile, err := os.OpenFile(vmState.StderrPath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaults.DataFilePerm) + if err != nil { + return nil, fmt.Errorf("opening sterr file %s: %w", vmState.StderrPath(), err) + } + proc, err := process.StartCommandDetached(cmd, stdErrFile, stdOutFile) + if err != nil { + return nil, fmt.Errorf("daemonizing command %s: %w", cmd.Path, err) + } + + return proc, nil +} + +func (p *fcProvider) startFirecracker(cmd *exec.Cmd, vmState State) (*os.Process, error) { + stdOutFile, err := p.fs.OpenFile(vmState.StdoutPath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaults.DataFilePerm) + if err != nil { + return nil, fmt.Errorf("opening stdout file %s: %w", vmState.StdoutPath(), err) + } + + stdErrFile, err := p.fs.OpenFile(vmState.StderrPath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaults.DataFilePerm) + if err != nil { + return nil, fmt.Errorf("opening sterr file %s: %w", vmState.StderrPath(), err) + } + cmd.Stderr = stdErrFile + cmd.Stdout = stdOutFile + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("starting firecracker process: %w", err) + } + + return cmd.Process, nil +} + +func (p *fcProvider) ensureState(vmState State) error { + exists, err := afero.DirExists(p.fs, vmState.Root()) + if err != nil { + return fmt.Errorf("checking if state dir %s exists: %w", vmState.Root(), err) + } + + if !exists { + if err := p.fs.MkdirAll(vmState.Root(), defaults.DataDirPerm); err != nil { + return fmt.Errorf("creating state directory %s: %w", vmState.Root(), err) + } + } + + sockExists, err := afero.Exists(p.fs, vmState.SockPath()) + if err != nil { + return fmt.Errorf("checking if sock dir exists: %w", err) + } + if sockExists { + if delErr := p.fs.Remove(vmState.SockPath()); delErr != nil { + return fmt.Errorf("deleting existing sock file: %w", err) + } + } + + logFile, err := p.fs.OpenFile(vmState.LogPath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaults.DataFilePerm) + if err != nil { + return fmt.Errorf("opening log file %s: %w", vmState.LogPath(), err) + } + logFile.Close() + + metricsFile, err := p.fs.OpenFile(vmState.MetricsPath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaults.DataFilePerm) + if err != nil { + return fmt.Errorf("opening metrics file %s: %w", vmState.MetricsPath(), err) + } + metricsFile.Close() + + return nil +} diff --git a/infrastructure/firecracker/errors.go b/infrastructure/firecracker/errors.go deleted file mode 100644 index d81f48b0..00000000 --- a/infrastructure/firecracker/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package firecracker - -import "errors" - -var ( - errNotImplemeted = errors.New("not implemented") - errSocketPathRequired = errors.New("socket path is required") -) diff --git a/infrastructure/firecracker/flags.go b/infrastructure/firecracker/flags.go deleted file mode 100644 index c75a86a9..00000000 --- a/infrastructure/firecracker/flags.go +++ /dev/null @@ -1,32 +0,0 @@ -package firecracker - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/weaveworks/reignite/pkg/defaults" -) - -const ( - socketFlagName = "socket-path" - firecrackerBinFlagName = "firecracker-bin" -) - -// AddFlagsToCommand will add the firecracker provider specific flags to the supplied cobra command. -func AddFlagsToCommand(cmd *cobra.Command, config *Config) error { - cmd.Flags().StringVar(&config.FirecrackerBin, - firecrackerBinFlagName, - defaults.FirecrackerBin, - "The path to the firecracker binary to use.") - cmd.Flags().StringVar(&config.SocketPath, - socketFlagName, - "", - "The path to the directory to store the socket files in.") - - if err := cmd.MarkFlagRequired(socketFlagName); err != nil { - return fmt.Errorf("setting %s as required: %w", socketFlagName, err) - } - - return nil -} diff --git a/infrastructure/firecracker/provider.go b/infrastructure/firecracker/provider.go index 5b6a78c8..fe1da06a 100644 --- a/infrastructure/firecracker/provider.go +++ b/infrastructure/firecracker/provider.go @@ -2,33 +2,49 @@ package firecracker import ( "context" + "errors" "fmt" - "os" - firecracker "github.com/firecracker-microvm/firecracker-go-sdk" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" + + "github.com/firecracker-microvm/firecracker-go-sdk" + fcmodels "github.com/firecracker-microvm/firecracker-go-sdk/client/models" "github.com/weaveworks/reignite/core/models" "github.com/weaveworks/reignite/core/ports" "github.com/weaveworks/reignite/pkg/log" + "github.com/weaveworks/reignite/pkg/process" ) +var errNotImplemeted = errors.New("not implemented") + // Config represents the configuration options for the Firecracker infrastructure. type Config struct { // FirecrackerBin is the firecracker binary to use. FirecrackerBin string - // SocketPath is the directory to use for the sockets. - SocketPath string + // StateRoot is the folder to store any required firecracker state (i.e. socks, pid, log files). + StateRoot string + // RunDetached indicates that the firecracker processes should be run detached (a.k.a daemon) from the parent process. + RunDetached bool + // APIConfig idnicates that te firecracker microvm should be configured using the API instead of file. + APIConfig bool } // New creates a new instance of the firecracker microvm provider. -func New(cfg *Config) ports.MicroVMProvider { +func New(cfg *Config, networkSvc ports.NetworkService, fs afero.Fs) ports.MicroVMService { return &fcProvider{ - config: cfg, + config: cfg, + networkSvc: networkSvc, + fs: fs, } } type fcProvider struct { config *Config + + networkSvc ports.NetworkService + fs afero.Fs } // Capabilities returns a list of the capabilities the Firecracker provider supports. @@ -36,65 +52,99 @@ func (p *fcProvider) Capabilities() models.Capabilities { return models.Capabilities{models.MetadataServiceCapability} } -// CreateVM will create a new microvm. -func (p *fcProvider) CreateVM(ctx context.Context, vm *models.MicroVM) (*models.MicroVM, error) { - cfg, err := p.getConfig(vm) - if err != nil { - return nil, fmt.Errorf("getting firecracker configuration for machine: %w", err) - } +// StartVM will start a created microvm. +func (p *fcProvider) Start(ctx context.Context, id string) error { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "firecracker_microvm", + "vmid": id, + }) + logger.Info("starting microvm") + + if !p.config.APIConfig { + logger.Info("using firecracker configuration file, no explicit start required") - logger := log.GetLogger(ctx) - opts := []firecracker.Opt{ - firecracker.WithLogger(logger), + return nil } - // Only if not using the jailer - builder := firecracker.VMCommandBuilder{} - fcCmd := builder. - WithBin(p.config.FirecrackerBin). - WithStdout(os.Stdout). // TODO: change to file output - WithStdin(os.Stdin). - WithStderr(os.Stderr). // TODO: change to file output - WithSocketPath("pathtosocket"). - Build(ctx) + vmid, err := models.NewVMIDFromString(id) + if err != nil { + return fmt.Errorf("parsing vmid: %w", err) + } + vmState := NewState(*vmid, p.config.StateRoot, p.fs) - opts = append(opts, firecracker.WithProcessRunner(fcCmd)) + socketPath := vmState.SockPath() + logger.Tracef("using socket %s", socketPath) - m, err := firecracker.NewMachine(ctx, *cfg, opts...) + client := firecracker.NewClient(socketPath, logger, true) + _, err = client.CreateSyncAction(ctx, &fcmodels.InstanceActionInfo{ + ActionType: firecracker.String("InstanceStart"), + }) if err != nil { - return nil, fmt.Errorf("creating new machine for %s: %w", vm.ID, err) + return fmt.Errorf("failed to create start action: %w", err) } - logger.Trace(m) - return nil, nil -} + logger.Info("started microvm") -// StartVM will start a created microvm. -func (p *fcProvider) StartVM(ctx context.Context, id string) error { - return errNotImplemeted + return nil } -// PauseVM will pause a started microvm. -func (p *fcProvider) PauseVM(ctx context.Context, id string) error { +// Pause will pause a started microvm. +func (p *fcProvider) Pause(ctx context.Context, id string) error { return errNotImplemeted } -// ResumeVM will resume a paused microvm. -func (p *fcProvider) ResumeVM(ctx context.Context, id string) error { +// Resume will resume a paused microvm. +func (p *fcProvider) Resume(ctx context.Context, id string) error { return errNotImplemeted } -// StopVM will stop a paused or running microvm. -func (p *fcProvider) StopVM(ctx context.Context, id string) error { +// Stop will stop a paused or running microvm. +func (p *fcProvider) Stop(ctx context.Context, id string) error { return errNotImplemeted } -// DeleteVM will delete a VM and its runtime state. -func (p *fcProvider) DeleteVM(ctx context.Context, id string) error { +// Delete will delete a VM and its runtime state. +func (p *fcProvider) Delete(ctx context.Context, id string) error { return errNotImplemeted } -// ListVMs will return a list of the microvms. -func (p *fcProvider) ListVMs(ctx context.Context, count int) ([]*models.MicroVM, error) { - return nil, errNotImplemeted +// IsRunning returns true if the microvm is running. +func (p *fcProvider) IsRunning(ctx context.Context, id string) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "firecracker_microvm", + "vmid": id, + }) + logger.Info("checking if microvm is running") + + vmid, err := models.NewVMIDFromString(id) + if err != nil { + return false, fmt.Errorf("parsing vmid: %w", err) + } + vmState := NewState(*vmid, p.config.StateRoot, p.fs) + + pidPath := vmState.PIDPath() + exists, err := afero.Exists(p.fs, pidPath) + if err != nil { + return false, fmt.Errorf("checking pid file exists: %w", err) + } + if !exists { + return false, nil + } + + pid, err := vmState.PID() + if err != nil { + return false, fmt.Errorf("getting pid from file: %w", err) + } + + processExists, err := process.Exists(pid) + if err != nil { + return false, fmt.Errorf("checking if firecracker process is running: %w", err) + } + if !processExists { + return false, nil + } + + // TODO: do we need to query via the sock interface? + + return true, nil } diff --git a/infrastructure/firecracker/state.go b/infrastructure/firecracker/state.go new file mode 100644 index 00000000..2db4b774 --- /dev/null +++ b/infrastructure/firecracker/state.go @@ -0,0 +1,167 @@ +package firecracker + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strconv" + + "github.com/spf13/afero" + + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/pkg/defaults" +) + +type State interface { + Root() string + + PID() (int, error) + PIDPath() string + SetPid(pid int) error + + LogPath() string + MetricsPath() string + StdoutPath() string + StderrPath() string + SockPath() string + + ConfigPath() string + Config() (*VmmConfig, error) + SetConfig(cfg *VmmConfig) error +} + +func NewState(vmid models.VMID, stateDir string, fs afero.Fs) State { + return &fsState{ + stateRoot: fmt.Sprintf("%s/%s", stateDir, vmid), + fs: fs, + } +} + +type fsState struct { + stateRoot string + fs afero.Fs +} + +func (s *fsState) Root() string { + return s.stateRoot +} + +func (s *fsState) PIDPath() string { + return fmt.Sprintf("%s/firecracker.pid", s.stateRoot) +} + +func (s *fsState) PID() (int, error) { + return s.pidReadFromFile(s.PIDPath()) +} + +func (s *fsState) LogPath() string { + return fmt.Sprintf("%s/firecracker.log", s.stateRoot) +} + +func (s *fsState) MetricsPath() string { + return fmt.Sprintf("%s/firecracker.metrics", s.stateRoot) +} + +func (s *fsState) StdoutPath() string { + return fmt.Sprintf("%s/firecracker.stdout", s.stateRoot) +} + +func (s *fsState) StderrPath() string { + return fmt.Sprintf("%s/firecracker.stderr", s.stateRoot) +} + +func (s *fsState) SockPath() string { + return fmt.Sprintf("%s/firecracker.sock", s.stateRoot) +} + +func (s *fsState) SetPid(pid int) error { + return s.pidWriteToFile(pid, s.PIDPath()) +} + +func (s *fsState) ConfigPath() string { + return fmt.Sprintf("%s/firecracker.cfg", s.stateRoot) +} + +func (s *fsState) Config() (*VmmConfig, error) { + return s.cfgReadFromFile(s.ConfigPath()) +} + +func (s *fsState) SetConfig(cfg *VmmConfig) error { + return s.cfgWriteToFile(cfg, s.ConfigPath()) +} + +func (s *fsState) pidReadFromFile(pidFile string) (int, error) { + file, err := s.fs.Open(pidFile) + if err != nil { + return -1, fmt.Errorf("opening pid file %s: %w", pidFile, err) + } + + data, err := ioutil.ReadAll(file) + if err != nil { + return -1, fmt.Errorf("reading pid file %s: %w", pidFile, err) + } + + pid, err := strconv.Atoi(string(bytes.TrimSpace(data))) + if err != nil { + return -1, fmt.Errorf("converting data to int: %w", err) + } + + return pid, nil +} + +func (s *fsState) pidWriteToFile(pid int, pidFile string) error { + file, err := s.fs.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, defaults.DataFilePerm) + defer file.Close() //nolint: staticcheck + if err != nil { + return fmt.Errorf("opening pid file %s: %w", pidFile, err) + } + + _, err = fmt.Fprintf(file, "%d", pid) + if err != nil { + return fmt.Errorf("writing pid %d to file %s: %w", pid, pidFile, err) + } + + return nil +} + +func (s *fsState) cfgReadFromFile(cfgFile string) (*VmmConfig, error) { + file, err := s.fs.Open(cfgFile) + if err != nil { + return nil, fmt.Errorf("opening firecracker config file %s: %w", cfgFile, err) + } + + data, err := ioutil.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("reading firecracker config file %s: %w", cfgFile, err) + } + + cfg := &VmmConfig{} + err = json.Unmarshal(data, cfg) + if err != nil { + return nil, fmt.Errorf("unmarshalling firecracker config: %w", err) + } + + return cfg, nil +} + +func (s *fsState) cfgWriteToFile(cfg *VmmConfig, cfgFile string) error { + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("marshalling firecracker config: %w", err) + } + + file, err := s.fs.OpenFile(cfgFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, defaults.DataFilePerm) + defer file.Close() //nolint: staticcheck + if err != nil { + return fmt.Errorf("opening firecracker config file %s: %w", cfgFile, err) + } + + _, err = file.Write(data) + if err != nil { + return fmt.Errorf("writing firecracker cfg to file %s: %w", cfgFile, err) + } + + return nil +} diff --git a/infrastructure/firecracker/testdata/vm_config.json b/infrastructure/firecracker/testdata/vm_config.json new file mode 100644 index 00000000..9633d326 --- /dev/null +++ b/infrastructure/firecracker/testdata/vm_config.json @@ -0,0 +1,30 @@ +{ + "boot-source": { + "kernel_image_path": "vmlinux.bin", + "boot_args": "console=ttyS0 reboot=k panic=1 pci=off", + "initrd_path": null + }, + "drives": [ + { + "drive_id": "rootfs", + "path_on_host": "bionic.rootfs.ext4", + "is_root_device": true, + "partuuid": null, + "is_read_only": false, + "cache_type": "Unsafe", + "rate_limiter": null + } + ], + "machine-config": { + "vcpu_count": 2, + "mem_size_mib": 1024, + "ht_enabled": false, + "track_dirty_pages": false + }, + "balloon": null, + "network-interfaces": [], + "vsock": null, + "logger": null, + "metrics": null, + "mmds-config": null +} \ No newline at end of file diff --git a/infrastructure/firecracker/types.go b/infrastructure/firecracker/types.go new file mode 100644 index 00000000..6411a07c --- /dev/null +++ b/infrastructure/firecracker/types.go @@ -0,0 +1,160 @@ +package firecracker + +// VmmConfig contains the configuration of the microvm. Based on the rust structure +// from here https://github.com/firecracker-microvm/firecracker/blob/0690010524001b606f67c1a65c67f3c27883183f/src/vmm/src/resources.rs#L51. +type VmmConfig struct { + // Balloon hols the balloon device configuration. + Balloon *BalloonDeviceConfig `json:"balloon,omitempty"` + // BlockDevices is the configuration for the drives. + BlockDevices []BlockDeviceConfig `json:"drives"` + // BootSourec is the boot source configuration for the microvm. + BootSource BootSourceConfig `json:"boot-source"` + // Logger is the logger configuration. + Logger *LoggerConfig `json:"logger,omitempty"` + // MachineConfig contains the microvm machine config. + MachineConfig VMConfig `json:"machine-config"` + // Metrics is the metrics configuration. + Metrics *MetricsConfig `json:"metrics,omitempty"` + // Mmds is the configuration for the metadata service + Mmds *MmdsConfig `json:"MmdsConfig,omitempty"` + // NetDevices is the configuration for the microvm network devices. + NetDevices []NetworkInterfaceConfig `json:"network-interfaces"` + // VsockDevice is the configuration for the vsock device. + VsockDevice *VsockDeviceConfig `json:"vsock,omitempty"` +} + +type VMConfig struct { + // VcpuCount is the number of vcpu to start. + VcpuCount int64 `json:"vcpu_count"` + // MemSizeMib is the memory size in MiB. + MemSizeMib int64 `json:"mem_size_mib"` + // HTEnabled enables or disabled hyperthreading. + HTEnabled bool `json:"ht_enabled"` + // CPUTemplate is a CPU template that it is used to filter the CPU features exposed to the guest. + CPUTemplate *string `json:"cpu_template,omitempty"` + // TrackDirtyPages enables or disables dirty page tracking. Enabling allows incremental snapshots. + TrackDirtyPages bool `json:"track_dirty_pages"` +} + +type CacheType string + +var ( + // CacheTypeUnsafe indovates the flushing mechanic will be advertised to + // the guest driver, but the operation will be a noop. + CacheTypeUnsafe CacheType = "Unsafe" + // CacheTypeWriteBack indicates the flushing mechanic will be advertised + // to the guest driver and flush requests coming from the guest will be + // performed using `fsync`. + CacheTypeWriteBack CacheType = "WriteBack" +) + +// BlockDeviceConfig contains the configuration for a microvm block device. +type BlockDeviceConfig struct { + // ID is the unique identifier of the drive. + ID string `json:"drive_id"` + // PathOnHost is the path of the drive on the host machine. + PathOnHost string `json:"path_on_host"` + // IsRootDevice when true makes the current device the root block device. + // Setting this flag to true will mount the block device in the + // guest under /dev/vda unless the partuuid is present. + IsRootDevice bool `json:"is_root_device"` + // PartUUID represents the unique id of the boot partition of this device. It is + // optional and it will be used only if the `IsRootDevice` field is true. + PartUUID string `json:"partuuid,omitempty"` + // IsReadOnly when true the drive is opened in read-only mode. Otherwise, the + // drive is opened as read-write. + IsReadOnly bool `json:"is_read_only"` + // CacheType indicates whether the drive will ignore flush requests coming from + // the guest driver. + CacheType CacheType `json:"cache_type"` + // RateLimiter is the config for rate limiting the I/O operations. + // RateLimiter *RateLimiterConfig `json:"rate_limiter"` +} + +// BootSourceConfig holds the configuration for the boot source of a microvm. +type BootSourceConfig struct { + // KernelImagePage is the path of the kernel image. + KernelImagePage string `json:"kernel_image_path"` + // InitrdPath is the path of the initrd, if there is one. + InitrdPath *string `json:"initrd_path,omitempty"` + // BootArgs contains the boot arguments to pass to the kernel. If this field is uninitialized, the default + // kernel command line is used: `reboot=k panic=1 pci=off nomodules 8250.nr_uarts=0`. + BootArgs *string `json:"boot_args,omitempty"` +} + +// NetworkInterfaceConfig is the configuration for a network interface of a microvm. +type NetworkInterfaceConfig struct { + // IfaceID is the ID of the guest network interface. + IfaceID string `json:"iface_id"` + // HostDevName is the host level path for the guest network interface. + HostDevName string `json:"host_dev_name"` + // GuestMAC is the mac address to use. + GuestMAC string `json:"guest_mac,omitempty"` + // AllowMMDSRequests is true the device model will reply to HTTP GET + // requests sent to the MMDS address via this interface. In this case, + // both ARP requests for `169.254.169.254` and TCP segments heading to the + // same address are intercepted by the device model, and do not reach + // the associated TAP device. + AllowMMDSRequests bool `json:"allow_mmds_requests"` + // RxRateLimiter is the rate limiter for received packages. + // RxRateLimiter *RateLimiterConfig `json:"rx_rate_limiter,omitempty"` + // TxRateLimiter is the rate limiter for transmitted packages. + // TxRateLimiter *RateLimiterConfig `json:"tx_rate_limiter,omitempty"` +} + +type LogLevel string + +var ( + LogLevelError LogLevel = "Error" + LogLevelWarning LogLevel = "Warning" + LogLevelInfo LogLevel = "Info" + LogLevelDebug LogLevel = "Debug" +) + +// LoggerConfig holds the configuration for the logger. +type LoggerConfig struct { + // LogPath is the named pipe or file used as output for logs. + LogPath string `json:"log_path"` + // Level is the level of the logger. + Level LogLevel `json:"level"` + // ShowLevel when set to true the logger will append to the output the severity of the log entry. + ShowLevel bool `json:"show_level"` + // ShowLogOrigin when set to true the logger will append the origin of the log entry. + ShowLogOrigin bool `json:"show_log_origin"` +} + +// BalloonDeviceConfig holds the configuration for the memory balloon device. +type BalloonDeviceConfig struct { + // AmountMib is the target balloon size in MiB. + AmountMib int64 `json:"amount_mib"` + // DeflateOnOOM when set to true will deflate the balloon in case the guest is out of memory. + DeflateOnOOM bool `json:"deflate_on_oom"` + // StatsPollingInterval is the interval in seconds between refreshing statistics. + StatsPollingInterval int64 `json:"stats_polling_interval_s"` +} + +// MetrcsConfig contains the configuration for the microvm metrics. +type MetricsConfig struct { + // Path is the named pipe or file used as output for metrics. + Path string `json:"metrics_path"` +} + +// MmdsConfig is the config related to the mmds. +type MmdsConfig struct { + // IPV4Address is the MMDS IPv4 configured address. + IPV4Address *string `json:"ipv4_address,omitempty"` +} + +type VsockDeviceConfig struct { + // ID of the vsock device. + ID string `json:"vsock_id"` + // GuestCID is a 32-bit Context Identifier (CID) used to identify the guest. + GuestCID int64 `json:"guest_cid"` + // UDSPath is the path to local unix socket. + UDSPath string `json:"uds_path"` +} + +// Metadata represents metadata in the MMDS. +type Metadata struct { + Latest map[string]string `json:"latest"` +} diff --git a/infrastructure/firecracker/types_test.go b/infrastructure/firecracker/types_test.go new file mode 100644 index 00000000..d079cb21 --- /dev/null +++ b/infrastructure/firecracker/types_test.go @@ -0,0 +1,28 @@ +package firecracker_test + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/weaveworks/reignite/infrastructure/firecracker" +) + +func TestUnmarshallWithFCSample(t *testing.T) { + file, err := os.Open("testdata/vm_config.json") + if err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) + } + + cfg := &firecracker.VmmConfig{} + err = json.Unmarshal(data, cfg) + if err != nil { + t.Fatal(err) + } +} diff --git a/infrastructure/grpc/convert.go b/infrastructure/grpc/convert.go index 2ae081b9..8b76daed 100644 --- a/infrastructure/grpc/convert.go +++ b/infrastructure/grpc/convert.go @@ -17,15 +17,25 @@ func convertMicroVMToModel(spec *types.MicroVMSpec) (*models.MicroVM, error) { // Labels Spec: models.MicroVMSpec{ Kernel: models.Kernel{ - Image: models.ContainerImage(spec.Kernel.Image), - Filename: *spec.Kernel.Filename, - CmdLine: spec.Kernel.Cmdline, + Image: models.ContainerImage(spec.Kernel.Image), + CmdLine: spec.Kernel.Cmdline, + AddNetworkConfig: spec.Kernel.AddNetworkConfig, }, - InitrdImage: models.ContainerImage(*spec.InitrdImage), - VCPU: int64(spec.Vcpu), - MemoryInMb: int64(spec.MemoryInMb), + VCPU: int64(spec.Vcpu), + MemoryInMb: int64(spec.MemoryInMb), }, } + if spec.Kernel.Filename != nil { + convertedModel.Spec.Kernel.Filename = *spec.Kernel.Filename + } + if spec.Initrd != nil { + convertedModel.Spec.Initrd = &models.Initrd{ + Image: models.ContainerImage(spec.Initrd.Image), + } + if spec.Initrd.Filename != nil { + convertedModel.Spec.Initrd.Filename = *spec.Initrd.Filename + } + } for _, volume := range spec.Volumes { convertedVolume := convertVolumeToModel(volume) @@ -37,38 +47,54 @@ func convertMicroVMToModel(spec *types.MicroVMSpec) (*models.MicroVM, error) { convertedModel.Spec.NetworkInterfaces = append(convertedModel.Spec.NetworkInterfaces, *convertedNetInt) } + convertedModel.Spec.Metadata = map[string]string{} + for metadataKey, metadataValue := range spec.Metadata { + convertedModel.Spec.Metadata[metadataKey] = metadataValue + } + return convertedModel, nil } func convertNetworkInterfaceToModel(netInt *types.NetworkInterface) *models.NetworkInterface { - return &models.NetworkInterface{ + converted := &models.NetworkInterface{ AllowMetadataRequests: netInt.AllowMetadataReq, - GuestMAC: *netInt.GuestMac, - GuestDeviceName: *netInt.GuestDeviceName, + GuestDeviceName: netInt.GuestDeviceName, + } + if netInt.GuestMac != nil { + converted.GuestMAC = *netInt.GuestMac + } + if netInt.Address != nil { + converted.Address = *netInt.Address } + + switch netInt.Type { + case types.NetworkInterface_MACVTAP: + converted.Type = models.IfaceTypeMacvtap + case types.NetworkInterface_TAP: + converted.Type = models.IfaceTypeTap + } + + return converted } func convertVolumeToModel(volume *types.Volume) *models.Volume { convertedVol := &models.Volume{ - ID: volume.Id, - MountPoint: volume.MountPoint, - IsRoot: volume.IsRoot, - IsReadOnly: volume.IsReadOnly, - PartitionID: *volume.PartitionId, - Size: *volume.SizeInMb, + ID: volume.Id, + MountPoint: volume.MountPoint, + IsRoot: volume.IsRoot, + IsReadOnly: volume.IsReadOnly, + } + if volume.PartitionId != nil { + convertedVol.PartitionID = *volume.PartitionId + } + if volume.SizeInMb != nil { + convertedVol.Size = *volume.SizeInMb } if volume.Source != nil { if volume.Source.ContainerSource != nil { - convertedVol.Source.Container.Image = models.ContainerImage(*volume.Source.ContainerSource) - } - if volume.Source.HostpathSource != nil { - convertedVol.Source.HostPath = &models.HostPathVolumeSource{ - Path: volume.Source.HostpathSource.Path, - } - // TODO: in the future change to switch when there are more types. - if volume.Source.HostpathSource.Type == types.HostPathVolumeSource_RAW_FILE { - convertedVol.Source.HostPath.Type = models.HostPathRawFile + convertedVol.Source.Container = &models.ContainerVolumeSource{ + Image: models.ContainerImage(*volume.Source.ContainerSource), } } } @@ -84,11 +110,18 @@ func convertModelToMicroVM(mvm *models.MicroVM) *types.MicroVMSpec { Vcpu: int32(mvm.Spec.VCPU), MemoryInMb: int32(mvm.Spec.MemoryInMb), Kernel: &types.Kernel{ - Image: string(mvm.Spec.Kernel.Image), - Cmdline: mvm.Spec.Kernel.CmdLine, - Filename: &mvm.Spec.Kernel.Filename, + Image: string(mvm.Spec.Kernel.Image), + Cmdline: mvm.Spec.Kernel.CmdLine, + Filename: &mvm.Spec.Kernel.Filename, + AddNetworkConfig: mvm.Spec.Kernel.AddNetworkConfig, }, - InitrdImage: (*string)(&mvm.Spec.InitrdImage), + } + + if mvm.Spec.Initrd != nil { + converted.Initrd = &types.Initrd{ + Image: (string)(mvm.Spec.Initrd.Image), + Filename: &mvm.Spec.Initrd.Filename, + } } for i := range mvm.Spec.NetworkInterfaces { @@ -101,6 +134,11 @@ func convertModelToMicroVM(mvm *models.MicroVM) *types.MicroVMSpec { converted.Volumes = append(converted.Volumes, convertedVol) } + converted.Metadata = map[string]string{} + for metadataKey, metadataValue := range mvm.Spec.Metadata { + converted.Metadata[metadataKey] = metadataValue + } + return converted } @@ -115,15 +153,8 @@ func convertModelToVolumne(modelVolume *models.Volume) *types.Volume { } if modelVolume.Source.Container != nil { - convertedVol.Source.ContainerSource = (*string)(&modelVolume.Source.Container.Image) - } - if modelVolume.Source.HostPath != nil { - convertedVol.Source.HostpathSource = &types.HostPathVolumeSource{ - Path: modelVolume.Source.HostPath.Path, - } - // TODO: in the future change to switch when there are different types - if modelVolume.Source.HostPath.Type == models.HostPathRawFile { - convertedVol.Source.HostpathSource.Type = types.HostPathVolumeSource_RAW_FILE + convertedVol.Source = &types.VolumeSource{ + ContainerSource: (*string)(&modelVolume.Source.Container.Image), } } @@ -131,10 +162,23 @@ func convertModelToVolumne(modelVolume *models.Volume) *types.Volume { } func convertModelToNetworkInterface(modelNetInt *models.NetworkInterface) *types.NetworkInterface { - return &types.NetworkInterface{ + converted := &types.NetworkInterface{ AllowMetadataReq: modelNetInt.AllowMetadataRequests, GuestMac: &modelNetInt.GuestMAC, - GuestDeviceName: &modelNetInt.GuestDeviceName, + GuestDeviceName: modelNetInt.GuestDeviceName, // HostDevice } + + switch modelNetInt.Type { + case models.IfaceTypeMacvtap: + converted.Type = types.NetworkInterface_MACVTAP + case models.IfaceTypeTap: + converted.Type = types.NetworkInterface_TAP + } + + if modelNetInt.Address != "" { + converted.Address = &modelNetInt.Address + } + + return converted } diff --git a/infrastructure/mock/gen.go b/infrastructure/mock/gen.go index 3e58bcc3..a80ca398 100644 --- a/infrastructure/mock/gen.go +++ b/infrastructure/mock/gen.go @@ -1,3 +1,3 @@ package mock -//go:generate ../../hack/tools/bin/mockgen -destination mock.go -package mock github.com/weaveworks/reignite/core/ports MicroVMProvider,MicroVMRepository,EventService,IDService,ImageService,ReconcileMicroVMsUseCase +//go:generate ../../hack/tools/bin/mockgen -destination mock.go -package mock github.com/weaveworks/reignite/core/ports MicroVMService,MicroVMRepository,EventService,IDService,ImageService,ReconcileMicroVMsUseCase,NetworkService diff --git a/infrastructure/mock/mock.go b/infrastructure/mock/mock.go index 9f166057..7211b18c 100644 --- a/infrastructure/mock/mock.go +++ b/infrastructure/mock/mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/weaveworks/reignite/core/ports (interfaces: MicroVMProvider,MicroVMRepository,EventService,IDService,ImageService,ReconcileMicroVMsUseCase) +// Source: github.com/weaveworks/reignite/core/ports (interfaces: MicroVMService,MicroVMRepository,EventService,IDService,ImageService,ReconcileMicroVMsUseCase,NetworkService) // Package mock is a generated GoMock package. package mock @@ -13,31 +13,31 @@ import ( ports "github.com/weaveworks/reignite/core/ports" ) -// MockMicroVMProvider is a mock of MicroVMProvider interface. -type MockMicroVMProvider struct { +// MockMicroVMService is a mock of MicroVMService interface. +type MockMicroVMService struct { ctrl *gomock.Controller - recorder *MockMicroVMProviderMockRecorder + recorder *MockMicroVMServiceMockRecorder } -// MockMicroVMProviderMockRecorder is the mock recorder for MockMicroVMProvider. -type MockMicroVMProviderMockRecorder struct { - mock *MockMicroVMProvider +// MockMicroVMServiceMockRecorder is the mock recorder for MockMicroVMService. +type MockMicroVMServiceMockRecorder struct { + mock *MockMicroVMService } -// NewMockMicroVMProvider creates a new mock instance. -func NewMockMicroVMProvider(ctrl *gomock.Controller) *MockMicroVMProvider { - mock := &MockMicroVMProvider{ctrl: ctrl} - mock.recorder = &MockMicroVMProviderMockRecorder{mock} +// NewMockMicroVMService creates a new mock instance. +func NewMockMicroVMService(ctrl *gomock.Controller) *MockMicroVMService { + mock := &MockMicroVMService{ctrl: ctrl} + mock.recorder = &MockMicroVMServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMicroVMProvider) EXPECT() *MockMicroVMProviderMockRecorder { +func (m *MockMicroVMService) EXPECT() *MockMicroVMServiceMockRecorder { return m.recorder } // Capabilities mocks base method. -func (m *MockMicroVMProvider) Capabilities() models.Capabilities { +func (m *MockMicroVMService) Capabilities() models.Capabilities { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Capabilities") ret0, _ := ret[0].(models.Capabilities) @@ -45,109 +45,108 @@ func (m *MockMicroVMProvider) Capabilities() models.Capabilities { } // Capabilities indicates an expected call of Capabilities. -func (mr *MockMicroVMProviderMockRecorder) Capabilities() *gomock.Call { +func (mr *MockMicroVMServiceMockRecorder) Capabilities() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capabilities", reflect.TypeOf((*MockMicroVMProvider)(nil).Capabilities)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capabilities", reflect.TypeOf((*MockMicroVMService)(nil).Capabilities)) } -// CreateVM mocks base method. -func (m *MockMicroVMProvider) CreateVM(arg0 context.Context, arg1 *models.MicroVM) (*models.MicroVM, error) { +// Create mocks base method. +func (m *MockMicroVMService) Create(arg0 context.Context, arg1 *models.MicroVM) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateVM", arg0, arg1) - ret0, _ := ret[0].(*models.MicroVM) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 } -// CreateVM indicates an expected call of CreateVM. -func (mr *MockMicroVMProviderMockRecorder) CreateVM(arg0, arg1 interface{}) *gomock.Call { +// Create indicates an expected call of Create. +func (mr *MockMicroVMServiceMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVM", reflect.TypeOf((*MockMicroVMProvider)(nil).CreateVM), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockMicroVMService)(nil).Create), arg0, arg1) } -// DeleteVM mocks base method. -func (m *MockMicroVMProvider) DeleteVM(arg0 context.Context, arg1 string) error { +// Delete mocks base method. +func (m *MockMicroVMService) Delete(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteVM", arg0, arg1) + ret := m.ctrl.Call(m, "Delete", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// DeleteVM indicates an expected call of DeleteVM. -func (mr *MockMicroVMProviderMockRecorder) DeleteVM(arg0, arg1 interface{}) *gomock.Call { +// Delete indicates an expected call of Delete. +func (mr *MockMicroVMServiceMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVM", reflect.TypeOf((*MockMicroVMProvider)(nil).DeleteVM), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockMicroVMService)(nil).Delete), arg0, arg1) } -// ListVMs mocks base method. -func (m *MockMicroVMProvider) ListVMs(arg0 context.Context, arg1 int) ([]*models.MicroVM, error) { +// IsRunning mocks base method. +func (m *MockMicroVMService) IsRunning(arg0 context.Context, arg1 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListVMs", arg0, arg1) - ret0, _ := ret[0].([]*models.MicroVM) + ret := m.ctrl.Call(m, "IsRunning", arg0, arg1) + ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListVMs indicates an expected call of ListVMs. -func (mr *MockMicroVMProviderMockRecorder) ListVMs(arg0, arg1 interface{}) *gomock.Call { +// IsRunning indicates an expected call of IsRunning. +func (mr *MockMicroVMServiceMockRecorder) IsRunning(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVMs", reflect.TypeOf((*MockMicroVMProvider)(nil).ListVMs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRunning", reflect.TypeOf((*MockMicroVMService)(nil).IsRunning), arg0, arg1) } -// PauseVM mocks base method. -func (m *MockMicroVMProvider) PauseVM(arg0 context.Context, arg1 string) error { +// Pause mocks base method. +func (m *MockMicroVMService) Pause(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PauseVM", arg0, arg1) + ret := m.ctrl.Call(m, "Pause", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// PauseVM indicates an expected call of PauseVM. -func (mr *MockMicroVMProviderMockRecorder) PauseVM(arg0, arg1 interface{}) *gomock.Call { +// Pause indicates an expected call of Pause. +func (mr *MockMicroVMServiceMockRecorder) Pause(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseVM", reflect.TypeOf((*MockMicroVMProvider)(nil).PauseVM), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockMicroVMService)(nil).Pause), arg0, arg1) } -// ResumeVM mocks base method. -func (m *MockMicroVMProvider) ResumeVM(arg0 context.Context, arg1 string) error { +// Resume mocks base method. +func (m *MockMicroVMService) Resume(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ResumeVM", arg0, arg1) + ret := m.ctrl.Call(m, "Resume", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// ResumeVM indicates an expected call of ResumeVM. -func (mr *MockMicroVMProviderMockRecorder) ResumeVM(arg0, arg1 interface{}) *gomock.Call { +// Resume indicates an expected call of Resume. +func (mr *MockMicroVMServiceMockRecorder) Resume(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeVM", reflect.TypeOf((*MockMicroVMProvider)(nil).ResumeVM), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resume", reflect.TypeOf((*MockMicroVMService)(nil).Resume), arg0, arg1) } -// StartVM mocks base method. -func (m *MockMicroVMProvider) StartVM(arg0 context.Context, arg1 string) error { +// Start mocks base method. +func (m *MockMicroVMService) Start(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StartVM", arg0, arg1) + ret := m.ctrl.Call(m, "Start", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// StartVM indicates an expected call of StartVM. -func (mr *MockMicroVMProviderMockRecorder) StartVM(arg0, arg1 interface{}) *gomock.Call { +// Start indicates an expected call of Start. +func (mr *MockMicroVMServiceMockRecorder) Start(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartVM", reflect.TypeOf((*MockMicroVMProvider)(nil).StartVM), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockMicroVMService)(nil).Start), arg0, arg1) } -// StopVM mocks base method. -func (m *MockMicroVMProvider) StopVM(arg0 context.Context, arg1 string) error { +// Stop mocks base method. +func (m *MockMicroVMService) Stop(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StopVM", arg0, arg1) + ret := m.ctrl.Call(m, "Stop", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// StopVM indicates an expected call of StopVM. -func (mr *MockMicroVMProviderMockRecorder) StopVM(arg0, arg1 interface{}) *gomock.Call { +// Stop indicates an expected call of Stop. +func (mr *MockMicroVMServiceMockRecorder) Stop(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopVM", reflect.TypeOf((*MockMicroVMProvider)(nil).StopVM), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockMicroVMService)(nil).Stop), arg0, arg1) } // MockMicroVMRepository is a mock of MicroVMRepository interface. @@ -390,33 +389,63 @@ func (m *MockImageService) EXPECT() *MockImageServiceMockRecorder { return m.recorder } -// Get mocks base method. -func (m *MockImageService) Get(arg0 context.Context, arg1 ports.GetImageInput) error { +// Exists mocks base method. +func (m *MockImageService) Exists(arg0 context.Context, arg1 *ports.ImageSpec) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exists", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exists indicates an expected call of Exists. +func (mr *MockImageServiceMockRecorder) Exists(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockImageService)(nil).Exists), arg0, arg1) +} + +// IsMounted mocks base method. +func (m *MockImageService) IsMounted(arg0 context.Context, arg1 *ports.ImageMountSpec) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret := m.ctrl.Call(m, "IsMounted", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsMounted indicates an expected call of IsMounted. +func (mr *MockImageServiceMockRecorder) IsMounted(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMounted", reflect.TypeOf((*MockImageService)(nil).IsMounted), arg0, arg1) +} + +// Pull mocks base method. +func (m *MockImageService) Pull(arg0 context.Context, arg1 *ports.ImageSpec) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Pull", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// Get indicates an expected call of Get. -func (mr *MockImageServiceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { +// Pull indicates an expected call of Pull. +func (mr *MockImageServiceMockRecorder) Pull(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockImageService)(nil).Get), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pull", reflect.TypeOf((*MockImageService)(nil).Pull), arg0, arg1) } -// GetAndMount mocks base method. -func (m *MockImageService) GetAndMount(arg0 context.Context, arg1 ports.GetImageInput) ([]models.Mount, error) { +// PullAndMount mocks base method. +func (m *MockImageService) PullAndMount(arg0 context.Context, arg1 *ports.ImageMountSpec) ([]models.Mount, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAndMount", arg0, arg1) + ret := m.ctrl.Call(m, "PullAndMount", arg0, arg1) ret0, _ := ret[0].([]models.Mount) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetAndMount indicates an expected call of GetAndMount. -func (mr *MockImageServiceMockRecorder) GetAndMount(arg0, arg1 interface{}) *gomock.Call { +// PullAndMount indicates an expected call of PullAndMount. +func (mr *MockImageServiceMockRecorder) PullAndMount(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAndMount", reflect.TypeOf((*MockImageService)(nil).GetAndMount), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PullAndMount", reflect.TypeOf((*MockImageService)(nil).PullAndMount), arg0, arg1) } // MockReconcileMicroVMsUseCase is a mock of ReconcileMicroVMsUseCase interface. @@ -455,3 +484,99 @@ func (mr *MockReconcileMicroVMsUseCaseMockRecorder) ReconcileMicroVM(arg0, arg1, mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcileMicroVM", reflect.TypeOf((*MockReconcileMicroVMsUseCase)(nil).ReconcileMicroVM), arg0, arg1, arg2) } + +// ResyncMicroVMs mocks base method. +func (m *MockReconcileMicroVMsUseCase) ResyncMicroVMs(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResyncMicroVMs", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ResyncMicroVMs indicates an expected call of ResyncMicroVMs. +func (mr *MockReconcileMicroVMsUseCaseMockRecorder) ResyncMicroVMs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResyncMicroVMs", reflect.TypeOf((*MockReconcileMicroVMsUseCase)(nil).ResyncMicroVMs), arg0, arg1) +} + +// MockNetworkService is a mock of NetworkService interface. +type MockNetworkService struct { + ctrl *gomock.Controller + recorder *MockNetworkServiceMockRecorder +} + +// MockNetworkServiceMockRecorder is the mock recorder for MockNetworkService. +type MockNetworkServiceMockRecorder struct { + mock *MockNetworkService +} + +// NewMockNetworkService creates a new mock instance. +func NewMockNetworkService(ctrl *gomock.Controller) *MockNetworkService { + mock := &MockNetworkService{ctrl: ctrl} + mock.recorder = &MockNetworkServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNetworkService) EXPECT() *MockNetworkServiceMockRecorder { + return m.recorder +} + +// IfaceCreate mocks base method. +func (m *MockNetworkService) IfaceCreate(arg0 context.Context, arg1 ports.IfaceCreateInput) (*ports.IfaceDetails, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IfaceCreate", arg0, arg1) + ret0, _ := ret[0].(*ports.IfaceDetails) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IfaceCreate indicates an expected call of IfaceCreate. +func (mr *MockNetworkServiceMockRecorder) IfaceCreate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IfaceCreate", reflect.TypeOf((*MockNetworkService)(nil).IfaceCreate), arg0, arg1) +} + +// IfaceDelete mocks base method. +func (m *MockNetworkService) IfaceDelete(arg0 context.Context, arg1 ports.DeleteIfaceInput) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IfaceDelete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// IfaceDelete indicates an expected call of IfaceDelete. +func (mr *MockNetworkServiceMockRecorder) IfaceDelete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IfaceDelete", reflect.TypeOf((*MockNetworkService)(nil).IfaceDelete), arg0, arg1) +} + +// IfaceDetails mocks base method. +func (m *MockNetworkService) IfaceDetails(arg0 context.Context, arg1 string) (*ports.IfaceDetails, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IfaceDetails", arg0, arg1) + ret0, _ := ret[0].(*ports.IfaceDetails) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IfaceDetails indicates an expected call of IfaceDetails. +func (mr *MockNetworkServiceMockRecorder) IfaceDetails(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IfaceDetails", reflect.TypeOf((*MockNetworkService)(nil).IfaceDetails), arg0, arg1) +} + +// IfaceExists mocks base method. +func (m *MockNetworkService) IfaceExists(arg0 context.Context, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IfaceExists", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IfaceExists indicates an expected call of IfaceExists. +func (mr *MockNetworkServiceMockRecorder) IfaceExists(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IfaceExists", reflect.TypeOf((*MockNetworkService)(nil).IfaceExists), arg0, arg1) +} diff --git a/infrastructure/network/network_service.go b/infrastructure/network/network_service.go new file mode 100644 index 00000000..d53a79b4 --- /dev/null +++ b/infrastructure/network/network_service.go @@ -0,0 +1,188 @@ +package network + +import ( + "context" + "fmt" + "strings" + + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + + "github.com/weaveworks/reignite/core/errors" + "github.com/weaveworks/reignite/core/models" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/pkg/log" +) + +type Config struct { + ParentDeviceName string +} + +func New(cfg *Config) ports.NetworkService { + return &networkService{ + parentDeviceName: cfg.ParentDeviceName, + } +} + +type networkService struct { + parentDeviceName string +} + +// IfaceCreate will create the network interface. +func (n *networkService) IfaceCreate(ctx context.Context, input ports.IfaceCreateInput) (*ports.IfaceDetails, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "netlink_network", + "iface": input.DeviceName, + }) + logger.Debugf("creating network interface with type %s and MAC %s using parent %s", input.Type, input.MAC, n.parentDeviceName) + + var parentLink netlink.Link + var err error + if input.Type == models.IfaceTypeMacvtap { + if n.parentDeviceName == "" { + return nil, errors.ErrParentIfaceRequired + } + + parentLink, err = netlink.LinkByName(n.parentDeviceName) + if err != nil { + return nil, fmt.Errorf("failed to lookup parent network interface %q: %w", n.parentDeviceName, err) + } + } + + var link netlink.Link + switch input.Type { + case models.IfaceTypeTap: + link = &netlink.Tuntap{ + LinkAttrs: netlink.LinkAttrs{ + Name: input.DeviceName, + // TODO: add Namespace + }, + Mode: netlink.TUNTAP_MODE_TAP, + } + case models.IfaceTypeMacvtap: + link = &netlink.Macvtap{ + Macvlan: netlink.Macvlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: input.DeviceName, + MTU: parentLink.Attrs().MTU, + ParentIndex: parentLink.Attrs().Index, + Namespace: parentLink.Attrs().Namespace, // TODO: add namespace specific to vm + TxQLen: parentLink.Attrs().TxQLen, + }, + Mode: netlink.MACVLAN_MODE_BRIDGE, + }, + } + default: + return nil, errors.NewErrUnsupportedInterface(string(input.Type)) + } + + if err := netlink.LinkAdd(link); err != nil { + return nil, fmt.Errorf("creating interface %s using netlink: %w", link.Attrs().Name, err) + } + + macIf, err := netlink.LinkByName(link.Attrs().Name) + if err != nil { + return nil, fmt.Errorf("getting interface %s using netlink: %w", link.Attrs().Name, err) + } + + if err := netlink.LinkSetUp(macIf); err != nil { + return nil, fmt.Errorf("enabling device %s: %w", macIf.Attrs().Name, err) + } + + return &ports.IfaceDetails{ + DeviceName: input.DeviceName, + Type: input.Type, + MAC: strings.ToUpper(macIf.Attrs().HardwareAddr.String()), + Index: macIf.Attrs().Index, + }, nil +} + +// IfaceDelete is used to delete a network interface. +func (n *networkService) IfaceDelete(ctx context.Context, input ports.DeleteIfaceInput) error { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "netlink_network", + "iface": input.DeviceName, + }) + logger.Debug("deleting network interface") + + link, err := netlink.LinkByName(input.DeviceName) + if err != nil { + _, ok := err.(netlink.LinkNotFoundError) //nolint: errorlint + if !ok { + return fmt.Errorf("failed to lookup network interface %s: %w", input.DeviceName, err) + } + + logger.Debug("network interface doesn't exist, no action") + + return nil + } + + if err = netlink.LinkDel(link); err != nil { + return fmt.Errorf("deleting interface %s: %w", link.Attrs().Name, err) + } + + return nil +} + +func (n *networkService) IfaceExists(ctx context.Context, name string) (bool, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "netlink_network", + "iface": name, + }) + logger.Debug("checking if network interface exists") + + found, _, err := n.getIface(name) + if err != nil { + return false, fmt.Errorf("getting interface %s: %w", name, err) + } + + return found, nil +} + +// IfaceDetails will get the details of the supplied network interface. +func (n *networkService) IfaceDetails(ctx context.Context, name string) (*ports.IfaceDetails, error) { + logger := log.GetLogger(ctx).WithFields(logrus.Fields{ + "service": "netlink_network", + "iface": name, + }) + logger.Debug("getting network interface details") + + found, link, err := n.getIface(name) + if err != nil { + return nil, fmt.Errorf("getting interface %s: %w", name, err) + } + if !found { + return nil, errors.ErrIfaceNotFound + } + + details := &ports.IfaceDetails{ + DeviceName: name, + MAC: strings.ToUpper(link.Attrs().HardwareAddr.String()), + Index: link.Attrs().Index, + } + + switch link.(type) { + case *netlink.Macvtap: + details.Type = models.IfaceTypeMacvtap + case *netlink.Tuntap: + details.Type = models.IfaceTypeTap + default: + details.Type = models.IfaceTypeUnsupported + } + + return details, nil +} + +func (n *networkService) getIface(name string) (bool, netlink.Link, error) { + link, err := netlink.LinkByName(name) + if err != nil { + _, ok := err.(netlink.LinkNotFoundError) //nolint: errorlint + if !ok { + return false, nil, fmt.Errorf("failed to lookup network interface %s: %w", name, err) + } + + return false, nil, nil + } + + return true, link, nil +} diff --git a/infrastructure/ulid/ulid_test.go b/infrastructure/ulid/ulid_test.go index eb8fb28a..f44e8b46 100644 --- a/infrastructure/ulid/ulid_test.go +++ b/infrastructure/ulid/ulid_test.go @@ -3,5 +3,4 @@ package ulid_test import "testing" func TestUlidService_GenerateRandom(t *testing.T) { - } diff --git a/internal/command/flags/flags.go b/internal/command/flags/flags.go index 6912c965..8b5bfbe1 100644 --- a/internal/command/flags/flags.go +++ b/internal/command/flags/flags.go @@ -1,6 +1,8 @@ package flags import ( + "fmt" + "github.com/spf13/cobra" "github.com/weaveworks/reignite/internal/config" @@ -8,10 +10,18 @@ import ( ) const ( - grpcEndpointFlag = "grpc-endpoint" - httpEndpointFlag = "http-endpoint" - containerdSocketFlag = "containerd-socket" - containerdNamespaceFlag = "containerd-namespace" + grpcEndpointFlag = "grpc-endpoint" + httpEndpointFlag = "http-endpoint" + parentIfaceFlag = "parent-iface" + disableReconcileFlag = "disable-reconcile" + disableAPIFlag = "disable-api" + firecrackerBinFlag = "firecracker-bin" + firecrackerDetachFlag = "firecracker-detach" + firecrackerAPIFlag = "firecracker-api" + containerdSocketFlag = "containerd-socket" + volSnapshotterFlag = "containerd-volume-ss" + kernelSnapshotterFlag = "containerd-kernel-ss" + containerdNamespace = "containerd-ns" ) // AddGRPCServerFlagsToCommand will add gRPC server flags to the supplied command. @@ -35,13 +45,79 @@ func AddGWServerFlagsToCommand(cmd *cobra.Command, cfg *config.Config) { "The endpoint for the HTTP proxy to the gRPC service to listen on.") } -func AddContainerdFlagsToCommand(cmd *cobra.Command, cfg *config.Config) { - cmd.Flags().StringVar(&cfg.ContainerdSocketPath, +func AddNetworkFlagsToCommand(cmd *cobra.Command, cfg *config.Config) error { + cmd.Flags().StringVar(&cfg.ParentIface, + parentIfaceFlag, + "", + "The parent iface for the network interfaces. Note it could also be a bond") + + if err := cmd.MarkFlagRequired(parentIfaceFlag); err != nil { + return fmt.Errorf("setting %s as required: %w", parentIfaceFlag, err) + } + + return nil +} + +func AddHiddenFlagsToCommand(cmd *cobra.Command, cfg *config.Config) error { + cmd.Flags().BoolVar(&cfg.DisableReconcile, + disableReconcileFlag, + false, + "Set to true to stop the reconciler running") + + cmd.Flags().BoolVar(&cfg.DisableAPI, + disableAPIFlag, + false, + "Set to true to stop the api server running") + + if err := cmd.Flags().MarkHidden(disableReconcileFlag); err != nil { + return fmt.Errorf("setting %s as hidden: %w", disableReconcileFlag, err) + } + if err := cmd.Flags().MarkHidden(disableAPIFlag); err != nil { + return fmt.Errorf("setting %s as hidden: %w", disableAPIFlag, err) + } + + return nil +} + +// AddFirecrackerFlagsToCommand will add the firecracker provider specific flags to the supplied cobra command. +func AddFirecrackerFlagsToCommand(cmd *cobra.Command, cfg *config.Config) error { + cmd.Flags().StringVar(&cfg.FirecrackerBin, + firecrackerBinFlag, + defaults.FirecrackerBin, + "The path to the firecracker binary to use.") + cmd.Flags().BoolVar(&cfg.FirecrackerDetatch, + firecrackerDetachFlag, + defaults.FirecrackerDetach, + "If true the child firecracker processes will be detached from the parent reignite process.") + cmd.Flags().BoolVar(&cfg.FirecrackerUseAPI, + firecrackerAPIFlag, + defaults.FirecrackerUseAPI, + "Indicates that the Firecracker API should be used to configure the microvm.") + + return nil +} + +// AddContainerDFlagsToCommand will add the containerd specific flags to the supplied cobra command. +func AddContainerDFlagsToCommand(cmd *cobra.Command, cfg *config.Config) error { + cmd.Flags().StringVar(&cfg.CtrSocketPath, containerdSocketFlag, defaults.ContainerdSocket, "The path to the containerd socket.") - cmd.Flags().StringVar(&cfg.ContainerdNamespace, - containerdNamespaceFlag, + + cmd.Flags().StringVar(&cfg.CtrSnapshotterKernel, + kernelSnapshotterFlag, + defaults.ContainerdKernelSnapshotter, + "The name of the snapshotter to use with containerd for kernel/initrd images.") + + cmd.Flags().StringVar(&cfg.CtrSnapshotterVolume, + volSnapshotterFlag, + defaults.ContainerdVolumeSnapshotter, + "The name of the snapshotter to use with containerd for volume images.") + + cmd.Flags().StringVar(&cfg.CtrNamespace, + containerdNamespace, defaults.ContainerdNamespace, - "The name of the default containerd namespace.") + "The name of the containerd namespace to use.") + + return nil } diff --git a/internal/command/run/run.go b/internal/command/run/run.go index bb190097..aec40d6d 100644 --- a/internal/command/run/run.go +++ b/internal/command/run/run.go @@ -9,21 +9,16 @@ import ( "os/signal" "sync" - "github.com/containerd/containerd" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" "google.golang.org/grpc" mvmv1 "github.com/weaveworks/reignite/api/services/microvm/v1alpha1" - "github.com/weaveworks/reignite/core/application" - reignite_ctr "github.com/weaveworks/reignite/infrastructure/containerd" - "github.com/weaveworks/reignite/infrastructure/controllers" - "github.com/weaveworks/reignite/infrastructure/firecracker" - microvmgrpc "github.com/weaveworks/reignite/infrastructure/grpc" - "github.com/weaveworks/reignite/infrastructure/ulid" cmdflags "github.com/weaveworks/reignite/internal/command/flags" "github.com/weaveworks/reignite/internal/config" + "github.com/weaveworks/reignite/internal/inject" + "github.com/weaveworks/reignite/pkg/defaults" "github.com/weaveworks/reignite/pkg/flags" "github.com/weaveworks/reignite/pkg/log" ) @@ -44,10 +39,20 @@ func NewCommand(cfg *config.Config) (*cobra.Command, error) { } cmdflags.AddGRPCServerFlagsToCommand(cmd, cfg) - cmdflags.AddContainerdFlagsToCommand(cmd, cfg) - if err := firecracker.AddFlagsToCommand(cmd, &cfg.Firecracker); err != nil { + if err := cmdflags.AddContainerDFlagsToCommand(cmd, cfg); err != nil { + return nil, fmt.Errorf("adding containerd flags to run command: %w", err) + } + if err := cmdflags.AddFirecrackerFlagsToCommand(cmd, cfg); err != nil { return nil, fmt.Errorf("adding firecracker flags to run command: %w", err) } + if err := cmdflags.AddNetworkFlagsToCommand(cmd, cfg); err != nil { + return nil, fmt.Errorf("adding network flags to run command: %w", err) + } + if err := cmdflags.AddHiddenFlagsToCommand(cmd, cfg); err != nil { + return nil, fmt.Errorf("adding hidden flags to run command: %w", err) + } + cmd.Flags().StringVar(&cfg.StateRootDir, "state-dir", defaults.StateRootDir, "The directory to use for the as the root for runtime state.") + cmd.Flags().DurationVar(&cfg.ResyncPeriod, "resync-period", defaults.ResyncPeriod, "Reconcile the specs to resynchronise them based on this period.") return cmd, nil } @@ -62,21 +67,25 @@ func runServer(ctx context.Context, cfg *config.Config) error { wg := &sync.WaitGroup{} ctx, cancel := context.WithCancel(log.WithLogger(ctx, logger)) - wg.Add(1) - go func() { - defer wg.Done() - if err := serveAPI(ctx, cfg); err != nil { - logger.Errorf("failed serving api: %v", err) - } - }() + if !cfg.DisableAPI { + wg.Add(1) + go func() { + defer wg.Done() + if err := serveAPI(ctx, cfg); err != nil { + logger.Errorf("failed serving api: %v", err) + } + }() + } - wg.Add(1) - go func() { - defer wg.Done() - if err := runControllers(ctx, cfg); err != nil { - logger.Errorf("failed running controllers: %v", err) - } - }() + if !cfg.DisableReconcile { + wg.Add(1) + go func() { + defer wg.Done() + if err := runControllers(ctx, cfg); err != nil { + logger.Errorf("failed running controllers: %v", err) + } + }() + } <-sigChan logger.Debug("shutdown signal received, waiting for work to finish") @@ -92,19 +101,12 @@ func runServer(ctx context.Context, cfg *config.Config) error { func serveAPI(ctx context.Context, cfg *config.Config) error { logger := log.GetLogger(ctx) - // TODO: Use DI framework to inject these ------- - containerdClient, err := containerd.New(cfg.ContainerdSocketPath) + ports, err := inject.InitializePorts(cfg) if err != nil { - return fmt.Errorf("creating containerd client: %w", err) + return fmt.Errorf("initializing ports for application: %w", err) } - repo := reignite_ctr.NewMicroVMRepoWithClient(containerdClient) - eventSvc := reignite_ctr.NewEventServiceWithClient(containerdClient) - idSvc := ulid.New() - mvmprovider := firecracker.New(&cfg.Firecracker) - - app := application.New(repo, eventSvc, idSvc, mvmprovider) - server := microvmgrpc.NewServer(app, app) - // END todo ----------------------------------------- + app := inject.InitializeApp(cfg, ports) + server := inject.InitializeGRPCServer(app) grpcServer := grpc.NewServer( grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), @@ -137,22 +139,15 @@ func serveAPI(ctx context.Context, cfg *config.Config) error { func runControllers(ctx context.Context, cfg *config.Config) error { logger := log.GetLogger(ctx) - // TODO: Use DI framework to inject these ------- - containerdClient, err := containerd.New(cfg.ContainerdSocketPath) + ports, err := inject.InitializePorts(cfg) if err != nil { - return fmt.Errorf("creating containerd client: %w", err) + return fmt.Errorf("initializing ports for controller: %w", err) } - repo := reignite_ctr.NewMicroVMRepoWithClient(containerdClient) - eventSvc := reignite_ctr.NewEventServiceWithClient(containerdClient) - idSvc := ulid.New() - mvmprovider := firecracker.New(&cfg.Firecracker) - - app := application.New(repo, eventSvc, idSvc, mvmprovider) - mvmControllers := controllers.New(eventSvc, app) - // END todo ----------------------------------------- + app := inject.InitializeApp(cfg, ports) + mvmControllers := inject.InializeController(app, ports) logger.Info("starting microvm controller") - if err := mvmControllers.Run(ctx, 1); err != nil { + if err := mvmControllers.Run(ctx, 1, cfg.ResyncPeriod, true); err != nil { logger.Fatalf("starting microvm controller: %v", err) } diff --git a/internal/config/config.go b/internal/config/config.go index db968f1b..4b77d83f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,8 @@ package config import ( - "github.com/weaveworks/reignite/infrastructure/firecracker" + "time" + "github.com/weaveworks/reignite/pkg/log" ) @@ -15,10 +16,28 @@ type Config struct { GRPCAPIEndpoint string // HTTPAPIEndpoint is the endpoint for the HHTP proxy for the gRPC service.. HTTPAPIEndpoint string - // ContainerdSocketPath is the path to the containerd socket. - ContainerdSocketPath string - // ContainerdNamespace is the default containerd namespace to use - ContainerdNamespace string - // Firecracker is the configuration for the firecracker provider - Firecracker firecracker.Config + // FirecrackerBin is the firecracker binary to use. + FirecrackerBin string + // FirecrackerDetatch indicates if the child firecracker processes should be detached from their parent. + FirecrackerDetatch bool + // FirecrackerUseAPI indicates that we should configure the microvm using the api and not a config file. + FirecrackerUseAPI bool + // StateRootDir is the directory to act as the root for the runtime state of reignite. + StateRootDir string + // ParentIface is the name of the network interface to use for the parent in macvtap interfaces. + ParentIface string + // CtrSnapshotterKernel is the name of the containerd snapshotter to use for kernel images. + CtrSnapshotterKernel string + // CtrSnapshotterVolume is the name of the containerd snapshotter to use for volume (inc initrd) images. + CtrSnapshotterVolume string + // CtrSocketPath is the path to the containerd socket. + CtrSocketPath string + // CtrNamespace is the default containerd namespace to use + CtrNamespace string + // DisableReconcile is used to stop the reconcile part from running. + DisableReconcile bool + // DisableAPI is used to disable the api server. + DisableAPI bool + // ResyncPeriod defines the period when we should do a reconcile of the microvms (even if there are no events). + ResyncPeriod time.Duration } diff --git a/internal/inject/wire.go b/internal/inject/wire.go new file mode 100644 index 00000000..6275733d --- /dev/null +++ b/internal/inject/wire.go @@ -0,0 +1,113 @@ +//go:build wireinject +// +build wireinject + +package inject + +import ( + "fmt" + + "github.com/google/wire" + "github.com/spf13/afero" + + "github.com/weaveworks/reignite/core/application" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/infrastructure/containerd" + "github.com/weaveworks/reignite/infrastructure/controllers" + "github.com/weaveworks/reignite/infrastructure/firecracker" + microvmgrpc "github.com/weaveworks/reignite/infrastructure/grpc" + "github.com/weaveworks/reignite/infrastructure/network" + "github.com/weaveworks/reignite/infrastructure/ulid" + "github.com/weaveworks/reignite/internal/config" +) + +func InitializePorts(cfg *config.Config) (*ports.Collection, error) { + wire.Build(containerd.NewEventService, + containerd.NewImageService, + containerd.NewMicroVMRepo, + ulid.New, + firecracker.New, + network.New, + appPorts, + containerdConfig, + firecrackerConfig, + networkConfig, + afero.NewOsFs) + + return nil, nil +} + +func InitializeApp(cfg *config.Config, ports *ports.Collection) application.App { + wire.Build(application.New, appConfig) + + return nil +} + +func InializeController(app application.App, ports *ports.Collection) *controllers.MicroVMController { + wire.Build(controllers.New, eventSvcFromScope, reconcileUCFromApp) + + return nil +} + +func InitializeGRPCServer(app application.App) ports.MicroVMGRPCService { + wire.Build(microvmgrpc.NewServer, queryUCFromApp, commandUCFromApp) + + return nil +} + +func containerdConfig(cfg *config.Config) *containerd.Config { + return &containerd.Config{ + SnapshotterKernel: cfg.CtrSnapshotterKernel, + SnapshotterVolume: cfg.CtrSnapshotterVolume, + SocketPath: cfg.CtrSocketPath, + Namespace: cfg.CtrNamespace, + } +} + +func firecrackerConfig(cfg *config.Config) *firecracker.Config { + return &firecracker.Config{ + FirecrackerBin: cfg.FirecrackerBin, + RunDetached: cfg.FirecrackerDetatch, + APIConfig: cfg.FirecrackerUseAPI, + StateRoot: fmt.Sprintf("%s/vm", cfg.StateRootDir), + } +} + +func networkConfig(cfg *config.Config) *network.Config { + return &network.Config{ + ParentDeviceName: cfg.ParentIface, + } +} + +func appConfig(cfg *config.Config) *application.Config { + return &application.Config{ + RootStateDir: cfg.StateRootDir, + } +} + +func appPorts(repo ports.MicroVMRepository, prov ports.MicroVMService, es ports.EventService, is ports.IDService, ns ports.NetworkService, ims ports.ImageService, fs afero.Fs) *ports.Collection { + return &ports.Collection{ + Repo: repo, + Provider: prov, + EventService: es, + IdentifierService: is, + NetworkService: ns, + ImageService: ims, + FileSystem: fs, + } +} + +func eventSvcFromScope(ports *ports.Collection) ports.EventService { + return ports.EventService +} + +func reconcileUCFromApp(app application.App) ports.ReconcileMicroVMsUseCase { + return app +} + +func queryUCFromApp(app application.App) ports.MicroVMQueryUseCases { + return app +} + +func commandUCFromApp(app application.App) ports.MicroVMCommandUseCases { + return app +} diff --git a/internal/inject/wire_gen.go b/internal/inject/wire_gen.go new file mode 100644 index 00000000..bfbbb324 --- /dev/null +++ b/internal/inject/wire_gen.go @@ -0,0 +1,128 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package inject + +import ( + "fmt" + + "github.com/spf13/afero" + "github.com/weaveworks/reignite/core/application" + "github.com/weaveworks/reignite/core/ports" + "github.com/weaveworks/reignite/infrastructure/containerd" + "github.com/weaveworks/reignite/infrastructure/controllers" + "github.com/weaveworks/reignite/infrastructure/firecracker" + "github.com/weaveworks/reignite/infrastructure/grpc" + "github.com/weaveworks/reignite/infrastructure/network" + "github.com/weaveworks/reignite/infrastructure/ulid" + "github.com/weaveworks/reignite/internal/config" +) + +// Injectors from wire.go: + +func InitializePorts(cfg *config.Config) (*ports.Collection, error) { + config2 := containerdConfig(cfg) + microVMRepository, err := containerd.NewMicroVMRepo(config2) + if err != nil { + return nil, err + } + config3 := firecrackerConfig(cfg) + config4 := networkConfig(cfg) + networkService := network.New(config4) + fs := afero.NewOsFs() + microVMService := firecracker.New(config3, networkService, fs) + eventService, err := containerd.NewEventService(config2) + if err != nil { + return nil, err + } + idService := ulid.New() + imageService, err := containerd.NewImageService(config2) + if err != nil { + return nil, err + } + collection := appPorts(microVMRepository, microVMService, eventService, idService, networkService, imageService, fs) + return collection, nil +} + +func InitializeApp(cfg *config.Config, ports2 *ports.Collection) application.App { + applicationConfig := appConfig(cfg) + app := application.New(applicationConfig, ports2) + return app +} + +func InializeController(app application.App, ports2 *ports.Collection) *controllers.MicroVMController { + eventService := eventSvcFromScope(ports2) + reconcileMicroVMsUseCase := reconcileUCFromApp(app) + microVMController := controllers.New(eventService, reconcileMicroVMsUseCase) + return microVMController +} + +func InitializeGRPCServer(app application.App) ports.MicroVMGRPCService { + microVMCommandUseCases := commandUCFromApp(app) + microVMQueryUseCases := queryUCFromApp(app) + microVMGRPCService := grpc.NewServer(microVMCommandUseCases, microVMQueryUseCases) + return microVMGRPCService +} + +// wire.go: + +func containerdConfig(cfg *config.Config) *containerd.Config { + return &containerd.Config{ + SnapshotterKernel: cfg.CtrSnapshotterKernel, + SnapshotterVolume: cfg.CtrSnapshotterVolume, + SocketPath: cfg.CtrSocketPath, + Namespace: cfg.CtrNamespace, + } +} + +func firecrackerConfig(cfg *config.Config) *firecracker.Config { + return &firecracker.Config{ + FirecrackerBin: cfg.FirecrackerBin, + RunDetached: cfg.FirecrackerDetatch, + APIConfig: cfg.FirecrackerUseAPI, + StateRoot: fmt.Sprintf("%s/vm", cfg.StateRootDir), + } +} + +func networkConfig(cfg *config.Config) *network.Config { + return &network.Config{ + ParentDeviceName: cfg.ParentIface, + } +} + +func appConfig(cfg *config.Config) *application.Config { + return &application.Config{ + RootStateDir: cfg.StateRootDir, + } +} + +func appPorts(repo ports.MicroVMRepository, prov ports.MicroVMService, es ports.EventService, is ports.IDService, ns ports.NetworkService, ims ports.ImageService, fs afero.Fs) *ports.Collection { + return &ports.Collection{ + Repo: repo, + Provider: prov, + EventService: es, + IdentifierService: is, + NetworkService: ns, + ImageService: ims, + FileSystem: fs, + } +} + +func eventSvcFromScope(ports2 *ports.Collection) ports.EventService { + return ports2.EventService +} + +func reconcileUCFromApp(app application.App) ports.ReconcileMicroVMsUseCase { + return app +} + +func queryUCFromApp(app application.App) ports.MicroVMQueryUseCases { + return app +} + +func commandUCFromApp(app application.App) ports.MicroVMCommandUseCases { + return app +} diff --git a/pkg/cloudinit/metadata.go b/pkg/cloudinit/metadata.go new file mode 100644 index 00000000..6f16b2a7 --- /dev/null +++ b/pkg/cloudinit/metadata.go @@ -0,0 +1,7 @@ +package cloudinit + +type Metadata struct { + InstanceID string `yaml:"instance_id"` + LocalHostname string `yaml:"local_hostname"` + Platform string `yaml:"platform"` +} diff --git a/pkg/cloudinit/network.go b/pkg/cloudinit/network.go new file mode 100644 index 00000000..21017982 --- /dev/null +++ b/pkg/cloudinit/network.go @@ -0,0 +1,24 @@ +package cloudinit + +type Network struct { + Version int `yaml:"version"` + Ethernet map[string]*Ethernet `yaml:"ethernets"` +} + +type Ethernet struct { + Match Match `yaml:"match"` + Addresses []string `yaml:"addresses,omitempty"` + GatewayIPv4 *string `yaml:"gateway4,omitempty"` + DHCP4 *bool `yaml:"dhcp4,omitempty"` + Nameservers *Nameservers `yaml:"nameservers,omitempty"` +} + +type Match struct { + MACAddress *string `yaml:"macaddress,omitempty"` + Name *string `yaml:"name,omitempty"` +} + +type Nameservers struct { + Search []string `yaml:"search,omitempty"` + Addresses []string `yaml:"addresses,omitempty"` +} diff --git a/pkg/cloudinit/userdata.go b/pkg/cloudinit/userdata.go new file mode 100644 index 00000000..b0ddd934 --- /dev/null +++ b/pkg/cloudinit/userdata.go @@ -0,0 +1,30 @@ +package cloudinit + +type UserData struct { + HostName *string `yaml:"hostname,omitempty"` + Fqdn *string `yaml:"fqdn,omitempty"` + Users *[]User `yaml:"users,omitempty"` + SSHPasswordAuth *bool `yaml:"ssh_pwauth,omitempty"` + DisableRoot *bool `yaml:"disable_root,omitempty"` + PackageUpdate *bool `yaml:"package_update,omitempty"` + FinalMessage *string `yaml:"final_message,omitempty"` + WriteFiles *[]WriteFile `yaml:"write_files,omitempty"` + RunCommands *[]string `yaml:"runcmd,omitempty"` +} + +type User struct { + Name string `yaml:"name"` + Sudo *string `yaml:"sudo,omitempty"` + Groups *string `yaml:"groups,omitempty"` + Home *string `yaml:"home,omitempty"` + Shell *string `yaml:"shell,omitempty"` + LockPasswd *bool `yaml:"lock_passwd,omitempty"` + SSHAuthorizedKeys *[]string `yaml:"ssh_authorized_keys,omitempty"` +} + +type WriteFile struct { + Encoding string `yaml:"encoding"` + Content string `yaml:"content"` + Path string `yaml:"path"` + Permissions string `yaml:"permissions"` +} diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index fb0c7ed9..f89d53d9 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -1,5 +1,9 @@ package defaults +import ( + "time" +) + const ( // Domain is the reverse order domain name to use. Domain = "works.weave.reignited" @@ -10,15 +14,29 @@ const ( // ContainerdSocket is the defaults path for the containerd socket. ContainerdSocket = "/run/containerd/containerd.sock" - // ContainerdSnapshotter is the name of the default snapshotter to use for containerd. - ContainerdSnapshotter = "devmapper" + // ContainerdVolumeSnapshotter is the name of the default snapshotter to use for volumes. + ContainerdVolumeSnapshotter = "devmapper" + + // ContainerdKernelSnapshotter is the name of the default snapshotter to use for kernek/initrd. + ContainerdKernelSnapshotter = "native" // FirecrackerBin is the name of the firecracker binary. FirecrackerBin = "firecracker" + // FirecrackerDetach is the default for the flag to indicates with the child firecracker + // processes should be run detached. + FirecrackerDetach = false + + // FirecrackerUseAPI is the default that indicates the Firecracker microvm should be configured + // using the API instead of using a config file. + FirecrackerUseAPI = true + // ConfigurationDir is the default configuration directory. ConfigurationDir = "/etc/opt/reignited" + // StateRootDir is the default directory to use for state information. + StateRootDir = "/var/lib/reignite" + // GRPCEndpoint is the endpoint for the gRPC server. GRPCAPIEndpoint = "localhost:9090" @@ -30,4 +48,13 @@ const ( // MicroVMNamespace is the default namespace to use for microvms. MicroVMNamespace = "default" + + // ResyncPeriod is the default resync period duration. + ResyncPeriod time.Duration = 10 * time.Minute + + // DataDirPerm is the permissions to use for data folders. + DataDirPerm = 0o755 + + // DataFilePerm is the permissions to use for data files. + DataFilePerm = 0o644 ) diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index cfafbbed..2b21861c 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/viper" ) -// BindCommandFlagsToViper will bind the flags on a command to viper. +// BindCommandToViper will bind the flags on a command to viper. func BindCommandToViper(cmd *cobra.Command) { bindFlagsToViper(cmd.PersistentFlags()) bindFlagsToViper(cmd.Flags()) diff --git a/pkg/planner/actuator.go b/pkg/planner/actuator.go index 441ca614..513835e4 100644 --- a/pkg/planner/actuator.go +++ b/pkg/planner/actuator.go @@ -74,7 +74,6 @@ func (e *actuatorImpl) executePlan(ctx context.Context, p Plan, logger *logrus.E func (e *actuatorImpl) react(ctx context.Context, steps []Procedure, logger *logrus.Entry) (int, error) { var childSteps []Procedure - var err error numStepsExecuted := 0 for _, step := range steps { @@ -84,10 +83,16 @@ func (e *actuatorImpl) react(ctx context.Context, steps []Procedure, logger *log return numStepsExecuted, ctx.Err() //nolint:wrapcheck default: - numStepsExecuted++ - childSteps, err = step.Do(ctx) + shouldDo, err := step.ShouldDo(ctx) if err != nil { - return numStepsExecuted, fmt.Errorf("executing step %s: %w", step.Name(), err) + return numStepsExecuted, fmt.Errorf("checking if step %s should be executed: %w", step.Name(), err) + } + if shouldDo { + numStepsExecuted++ + childSteps, err = step.Do(ctx) + if err != nil { + return numStepsExecuted, fmt.Errorf("executing step %s: %w", step.Name(), err) + } } } if len(childSteps) > 0 { diff --git a/pkg/planner/actuator_test.go b/pkg/planner/actuator_test.go index 2ee8aecd..620a17fd 100644 --- a/pkg/planner/actuator_test.go +++ b/pkg/planner/actuator_test.go @@ -165,3 +165,7 @@ func (p *testProc) Do(ctx context.Context) ([]planner.Procedure, error) { return p.ChildProcs, nil } + +func (p *testProc) ShouldDo(ctx context.Context) (bool, error) { + return true, nil +} diff --git a/pkg/planner/planner.go b/pkg/planner/planner.go index 78c80e69..7bd1352d 100644 --- a/pkg/planner/planner.go +++ b/pkg/planner/planner.go @@ -13,16 +13,16 @@ type Plan interface { // Create will perform the plan and will return a list of operations/procedures // that need to be run to accomplish the plan Create(ctx context.Context) ([]Procedure, error) - - // Result is the result of the plan - Result() interface{} } // Procedure represents a procedure/operation that will be carried out -// as part of executing a plan. +// as part of executing a plan. All procedures must be idempotent, so they +// need to measure and then act. type Procedure interface { // Name is the name of the procedure/operation. Name() string // Do will perform the operation/procedure. Do(ctx context.Context) ([]Procedure, error) + // ShouldDo determines if this procedure should be executed + ShouldDo(ctx context.Context) (bool, error) } diff --git a/pkg/process/process.go b/pkg/process/process.go new file mode 100644 index 00000000..d823065a --- /dev/null +++ b/pkg/process/process.go @@ -0,0 +1,55 @@ +package process + +import ( + "fmt" + "os" + "os/exec" + "syscall" +) + +// StartCommandDetached will start the given cmd so its detached from its parent process. +func StartCommandDetached(cmd *exec.Cmd, stdErrFile *os.File, stdOutFile *os.File) (*os.Process, error) { + groups, err := os.Getgroups() + if err != nil { + return nil, fmt.Errorf("get os groups: %w", err) + } + groupsConv := []uint32{} + for _, groupID := range groups { + groupsConv = append(groupsConv, uint32(groupID)) + } + + files := []*os.File{nil, stdOutFile, stdErrFile} + + attr := &os.ProcAttr{ + Dir: "./", + Files: files, + Sys: &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + Groups: groupsConv, + }, + Setsid: true, + }, + } + + proc, err := os.StartProcess(cmd.Path, cmd.Args, attr) + if err != nil { + return nil, fmt.Errorf("starting process: %w", err) + } + + return proc, nil +} + +// Exists will check if a process exists with the given pid. +func Exists(pid int) (bool, error) { + proc, err := os.FindProcess(pid) + if err != nil { + return false, fmt.Errorf("finding process with pid %d: %w", pid, err) + } + + err = proc.Signal(syscall.Signal(0)) + exists := err == nil + + return exists, nil +} diff --git a/pkg/ptr/ptr.go b/pkg/ptr/ptr.go new file mode 100644 index 00000000..acc2c954 --- /dev/null +++ b/pkg/ptr/ptr.go @@ -0,0 +1,9 @@ +package ptr + +func Bool(val bool) *bool { + return &val +} + +func String(val string) *string { + return &val +} diff --git a/pkg/wait/wait.go b/pkg/wait/wait.go new file mode 100644 index 00000000..f07c4ad3 --- /dev/null +++ b/pkg/wait/wait.go @@ -0,0 +1,47 @@ +package wait + +import ( + "errors" + "fmt" + "time" + + "github.com/spf13/afero" +) + +// ErrWaitTimeout is an error when a condition hasn't been met within the supplied max wait duration. +var ErrWaitTimeout = errors.New("timeout waiting for condition") + +// ConditionFunc is a function type that is used to implement a wait condition. +type ConditionFunc func() (bool, error) + +// ForCondition will wait for the specified condition to be true until the max duration. +func ForCondition(conditionFn ConditionFunc, maxWait time.Duration, checkInternal time.Duration) error { + timeout := time.NewTimer(maxWait) + checkTicker := time.NewTicker(checkInternal) + defer checkTicker.Stop() + defer timeout.Stop() + + for { + conditionMet, err := conditionFn() + if err != nil { + return fmt.Errorf("checking if condition met: %w", err) + } + if conditionMet { + return nil + } + + select { + case <-timeout.C: + return ErrWaitTimeout + case <-checkTicker.C: + continue + } + } +} + +// FileExistsCondition creates a condition check on the existence of a file. +func FileExistsCondition(filepath string, fs afero.Fs) ConditionFunc { + return func() (bool, error) { + return afero.Exists(fs, filepath) //nolint: wrapcheck + } +} diff --git a/pkg/wait/wait_test.go b/pkg/wait/wait_test.go new file mode 100644 index 00000000..1be98a0f --- /dev/null +++ b/pkg/wait/wait_test.go @@ -0,0 +1,63 @@ +package wait_test + +import ( + "sync" + "testing" + "time" + + . "github.com/onsi/gomega" + "github.com/spf13/afero" + + "github.com/weaveworks/reignite/pkg/wait" +) + +func TestConditionMet(t *testing.T) { + RegisterTestingT(t) + + fs := afero.NewMemMapFs() + maxWait := 300 * time.Millisecond + pollInterval := 100 * time.Millisecond + testFile := "/test.txt" + + var conditionErr error + var createErr error + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + conditionErr = wait.ForCondition(wait.FileExistsCondition(testFile, fs), maxWait, pollInterval) + }() + go func() { + defer wg.Done() + time.Sleep(50 * time.Millisecond) + _, createErr = fs.Create(testFile) + }() + + wg.Wait() + Expect(conditionErr).NotTo(HaveOccurred()) + Expect(createErr).NotTo(HaveOccurred()) +} + +func TestConditionTimeout(t *testing.T) { + RegisterTestingT(t) + + fs := afero.NewMemMapFs() + maxWait := 50 * time.Millisecond + pollInterval := 25 * time.Millisecond + testFile := "/test.txt" + + var conditionErr error + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + conditionErr = wait.ForCondition(wait.FileExistsCondition(testFile, fs), maxWait, pollInterval) + }() + + wg.Wait() + Expect(conditionErr).To(HaveOccurred()) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 8c716309..dca8ebf4 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1,3 +1,4 @@ +//go:build e2e // +build e2e package e2e_test